Skip to content

Split into tensorcodec (pure Python) and tensorcodec-av; BMP; OpenCV >=4.12 - #14

Merged
MilkClouds merged 12 commits into
mainfrom
feat/pure-python-fallback
Oct 4, 2026
Merged

MilkClouds merged 12 commits into
mainfrom
feat/pure-python-fallback

Conversation

@MilkClouds

@MilkClouds MilkClouds commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Image codecs only need OpenCV, but tensorcodec 0.2.0 was one maturin package. Its import loaded the Rust extension, and installing on Windows or Intel macOS fell back to an sdist that needs Rust and FFmpeg. This PR splits the project into two distributions, the way pydantic and pydantic-core are split. It also adds BMP to decode_image and replaces the OpenCV bound with a measured one. Versions go to 0.3.0, since 0.2.0 is already published as a single package. Nothing is released by this PR.

Size: +829/−309 across 40 files. Most of it is moved workflow steps, the copied av/LICENSE (+204) and tests; the runtime code change is about 50 lines.

Packaging split

PyPI project Contents Build backend Distributions License metadata
tensorcodec (root pyproject.toml) Python API, image decoders/encoders (import tensorcodec) hatchling py3-none-any wheel, sdist Apache-2.0, LICENSE only, OS Independent
tensorcodec-av (av/pyproject.toml, new) Only the Rust/FFmpeg extension (import tensorcodec_av) maturin abi3 manylinux x86_64/aarch64 and macOS 14 arm64 wheels, sdist Apache-2.0 AND LGPL-3.0-or-later AND BSD-2-Clause; the FFmpeg/dav1d/OpenSSL notices moved to av/licenses/

tensorcodec-av (the audio/video decoders; images live in tensorcodec) is unregistered on PyPI as of 2026-10-05. The Rust package lives in av/; the extension module is tensorcodec_av._av.

  • Dependency. tensorcodec requires tensorcodec-av==<same version> behind the marker platform_python_implementation == 'CPython' and ((sys_platform == 'linux' and platform_machine in {x86_64, aarch64}) or (sys_platform == 'darwin' and platform_machine == 'arm64')).

    • So pip install tensorcodec behaves as before where tensorcodec-av wheels exist.
    • On Windows, Intel macOS, PyPy and 32-bit ARM it installs pure Python, with no Rust toolchain needed.
    • There is no native extra. Where the marker applies it would do nothing; elsewhere it would only trigger the same source build as installing tensorcodec-av directly. The docs say to install tensorcodec-av==<same version> explicitly to build it from source (needs Rust and FFmpeg 7).
    • The marker does not compare platform_release. pip's vendored packaging 22–25 raises InvalidVersion on Linux kernel strings like 6.8.0-azure; I verified this with packaging 22.0/23.2/24.2/25.0, and a test guards it.
  • Lazy import and skew check. VideoDecoder and AudioDecoder import tensorcodec_av when constructed.

    • If it is missing, they raise an ImportError that explains which platforms have tensorcodec-av wheels.
    • The extension now exports __version__ (CARGO_PKG_VERSION). A tensorcodec-av whose version differs from tensorcodec is rejected with an explicit message.
  • Lockstep. tests/test_versions.py (runs in every CI and release job) requires pyproject.toml, the pin, av/Cargo.toml (the tensorcodec-av version source), av/Cargo.lock and tensorcodec.__version__ to agree. It also checks that av/LICENSE equals LICENSE and that the marker matches the published wheel platforms.

  • Workflows: one reusable workflow per distribution. CI and the release call the same two workflows:

    Workflow Jobs Artifacts
    build-python.yml (renamed from pure-python-wheel.yml) build: tensorcodec wheel and sdist, python -m build + twine check --strict. test-without-av: installs tensorcodec[images] with --no-index on windows-2025 (Python 3.12, latest OpenCV = 5.0) and macos-15-intel (Python 3.10, OpenCV 4.12.0.88, the lower bound), then runs the image, encoder, optional-native and version tests dist-python
    build-av.yml (replaces macos-wheels.yml and the inline Linux jobs in publish.yml) linux (x86_64, aarch64): manylinux2014 via maturin-action with the existing native-deps cache, size check, glibc 2.17 Python 3.10/3.13 runtime check, contract and --compare oracle tests. macos (arm64): build, delocate, full pytest, clean 3.10/3.13 venvs. sdist. The wheel jobs validate against the dist-python wheel, so callers run build-python.yml first dist-av-linux-{x86_64,aarch64}, dist-av-macos-arm64, dist-av-sdist, wheel-size-linux-*
    ci.yml python → av, plus test (development build against conda-forge FFmpeg, Clippy, contract and --compare)
    publish.yml python → av → publish (tensorcodec-av first, so the exact pin never points at a missing release, then tensorcodec) → update-size-docs (now measures tensorcodec-av)
    • ci.yml runs av on every push and PR, so a release commit has already passed the same Linux and macOS release builds.
    • With the native-deps caches warm, each Linux job takes about 3 minutes.
    • source.yml is removed: publish.yml --field publish=false produces the same artifacts.
    • The animated-GIF fixture uses a numbered input sequence, because Windows FFmpeg has no glob pattern support. Fixture FFmpeg comes from conda-forge, because Homebrew's build lacks libaom. The Windows OpenCV wheel has no AVIF decoder, so that test checks for the explicit error there.
  • Removed. scripts/build_pure_wheel.py.

  • Docs. README, docs/releasing.md (two-project release, pending trusted publisher for tensorcodec-av, version-bump steps, recovery if the second upload fails), docs/system_ffmpeg.md, docs/images.md and docs/package_size.md.

