Assemble a standard .glb from a Meshy AI .meshy model container
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-30 02:58:00 +02:00
.gitignore feat: meshy .meshy container to standard glb extractor 2026-09-30 02:51:25 +02:00
extract.py feat: meshy .meshy container to standard glb extractor 2026-09-30 02:51:25 +02:00
README.md docs: credit meshy container format sources 2026-09-30 02:58:00 +02:00

meshy-extract

Assemble a standard .glb from a Meshy AI .meshy model container, without an account, the site UI, or the WASM decoder the viewer uses.

Why the download button is not enough

The page exposes only:

  • preview.png and a watermark-free 4K PBR set (colour / metallic-roughness / normal / emission) as plain signed CloudFront URLs, and
  • model.meshy, which is not a mesh — it is an encrypted container.

model.json is a 43-byte stub ({"verts_count":...,"faces_count":...}), so no URL swap or extension change yields geometry. The mesh only exists inside model.meshy, and the site's mesh_loader.wasm refuses to decode it unless authorize(hostname, timestamp, signature) passes. That gate is an access-control layer for the viewer, not part of the file format: the container is decryptable with a public, fixed key.

Container format

offset        bytes   content
0..7          8       magic "MESHY.AI"
8..9          2       version (uint16 LE, observed: 1)
10..21        12      AES nonce
22..31        10      reserved
32..8224      8192    AES-256-CTR ciphertext
                      (GLB header + glTF JSON + BIN chunk header)
8224..8240    16      AES-GCM authentication tag
8240..EOF     rest    plaintext (WebP textures + meshopt streams)
  • Cipher: AES-256-CTR.
  • Key: the 32-byte ASCII constant b'JSON{"accessors":[{"bufferView":' — a known-plaintext key, not a secret.
  • Counter block: nonce || uint32be(2), incremented per 16-byte block.
  • The GCM tag is present but not required to recover the plaintext.

The decrypted stream is a real GLB whose geometry bufferViews carry EXT_meshopt_compression, so a second step decompresses them with meshoptimizer into raw vertex/index buffers.

Usage

Requires uv. Dependencies are declared inline (PEP 723), so nothing needs installing first.

The single argument accepts either a task id (UUID) or a URL. A bare id, a model page URL, or a task API URL is reduced to its UUID and resolved through the public task API; a .meshy CDN URL is used as-is.

# bare task id
uv run extract.py <task-uuid>

# model page url
uv run extract.py 'https://www.meshy.ai/3d-models/<slug>-<task-uuid>'

# task api url
uv run extract.py 'https://www.meshy.ai/meshyd-api/web/public/v2/tasks/<task-uuid>'

# signed .meshy CDN url captured in devtools
uv run extract.py 'https://api.meshy.ai/misc/cdn-models/<uid>/tasks/<task-uuid>/output/model.meshy?sign=...'

Options:

  • -o, --output output path (default out/<task-id>.glb; the directory is gitignored)
  • --keep-webp keep the embedded WebP textures instead of converting to PNG

Texture note

The bundled textures are WebP and the material keeps them inside EXT_texture_webp. The script unwraps that extension and re-encodes the images to PNG by default, because Blender's glTF importer (and several other tools) do not read WebP. Pass --keep-webp only if your consumer supports WebP and you want the smaller file.

Pipeline

  1. Resolve — a bare id or any non-.meshy URL is reduced to its task UUID and looked up through the public task API (/meshyd-api/web/public/v2/tasks/<uuid>) to get the signed model.meshy URL; a .meshy URL is used directly.
  2. Download the container.
  3. Decrypt the 8192-byte header block (AES-256-CTR) and splice it onto the plaintext tail, skipping the 16-byte GCM tag.
  4. Decompress every bufferView carrying EXT_meshopt_compression (ATTRIBUTES / TRIANGLES / INDICES modes, octahedral/quaternion/exponential filters) into raw buffers.
  5. Rebuild a clean single-buffer GLB: empty and meshopt side-buffer views are dropped, accessors and images are remapped, offsets are re-aligned.

Files

  • extract.py — the extractor (single file, inline dependencies).
  • out/ — default output directory (gitignored).

Output validation

The produced file is a plain single-buffer glTF 2.0 GLB (PNG textures, no extensions) that loads in any standards-compliant importer; the pipeline's output passes @gltf-transform/cli inspect and Blender's glTF importer.

Credits

Only run this on models you are entitled to use. Community models on Meshy are published under the licence shown on the model page. The .meshy container format was documented by public reverse engineering; the key is a known plaintext constant and the WASM authorization gate protects the site's viewer, not the file.