Upgrade path: installing the 0.3.0 tensorcodec wheel over the published 0.2.0 removes tensorcodec/_native.abi3.so and tensorcodec.libs (checked locally). On marker platforms, the dependency then installs tensorcodec-av.

Known limitation (deliberate): environment markers cannot detect musl, free-threaded CPython or macOS older than 14. On those platforms installers still try tensorcodec-av and fall back to its sdist, which is what happened before the split. The README documents pip install --no-deps tensorcodec numpy for image-only use there. The adversarial review flags this; avoiding it would require making native opt-in everywhere, which changes pip install tensorcodec on the supported platforms.

BMP

  • decode_image detects BM and decodes through cv2.imdecode, losslessly.
  • Alpha matches Pillow: only 32-bit BI_BITFIELDS files with a nonzero alpha mask keep it.
  • There is no decode_bmp, matching TorchCodec's function set.
  • TIFF is not added. OpenCV premultiplies unassociated alpha, drops gray+alpha, and turns CMYK into RGBA, so lossless decoding cannot be promised. This is documented.

OpenCV bound: >=4.12, no upper cap

Image decoder/encoder/optional-AV tests (152 with TorchCodec 0.17.0 --compare). One throwaway venv per version; Linux, Python 3.12, PyPI opencv-python-headless.

Version Result
4.8.0.76, 4.9.0.80 cv2 does not import with NumPy 2
4.10.0.84 everything passes except GIF (non-oracle run)
4.11.0.86 124 passed, 28 failed: all GIF and AVIF (OpenCV could not decode)
4.12.0.88 152 passed
4.13.0.92 152 passed
5.0.0.93 152 passed
  • OpenCV's changelog lists the GIF decoder in 4.11 (#25691), but the 4.11 PyPI wheels report GIF: NO and AVIF: NO in cv2.getBuildInformation(). 4.12 reports both as YES.
  • So 4.12 is the lowest version where the full suite passes. JPEG, PNG, WebP (including animation and orientation) and BMP already pass on 4.10.
  • The runtime check in _opencv.py, the images extra and the dev group now use >=4.12, without the <5 cap. The reason is recorded at the check and in docs/images.md.

Validation

  • CI (final commit a9f9e1b, all green), including the Linux release builds:
    • av / linux x86_64 and aarch64: cache hit, tensorcodec_av-0.3.0 wheel built, glibc 2.17 runtime checks, 67 contract and 462 --compare tests passed.
    • av / macos arm64: 316 passed.
    • av / sdist, python / build, and python / test-without-av on Windows and Intel macOS.
    • test.
  • Local:
    • Built tensorcodec wheel and sdist: the wheel has only Python files and LICENSE. A wheel rebuilt from the sdist works, and the version tests pass in the extracted sdist.
    • tensorcodec-av sdist via maturin contains only the Rust sources and the notices.
    • Fresh copy-mode venvs (no site-packages edits): image tests pass on OpenCV 4.12 and 5.0, both with --compare.
  • Codex review (review --scope branch, final state after the trim): no actionable regressions. Earlier rounds found the reuse of version 0.2.0, the platform_release marker crash and the sdist missing native/; all three are fixed.
  • Adversarial review: remaining finding is the known limitation above.

Release steps (not done here)

  1. On PyPI, add tensorcodec-av as a pending trusted publisher (same repository, publish.yml, environment pypi).
  2. Merge, then run Publish to PyPI on main. A --field publish=false dry run is possible first.
  3. Verify that pip install tensorcodec==0.3.0 pulls tensorcodec-av==0.3.0 on Linux and macOS arm64, and installs pure Python on Windows.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS

MilkClouds and others added 8 commits October 5, 2026 02:16
Image codecs only need OpenCV, but importing tensorcodec.decoders loaded
the Rust extension, so they were unusable where no native wheel exists.
VideoDecoder/AudioDecoder now import it on construction and raise a clear
ImportError when it is missing. scripts/build_pure_wheel.py builds a
py3-none-any wheel with the same [project] metadata; CI builds it, tests
it on Windows, Intel macOS and Linux, and publish uploads it with the
native wheels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
OpenCV decodes BMP losslessly. Alpha follows Pillow: only 32-bit
BI_BITFIELDS with a nonzero alpha mask keeps it, since OpenCV otherwise
returns the padding byte as a fourth channel. TIFF stays unsupported
because OpenCV premultiplies unassociated alpha and drops gray+alpha.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
FFmpeg's glob pattern type is unavailable on Windows, so the animated GIF
fixture uses a numbered sequence. Homebrew's FFmpeg has no libaom, so the
fallback jobs use the conda-forge FFmpeg the other workflows already pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
The Windows opencv-python-headless 4.14 wheel cannot decode AVIF; the
dispatch test now checks the explicit error there instead of failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
Replaces the copy-and-rebuild fallback wheel. tensorcodec is built by
hatchling as a py3-none-any wheel with the Python API and image codecs.
native/ is the maturin project for tensorcodec-native, which contains only
the FFmpeg extension (import tensorcodec_native) and the bundled-library
notices. tensorcodec pins tensorcodec-native to its own version behind a
marker matching the native wheel tags, plus a `native` extra; the version
test and a runtime check reject skew. Publish uploads tensorcodec-native
before tensorcodec.

The OpenCV bound is now measured: the image suite, including TorchCodec
comparisons, passes with 4.12, 4.13 and 5.0; 4.10/4.11 wheels lack the GIF
and AVIF decoders. The extra becomes >=4.12 without the <5 cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
0.2.0 is an existing single-package release: reusing it would collide with
its sdist and leave installed 0.2.0 environments un-upgraded. The version
test also checks Cargo.lock and skips native checks in the tensorcodec
sdist, which does not contain native/. README notes the --no-deps route
for Linux environments the marker cannot exclude (musl, free-threaded).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
pip's vendored packaging 22-25 raises InvalidVersion when comparing
platform_release on Linux kernels such as 6.8.0-azure, which would break
installs on supported Linux. macOS arm64 older than 14 now tries the
native sdist, as it did before the split. The tensorcodec sdist includes
native/ so its uv source and version tests work from the archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
@MilkClouds MilkClouds changed the title Image codecs without the native extension: lazy loading, pure-Python wheel, BMP Split into tensorcodec (pure Python) and tensorcodec-native; BMP; OpenCV >=4.12 Oct 4, 2026
MilkClouds and others added 2 commits October 5, 2026 03:23
Where the marker applies it is a no-op; elsewhere it only triggers the
same source build as requesting tensorcodec-native directly, while
suggesting that it enables video on Windows. The docs now say to install
tensorcodec-native==<same version> explicitly to build from source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
build-python.yml builds and tests tensorcodec (artifact dist-python);
build-native.yml builds and validates the manylinux x86_64/aarch64 and
macOS arm64 wheels and the sdist of tensorcodec-native (dist-native-*).
CI now runs the Linux release builds too: on every push and on PRs
touching what they build or validate. publish.yml calls both, checks the
release set (one version, six files) and uploads native first. The
duplicated sdist steps and source.yml are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
@MilkClouds MilkClouds changed the title Split into tensorcodec (pure Python) and tensorcodec-native; BMP; OpenCV >=4.12 Split into tensorcodec (pure Python) and tensorcodec-native; image codecs without the native extension, BMP, OpenCV >= 4.12 Oct 4, 2026
The compiled distribution is the audio/video codec implementation;
images live in tensorcodec. Renamed consistently: PyPI name, import
package tensorcodec_av with extension module _av, Cargo package and lib,
the native/ directory (now av/), the pin, ImportError messages, tests,
workflows (build-av.yml, dist-av-* artifacts, av jobs) and docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
@MilkClouds MilkClouds changed the title Split into tensorcodec (pure Python) and tensorcodec-native; image codecs without the native extension, BMP, OpenCV >= 4.12 Split into tensorcodec (pure Python) and tensorcodec-av; BMP; OpenCV >=4.12 Oct 4, 2026
Docs keep only what users and maintainers need; releasing.md is reduced
to the two-package release steps and the recovery step. Workflows drop
the path filter (CI always runs build-av.yml), the publish release-set
check (needs already require every artifact), the single-entry macOS
matrix and redundant checks. Tests merge overlapping cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAijSnAE4aAjuS1EcCTwqS
@MilkClouds
MilkClouds merged commit c3cd74b into main Oct 4, 2026
8 checks passed
@MilkClouds
MilkClouds deleted the feat/pure-python-fallback branch October 4, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant