tensorcodec 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/PKG-INFO +64 -46
  2. tensorcodec-0.1.1/README.md +142 -0
  3. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/docs/releasing.md +16 -10
  4. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/native/Cargo.lock +1 -1
  5. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/native/Cargo.toml +1 -1
  6. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/native/src/ffmpeg.rs +1 -1
  7. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/pyproject.toml +2 -2
  8. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/scripts/build_linux_wheel.sh +3 -2
  9. tensorcodec-0.1.1/scripts/build_nasm.sh +13 -0
  10. tensorcodec-0.1.1/scripts/check_wheel_runtime.py +75 -0
  11. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/src/tensorcodec/__init__.py +1 -1
  12. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/src/tensorcodec/decoders/_decoder.py +5 -14
  13. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/tests/test_differential.py +3 -9
  14. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/tests/test_video_contract.py +1 -3
  15. tensorcodec-0.1.0/README.md +0 -124
  16. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/LICENSE +0 -0
  17. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/docs/compatibility.md +0 -0
  18. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/docs/playback_semantics.md +0 -0
  19. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/licenses/FFmpeg-GPL-3.0.txt +0 -0
  20. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/licenses/FFmpeg-LGPL-3.0.txt +0 -0
  21. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/licenses/FFmpeg-NOTICE.md +0 -0
  22. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/licenses/OpenSSL.txt +0 -0
  23. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/licenses/README.md +0 -0
  24. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/licenses/Zstandard.txt +0 -0
  25. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/native/src/lib.rs +0 -0
  26. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/scripts/build_ffmpeg.sh +0 -0
  27. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/scripts/build_openssl.sh +0 -0
  28. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/scripts/configure_oracle_ffmpeg.py +0 -0
  29. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/src/tensorcodec/_frame.py +0 -0
  30. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/src/tensorcodec/_metadata.py +0 -0
  31. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/src/tensorcodec/decoders/__init__.py +0 -0
  32. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/src/tensorcodec/py.typed +0 -0
  33. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/tests/__init__.py +0 -0
  34. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/tests/conftest.py +0 -0
  35. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/tests/test_audio_contract.py +0 -0
  36. {tensorcodec-0.1.0 → tensorcodec-0.1.1}/tests/test_runtime.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorcodec
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Operating System :: POSIX :: Linux
6
6
  Classifier: Programming Language :: Python :: 3
@@ -24,42 +24,63 @@ Project-URL: Repository, https://github.com/MilkClouds/tensorcodec
24
24
 
25
25
  # TensorCodec
26
26
 
27
+ [![CI](https://github.com/MilkClouds/tensorcodec/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/MilkClouds/tensorcodec/actions/workflows/ci.yml)
28
+ [![PyPI](https://img.shields.io/pypi/v/tensorcodec)](https://pypi.org/project/tensorcodec/)
29
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-blue)](https://pypi.org/project/tensorcodec/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue)](LICENSE)
31
+
27
32
  **TorchCodec-style video and audio decoding, without PyTorch.**
28
33
 
29
- - Use the CPU decoder API and playback rules of **TorchCodec 0.17.0**.
30
- - Get **NumPy arrays** instead of `torch.Tensor`.
31
- - Install **NumPy + TensorCodec**. No Torch, PyAV, or FFmpeg CLI at runtime.
34
+ - Use the CPU decoder API and playback rules of TorchCodec 0.17.0.
35
+ - Get NumPy arrays instead of `torch.Tensor`.
36
+ - Install NumPy + TensorCodec. No Torch, PyAV, or FFmpeg CLI at runtime.
32
37
 
33
38
  The goal is predictable frame selection, timestamps and audio ranges with a small
34
39
  runtime dependency set. This is a **CPU decoding subset**, not the entire
35
40
  TorchCodec package. It does not promise a speedup over PyAV or TorchCodec.
36
41
 
37
- ## What is available?
38
-
39
- | Capability | TorchCodec 0.17.0 | TensorCodec 0.1.0 |
40
- | --- | --- | --- |
41
- | Python runtime dependency | PyTorch | NumPy |
42
- | Output arrays | `torch.Tensor` | `numpy.ndarray`; array interface + DLPack |
43
- | Video index/slice/batch access | Supported | Supported |
44
- | Playback time/range access | Supported | Supported |
45
- | CFR, VFR, offset PTS, B-frames | Supported | Tested |
46
- | Request ordering and duplicates | Preserved | Preserved |
47
- | Exact / approximate seeking | Supported | Supported; exact is the default |
48
- | NCHW / NHWC RGB | Supported | Supported |
49
- | uint8 / float32 video | Supported | Supported for SDR |
50
- | FPS sampling, custom frame mappings | Supported | Supported |
51
- | Audio ranges, resampling, channel mixing | Supported | Supported; float32 output |
52
- | Paths, URLs, bytes, seekable file objects | Supported | Supported |
53
- | Encoded tensor input | `torch.Tensor` | 1-D uint8 NumPy arrays |
54
- | CUDA decoding | Supported | **Not implemented** |
55
- | Decoder transforms | Supported | **Not implemented** |
56
- | HDR inputs / display rotation | Supported | **Rejected explicitly** |
57
- | Other modules, including samplers/encoders | Available | **Outside the initial scope** |
42
+ ## Scope compared with TorchCodec
43
+
44
+ TorchCodec includes decoders, encoders, sampling and transforms.
45
+
46
+ Legend for both tables: ✓ supported · △ limited support · — unavailable.
47
+
48
+ | Module family | Component | TorchCodec 0.17.0 | TensorCodec 0.1.1 |
49
+ | --- | --- | :---: | --- |
50
+ | Decoders | Video · `VideoDecoder` | ✓ | △ CPU, SDR |
51
+ | | Audio · `AudioDecoder` | ✓ | △ CPU |
52
+ | | Images | ✓ | — |
53
+ | Encoders | Video / audio / JPEG / PNG | ✓ | — |
54
+ | Samplers | Clip sampling | ✓ | — |
55
+ | Transforms | Decoder transforms | ✓ | — |
56
+
57
+ FPS-based decoder queries are available; clip samplers are not implemented.
58
+
59
+ ### Decoder compatibility
60
+
61
+ | Area | Capability | TorchCodec 0.17.0 | TensorCodec 0.1.1 |
62
+ | --- | --- | :---: | --- |
63
+ | Video · selection | Index / slice / batch | ✓ | ✓ |
64
+ | | Playback time / range | ✓ | ✓ |
65
+ | | Order / duplicates preserved | ✓ | ✓ |
66
+ | | Exact / approximate seek | ✓ | ✓ Default: exact |
67
+ | | FPS queries / custom frame mappings | ✓ | ✓ |
68
+ | Video · formats | CFR / VFR / offset PTS / B-frames | ✓ | ✓ Tested |
69
+ | | NCHW / NHWC RGB | ✓ | ✓ |
70
+ | | uint8 / float32 | ✓ | △ SDR |
71
+ | | HDR transfer / display rotation | ✓ | — Explicit rejection |
72
+ | Audio | Ranges / resampling / channel mixing | ✓ | ✓ float32 |
73
+ | Input / output | Paths / URLs / bytes / seekable files | ✓ | ✓ |
74
+ | | Encoded array input | `torch.Tensor` | 1-D uint8 NumPy arrays |
75
+ | | Decoded arrays | `torch.Tensor` | `numpy.ndarray` + array interface / DLPack |
76
+ | Execution | CPU | ✓ | ✓ |
77
+ | | CUDA | ✓ | — |
78
+ | | Python runtime dependency | PyTorch | NumPy |
58
79
 
59
80
  ### Compatibility means
60
81
 
61
82
  - Match the supported CPU API's frame selection, ordering, timing and metadata.
62
- - Check behavior independently **and** against pinned TorchCodec 0.17.0.
83
+ - Check behavior independently and against pinned TorchCodec 0.17.0.
63
84
  - Allow color-conversion rounding: at most 1 uint8 unit or 1/65535 for float32
64
85
  in the tested cases. Do not claim identical pixels across every FFmpeg build.
65
86
  - Accept empty index lists, including the case affected by the reference's
@@ -69,9 +90,13 @@ Details and the tested scope: [compatibility contract](docs/compatibility.md).
69
90
 
70
91
  ### Current limits
71
92
 
72
- - Binary wheels: **Linux x86_64, glibc 2.28+, CPython 3.10+**.
73
- - No macOS or Windows wheels yet; free-threaded Python is not a release target.
93
+ - Binary wheels: **Linux x86_64 / ARM64 (aarch64), glibc 2.17+, CPython 3.10+**.
94
+ - A compatible NumPy wheel is also required. On older glibc, the installer may
95
+ select an older NumPy; newer Python versions may require a newer glibc.
96
+ - No macOS, Windows or musl/Alpine wheels yet; free-threaded Python is not a release target.
74
97
  - Exact video seeking scans packet timestamps when opening the decoder.
98
+ - Seeking trusts container keyframe flags; incorrect flags can corrupt decoded
99
+ frames. Corrected frame mappings or a repaired input are needed in that case.
75
100
  - Audio range queries currently decode from the beginning; late ranges can be
76
101
  expensive.
77
102
  - NumPy return types require caller changes where code expects Torch tensors.
@@ -80,18 +105,13 @@ Details and the tested scope: [compatibility contract](docs/compatibility.md).
80
105
  ## Install
81
106
 
82
107
  ```sh
83
- python -m pip install tensorcodec
108
+ uv pip install tensorcodec
84
109
  ```
85
110
 
111
+ Use an existing virtual environment, or create one with `uv venv` first.
86
112
  Linux wheels bundle shared FFmpeg libraries. Source builds need Rust, libclang
87
113
  and FFmpeg 7 development headers/libraries.
88
114
 
89
- Before the first PyPI upload, install a local wheel:
90
-
91
- ```sh
92
- python -m pip install dist/tensorcodec-*.whl
93
- ```
94
-
95
115
  ## Use
96
116
 
97
117
  ```python
@@ -124,17 +144,15 @@ A batch crosses the Python/Rust boundary once. Native decoding releases the GIL.
124
144
  ## Development and verification
125
145
 
126
146
  ```sh
127
- # Requires Rust, Clang/libclang, pkg-config and FFmpeg 7 development libraries.
128
- python -m venv .venv
129
- . .venv/bin/activate
130
- python -m pip install numpy pytest ruff 'maturin>=1.8,<2'
131
- maturin develop --locked
132
-
133
- # Reference dependencies are for tests only.
134
- python -m pip install torch==2.14.1 torchcodec==0.17.0 \
135
- --index-url https://download.pytorch.org/whl/cpu
136
- pytest tests/test_video_contract.py tests/test_audio_contract.py --backend torchcodec
137
- pytest --compare
147
+ # Requires uv, Rust, Clang/libclang, pkg-config and FFmpeg 7 development libraries.
148
+ # Build the editable package and install development + pinned CPU oracle groups.
149
+ uv sync --group dev --group oracle
150
+
151
+ uv run --group oracle pytest tests/test_video_contract.py tests/test_audio_contract.py --backend torchcodec
152
+ uv run --group oracle pytest --compare
153
+
154
+ # Rebuild after changing Rust code.
155
+ uv run --group oracle maturin develop --locked --uv
138
156
  ```
139
157
 
140
158
  Tests generate fixtures with FFmpeg/ffprobe and Python's `wave` module.
@@ -0,0 +1,142 @@
1
+ # TensorCodec
2
+
3
+ [![CI](https://github.com/MilkClouds/tensorcodec/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/MilkClouds/tensorcodec/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/tensorcodec)](https://pypi.org/project/tensorcodec/)
5
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-blue)](https://pypi.org/project/tensorcodec/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue)](LICENSE)
7
+
8
+ **TorchCodec-style video and audio decoding, without PyTorch.**
9
+
10
+ - Use the CPU decoder API and playback rules of TorchCodec 0.17.0.
11
+ - Get NumPy arrays instead of `torch.Tensor`.
12
+ - Install NumPy + TensorCodec. No Torch, PyAV, or FFmpeg CLI at runtime.
13
+
14
+ The goal is predictable frame selection, timestamps and audio ranges with a small
15
+ runtime dependency set. This is a **CPU decoding subset**, not the entire
16
+ TorchCodec package. It does not promise a speedup over PyAV or TorchCodec.
17
+
18
+ ## Scope compared with TorchCodec
19
+
20
+ TorchCodec includes decoders, encoders, sampling and transforms.
21
+
22
+ Legend for both tables: ✓ supported · △ limited support · — unavailable.
23
+
24
+ | Module family | Component | TorchCodec 0.17.0 | TensorCodec 0.1.1 |
25
+ | --- | --- | :---: | --- |
26
+ | Decoders | Video · `VideoDecoder` | ✓ | △ CPU, SDR |
27
+ | | Audio · `AudioDecoder` | ✓ | △ CPU |
28
+ | | Images | ✓ | — |
29
+ | Encoders | Video / audio / JPEG / PNG | ✓ | — |
30
+ | Samplers | Clip sampling | ✓ | — |
31
+ | Transforms | Decoder transforms | ✓ | — |
32
+
33
+ FPS-based decoder queries are available; clip samplers are not implemented.
34
+
35
+ ### Decoder compatibility
36
+
37
+ | Area | Capability | TorchCodec 0.17.0 | TensorCodec 0.1.1 |
38
+ | --- | --- | :---: | --- |
39
+ | Video · selection | Index / slice / batch | ✓ | ✓ |
40
+ | | Playback time / range | ✓ | ✓ |
41
+ | | Order / duplicates preserved | ✓ | ✓ |
42
+ | | Exact / approximate seek | ✓ | ✓ Default: exact |
43
+ | | FPS queries / custom frame mappings | ✓ | ✓ |
44
+ | Video · formats | CFR / VFR / offset PTS / B-frames | ✓ | ✓ Tested |
45
+ | | NCHW / NHWC RGB | ✓ | ✓ |
46
+ | | uint8 / float32 | ✓ | △ SDR |
47
+ | | HDR transfer / display rotation | ✓ | — Explicit rejection |
48
+ | Audio | Ranges / resampling / channel mixing | ✓ | ✓ float32 |
49
+ | Input / output | Paths / URLs / bytes / seekable files | ✓ | ✓ |
50
+ | | Encoded array input | `torch.Tensor` | 1-D uint8 NumPy arrays |
51
+ | | Decoded arrays | `torch.Tensor` | `numpy.ndarray` + array interface / DLPack |
52
+ | Execution | CPU | ✓ | ✓ |
53
+ | | CUDA | ✓ | — |
54
+ | | Python runtime dependency | PyTorch | NumPy |
55
+
56
+ ### Compatibility means
57
+
58
+ - Match the supported CPU API's frame selection, ordering, timing and metadata.
59
+ - Check behavior independently and against pinned TorchCodec 0.17.0.
60
+ - Allow color-conversion rounding: at most 1 uint8 unit or 1/65535 for float32
61
+ in the tested cases. Do not claim identical pixels across every FFmpeg build.
62
+ - Accept empty index lists, including the case affected by the reference's
63
+ empty-list dtype inference bug.
64
+
65
+ Details and the tested scope: [compatibility contract](docs/compatibility.md).
66
+
67
+ ### Current limits
68
+
69
+ - Binary wheels: **Linux x86_64 / ARM64 (aarch64), glibc 2.17+, CPython 3.10+**.
70
+ - A compatible NumPy wheel is also required. On older glibc, the installer may
71
+ select an older NumPy; newer Python versions may require a newer glibc.
72
+ - No macOS, Windows or musl/Alpine wheels yet; free-threaded Python is not a release target.
73
+ - Exact video seeking scans packet timestamps when opening the decoder.
74
+ - Seeking trusts container keyframe flags; incorrect flags can corrupt decoded
75
+ frames. Corrected frame mappings or a repaired input are needed in that case.
76
+ - Audio range queries currently decode from the beginning; late ranges can be
77
+ expensive.
78
+ - NumPy return types require caller changes where code expects Torch tensors.
79
+ - Historical avdec benchmarks are not TensorCodec performance results.
80
+
81
+ ## Install
82
+
83
+ ```sh
84
+ uv pip install tensorcodec
85
+ ```
86
+
87
+ Use an existing virtual environment, or create one with `uv venv` first.
88
+ Linux wheels bundle shared FFmpeg libraries. Source builds need Rust, libclang
89
+ and FFmpeg 7 development headers/libraries.
90
+
91
+ ## Use
92
+
93
+ ```python
94
+ from tensorcodec.decoders import VideoDecoder, AudioDecoder
95
+
96
+ with VideoDecoder("video.mp4") as video:
97
+ frame = video.get_frame_played_at(1.25) # frame playing at this time
98
+ print(frame.data.shape) # CHW NumPy array
99
+ batch = video.get_frames_at([4, 0, 4]) # order and duplicates preserved
100
+ clip = video.get_frames_played_in_range(0, 1, fps=8)
101
+
102
+ with AudioDecoder("audio.wav", sample_rate=16000, num_channels=1) as audio:
103
+ samples = audio.get_samples_played_in_range(0, 1)
104
+ print(samples.data.shape) # channels × samples, float32
105
+ ```
106
+
107
+ Decoded arrays keep their storage after the decoder closes. Input file objects
108
+ remain caller-owned.
109
+
110
+ ## Implementation
111
+
112
+ | Layer | Responsibility |
113
+ | --- | --- |
114
+ | Python | Public API, frame/time selection, validation, result objects |
115
+ | Rust + PyO3 | FFmpeg handles, seeking/decoding, color conversion, resampling |
116
+ | FFmpeg | Codec and container implementations |
117
+
118
+ A batch crosses the Python/Rust boundary once. Native decoding releases the GIL.
119
+
120
+ ## Development and verification
121
+
122
+ ```sh
123
+ # Requires uv, Rust, Clang/libclang, pkg-config and FFmpeg 7 development libraries.
124
+ # Build the editable package and install development + pinned CPU oracle groups.
125
+ uv sync --group dev --group oracle
126
+
127
+ uv run --group oracle pytest tests/test_video_contract.py tests/test_audio_contract.py --backend torchcodec
128
+ uv run --group oracle pytest --compare
129
+
130
+ # Rebuild after changing Rust code.
131
+ uv run --group oracle maturin develop --locked --uv
132
+ ```
133
+
134
+ Tests generate fixtures with FFmpeg/ffprobe and Python's `wave` module.
135
+ `--compare` requires the exact oracle version; otherwise differential tests skip.
136
+ The old avdec decoder and tests are never executed.
137
+
138
+ - [Playback rules](docs/playback_semantics.md)
139
+ - [Release builds and PyPI publishing](docs/releasing.md)
140
+ - [Native dependency licenses and source/build notices](licenses/README.md)
141
+
142
+ TensorCodec's own code is MIT licensed.
@@ -1,7 +1,8 @@
1
1
  # Publishing TensorCodec
2
2
 
3
- Release version: `0.1.0`. Distribution and import name: `tensorcodec`.
4
- The first binary release targets Linux x86_64, glibc 2.28+, CPython 3.10+.
3
+ Release version: `0.1.1`. Distribution and import name: `tensorcodec`.
4
+ Binary wheels target Linux x86_64 and ARM64 (aarch64), glibc 2.17+, CPython 3.10+.
5
+ NumPy must also provide a compatible wheel for the selected Python/glibc pair.
5
6
  The wheel bundles shared FFmpeg 7.1.5 and OpenSSL 3.5.9 LTS; its only Python
6
7
  runtime dependency is NumPy. macOS/Windows wheels are not yet provided.
7
8
 
@@ -26,7 +27,7 @@ settings instead. Do not put an API token in the repository or chat.
26
27
 
27
28
  ## Release
28
29
 
29
- Run the **Publish to PyPI** workflow on `main`. It builds the portable Linux wheel
30
+ Run the **Publish to PyPI** workflow on `main`. It builds the portable Linux wheels
30
31
  and source distribution, checks package metadata, validates the pinned oracle
31
32
  and compares playback before uploading through PyPI Trusted Publishing. It uses
32
33
  the existing GitHub `pypi` environment. Publication fails if authorization is
@@ -38,13 +39,14 @@ gh workflow run publish.yml --repo MilkClouds/tensorcodec --ref main
38
39
 
39
40
  For a build and full validation without uploading, pass `--field publish=false`.
40
41
 
41
- Check the workflow and https://pypi.org/project/tensorcodec/0.1.0/ before reporting
42
- success. Verify a fresh `pip install tensorcodec==0.1.0` and a decode without
43
- Torch/PyAV. Update the version before subsequent releases; PyPI versions cannot
44
- be overwritten.
42
+ Check the workflow and https://pypi.org/project/tensorcodec/0.1.1/ before reporting
43
+ success. Verify a fresh `uv pip install tensorcodec==0.1.1` and a decode without
44
+ Torch/PyAV on both architectures. Update the version before subsequent releases;
45
+ PyPI versions cannot be overwritten.
45
46
 
46
47
  The local Linux build is reproducible using `scripts/build_linux_wheel.sh` inside
47
- `quay.io/pypa/manylinux_2_28_x86_64` with Rust, maturin, libclang, NASM and Perl.
48
+ `quay.io/pypa/manylinux2014_x86_64` or
49
+ `quay.io/pypa/manylinux2014_aarch64` with Rust, maturin, libclang, NASM and Perl.
48
50
  Both native source archives are version- and checksum-pinned. Their licensing
49
51
  and source links are recorded in `licenses/README.md`.
50
52
 
@@ -53,8 +55,12 @@ and source links are recorded in `licenses/README.md`.
53
55
  - Ordinary CI uses prebuilt conda-forge FFmpeg 7.1.1 through Pixi, including its
54
56
  headers and shared libraries. It builds only the TensorCodec extension.
55
57
  - PyPI wheels use the smaller LGPL FFmpeg 7.1.5 build plus OpenSSL 3.5.9.
56
- Their native prefix is cached by the build-script checksums. This preserves the
57
- wheel's codec set, dependency size and licensing rather than bundling the full
58
+ Their native prefix is cached by architecture, glibc baseline and build-script
59
+ checksums. This preserves the wheel's codec set, dependency size and licensing rather than bundling the full
58
60
  conda-forge dependency graph.
61
+ - Release validation installs each repaired wheel on glibc 2.17 with Python 3.10
62
+ and 3.13 and decodes video/audio without Torch, PyAV or a system FFmpeg. Python
63
+ 3.10 also checks the minimum NumPy line (1.26.4). Native
64
+ x86_64 and ARM64 runners also run the full pinned playback oracle comparison.
59
65
  - Release validation still tests the installed repaired wheel. The fixture CLI
60
66
  can be FFmpeg 6 or 7; fixtures explicitly remove auxiliary sentinel packets.
@@ -455,7 +455,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
455
455
 
456
456
  [[package]]
457
457
  name = "tensorcodec-native"
458
- version = "0.1.0"
458
+ version = "0.1.1"
459
459
  dependencies = [
460
460
  "ffmpeg-sys-next",
461
461
  "numpy",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tensorcodec-native"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
 
@@ -26,7 +26,7 @@ fn check(code: i32, operation: &str) -> Result<()> {
26
26
  if code >= 0 {
27
27
  return Ok(());
28
28
  }
29
- let mut text = [0i8; 256];
29
+ let mut text: [std::ffi::c_char; 256] = [0; 256];
30
30
  unsafe {
31
31
  av::av_strerror(code, text.as_mut_ptr(), text.len());
32
32
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tensorcodec"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "NumPy audio/video decoding with TorchCodec-compatible playback semantics"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -46,7 +46,7 @@ include = [
46
46
  testpaths = ["tests"]
47
47
 
48
48
  [tool.ruff]
49
- line-length = 110
49
+ line-length = 119
50
50
  target-version = "py310"
51
51
 
52
52
  [tool.uv.sources]
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
- # Run inside manylinux_2_28 with Rust, maturin, libclang, NASM and Perl installed.
2
+ # Run inside manylinux2014 with Rust, maturin, libclang, NASM and Perl installed.
3
3
  set -euo pipefail
4
+ export BINDGEN_EXTRA_CLANG_ARGS="-I$(gcc -print-file-name=include)${BINDGEN_EXTRA_CLANG_ARGS:+ $BINDGEN_EXTRA_CLANG_ARGS}"
4
5
  build_prefix="${TENSORCODEC_NATIVE_PREFIX:-/opt/tensorcodec}"
5
6
  scripts/build_openssl.sh "$build_prefix/openssl"
6
7
  export PKG_CONFIG_PATH="$build_prefix/openssl/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
@@ -8,4 +9,4 @@ export LD_LIBRARY_PATH="$build_prefix/openssl/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY
8
9
  scripts/build_ffmpeg.sh "$build_prefix/ffmpeg"
9
10
  export FFMPEG_DIR="$build_prefix/ffmpeg"
10
11
  export LD_LIBRARY_PATH="$FFMPEG_DIR/lib:$LD_LIBRARY_PATH"
11
- maturin build --release --locked --auditwheel repair --compatibility manylinux_2_28 --out dist
12
+ maturin build --release --locked --auditwheel repair --compatibility manylinux2014 --out dist
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ # Build-only assembler; manylinux2014's system NASM is too old for FFmpeg 7.
3
+ set -euo pipefail
4
+ prefix="${1:?usage: build_nasm.sh ABSOLUTE_INSTALL_PREFIX}"
5
+ version=2.16.03
6
+ build_root="$(mktemp -d)"
7
+ curl -fsSL "https://www.nasm.us/pub/nasm/releasebuilds/${version}/nasm-${version}.tar.xz" -o "$build_root/nasm.tar.xz"
8
+ echo "1412a1c760bbd05db026b6c0d1657affd6631cd0a63cddb6f73cc6d4aa616148 $build_root/nasm.tar.xz" | sha256sum --check
9
+ tar -xf "$build_root/nasm.tar.xz" -C "$build_root"
10
+ cd "$build_root/nasm-${version}"
11
+ ./configure --prefix="$prefix"
12
+ make -j "${TENSORCODEC_BUILD_JOBS:-4}"
13
+ make install
@@ -0,0 +1,75 @@
1
+ """Generate portable fixtures, then decode them in a clean wheel runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib.util
7
+ import platform
8
+ import subprocess
9
+ import wave
10
+ from pathlib import Path
11
+
12
+
13
+ def generate(root: Path):
14
+ import numpy as np
15
+
16
+ root.mkdir(parents=True, exist_ok=True)
17
+ subprocess.run(
18
+ [
19
+ "ffmpeg",
20
+ "-hide_banner",
21
+ "-loglevel",
22
+ "error",
23
+ "-y",
24
+ "-f",
25
+ "lavfi",
26
+ "-i",
27
+ "testsrc2=size=64x48:rate=10:duration=1",
28
+ "-c:v",
29
+ "libx264",
30
+ "-g",
31
+ "10",
32
+ "-bf",
33
+ "2",
34
+ str(root / "video.mp4"),
35
+ ],
36
+ check=True,
37
+ )
38
+ samples = np.stack([np.arange(8000) % 1000, -(np.arange(8000) % 1000)], axis=1).astype("<i2")
39
+ with wave.open(str(root / "audio.wav"), "wb") as audio:
40
+ audio.setparams((2, 2, 8000, 8000, "NONE", "not compressed"))
41
+ audio.writeframes(samples.tobytes())
42
+
43
+
44
+ def check(root: Path):
45
+ import numpy as np
46
+
47
+ from tensorcodec.decoders import AudioDecoder, VideoDecoder
48
+
49
+ assert all(importlib.util.find_spec(name) is None for name in ("torch", "torchcodec", "av"))
50
+ with VideoDecoder(root / "video.mp4") as decoder:
51
+ assert len(decoder) == 10
52
+ frames = decoder.get_frames_at([7, 0, 7])
53
+ assert frames.data.shape == (3, 3, 48, 64)
54
+ assert frames.data.dtype == np.uint8
55
+ np.testing.assert_allclose(frames.pts_seconds, [0.7, 0.0, 0.7])
56
+ np.testing.assert_array_equal(frames.data[0], frames.data[2])
57
+ played = decoder.get_frame_played_at(0.75)
58
+ np.testing.assert_array_equal(played.data, frames.data[0])
59
+ assert frames.data.max() > frames.data.min()
60
+ with AudioDecoder(root / "audio.wav") as decoder:
61
+ audio = decoder.get_samples_played_in_range(0.125, 0.25)
62
+ assert audio.data.shape == (2, 1000)
63
+ assert audio.data.dtype == np.float32
64
+ expected = np.arange(1000, dtype=np.float32) / 32768
65
+ np.testing.assert_allclose(audio.data[0], expected, atol=1e-7)
66
+ np.testing.assert_allclose(audio.data[1], -expected, atol=1e-7)
67
+ print(f"Wheel decoding passed: Python {platform.python_version()}, {platform.machine()}, {platform.libc_ver()}")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ parser = argparse.ArgumentParser(description=__doc__)
72
+ parser.add_argument("command", choices=("generate", "check"))
73
+ parser.add_argument("fixtures", type=Path)
74
+ args = parser.parse_args()
75
+ {"generate": generate, "check": check}[args.command](args.fixtures)
@@ -3,5 +3,5 @@
3
3
  from tensorcodec import decoders
4
4
  from tensorcodec._frame import AudioSamples, Frame, FrameBatch
5
5
 
6
- __version__ = "0.1.0"
6
+ __version__ = "0.1.1"
7
7
  __all__ = ["AudioSamples", "Frame", "FrameBatch", "decoders"]
@@ -241,9 +241,7 @@ class VideoDecoder(_Decoder):
241
241
  )
242
242
  if self._order == "NCHW":
243
243
  data = data.transpose(0, 3, 1, 2)
244
- return FrameBatch(
245
- data, np.asarray(pts, dtype=np.float64), np.asarray(durations, dtype=np.float64)
246
- )
244
+ return FrameBatch(data, np.asarray(pts, dtype=np.float64), np.asarray(durations, dtype=np.float64))
247
245
 
248
246
  def get_frame_at(self, index):
249
247
  batch = self.get_frames_at([index])
@@ -275,9 +273,7 @@ class VideoDecoder(_Decoder):
275
273
  raise RuntimeError("timestamp is outside the stream")
276
274
  if self._mappings is not None:
277
275
  return np.searchsorted(self._pts, values, side="right") - 1
278
- return np.floor((values - self.metadata.begin_stream_seconds) * self.metadata.average_fps).astype(
279
- np.int64
280
- )
276
+ return np.floor((values - self.metadata.begin_stream_seconds) * self.metadata.average_fps).astype(np.int64)
281
277
 
282
278
  def get_frames_played_at(self, seconds):
283
279
  with self._lock:
@@ -312,12 +308,8 @@ class VideoDecoder(_Decoder):
312
308
  start = int(np.searchsorted(self._pts, start_seconds, side="right") - 1)
313
309
  stop = int(np.searchsorted(self._pts, stop_seconds, side="left"))
314
310
  else:
315
- start = math.floor(
316
- (start_seconds - self.metadata.begin_stream_seconds) * self.metadata.average_fps
317
- )
318
- stop = math.ceil(
319
- (stop_seconds - self.metadata.begin_stream_seconds) * self.metadata.average_fps
320
- )
311
+ start = math.floor((start_seconds - self.metadata.begin_stream_seconds) * self.metadata.average_fps)
312
+ stop = math.ceil((stop_seconds - self.metadata.begin_stream_seconds) * self.metadata.average_fps)
321
313
  return self.get_frames_in_range(start, stop)
322
314
 
323
315
  def get_all_frames(self, fps=None):
@@ -365,8 +357,7 @@ class AudioDecoder(_Decoder):
365
357
  with self._lock:
366
358
  self._check_open()
367
359
  if not math.isfinite(start_seconds) or (
368
- stop_seconds is not None
369
- and (not math.isfinite(stop_seconds) or not start_seconds <= stop_seconds)
360
+ stop_seconds is not None and (not math.isfinite(stop_seconds) or not start_seconds <= stop_seconds)
370
361
  ):
371
362
  raise ValueError("invalid audio time range")
372
363
  data, first_pts = self._native.decode_audio(self._rate, self._channels, stop_seconds)
@@ -9,9 +9,7 @@ from tests.conftest import as_numpy, index_input, time_input
9
9
  def compare_batch(actual, expected):
10
10
  assert actual.data.shape == expected.data.shape
11
11
  np.testing.assert_allclose(actual.pts_seconds, as_numpy(expected.pts_seconds), atol=1e-12, rtol=0)
12
- np.testing.assert_allclose(
13
- actual.duration_seconds, as_numpy(expected.duration_seconds), atol=1e-12, rtol=0
14
- )
12
+ np.testing.assert_allclose(actual.duration_seconds, as_numpy(expected.duration_seconds), atol=1e-12, rtol=0)
15
13
  # Identical FFmpeg conversion settings should match; allow one rounding unit, not 20.
16
14
  np.testing.assert_allclose(actual.data, as_numpy(expected.data), atol=1, rtol=0)
17
15
 
@@ -40,9 +38,7 @@ def test_video_differential(backend, oracle, video):
40
38
  for indices in [[11, 0, 5, 5, 2], [], list(range(12))]:
41
39
  compare_batch(actual.get_frames_at(indices), expected.get_frames_at(index_input(oracle, indices)))
42
40
  times = ((video.pts[:-1] + video.pts[1:]) / 2).tolist()[::-1]
43
- compare_batch(
44
- actual.get_frames_played_at(times), expected.get_frames_played_at(time_input(oracle, times))
45
- )
41
+ compare_batch(actual.get_frames_played_at(times), expected.get_frames_played_at(time_input(oracle, times)))
46
42
  compare_batch(
47
43
  actual.get_frames_played_in_range(float(video.pts[1]), float(video.pts[8])),
48
44
  expected.get_frames_played_in_range(float(video.pts[1]), float(video.pts[8])),
@@ -73,9 +69,7 @@ def test_color_conversion_differential(backend, oracle, color_video, dtype):
73
69
  # Exact pixel conversion when both backends link the same FFmpeg version.
74
70
  for indices in ([11, 3, 3, 0], [4, 1, 9], [0, 11]):
75
71
  left, right = actual.get_frames_at(indices), expected.get_frames_at(indices)
76
- np.testing.assert_allclose(
77
- left.data, as_numpy(right.data), atol=1 if dtype == "uint8" else 1 / 65535, rtol=0
78
- )
72
+ np.testing.assert_allclose(left.data, as_numpy(right.data), atol=1 if dtype == "uint8" else 1 / 65535, rtol=0)
79
73
  np.testing.assert_allclose(left.pts_seconds, as_numpy(right.pts_seconds), atol=1e-12, rtol=0)
80
74
  for field in ("color_space", "color_primaries", "color_transfer_characteristic", "pixel_format"):
81
75
  assert getattr(actual.metadata, field) == getattr(expected.metadata, field)
@@ -115,9 +115,7 @@ def test_encoded_sources(backend, videos, kind):
115
115
  assert_batch(backend.VideoDecoder(source).get_frames_at([11, 0, 4]), case, [11, 0, 4])
116
116
 
117
117
 
118
- @pytest.mark.parametrize(
119
- "kwargs", [{"dimension_order": "CHWN"}, {"seek_mode": "wrong"}, {"num_ffmpeg_threads": None}]
120
- )
118
+ @pytest.mark.parametrize("kwargs", [{"dimension_order": "CHWN"}, {"seek_mode": "wrong"}, {"num_ffmpeg_threads": None}])
121
119
  def test_invalid_constructor_arguments(backend, videos, kwargs):
122
120
  with pytest.raises(ValueError):
123
121
  backend.VideoDecoder(videos["cfr"].path, **kwargs)
@@ -1,124 +0,0 @@
1
- # TensorCodec
2
-
3
- **TorchCodec-style video and audio decoding, without PyTorch.**
4
-
5
- - Use the CPU decoder API and playback rules of **TorchCodec 0.17.0**.
6
- - Get **NumPy arrays** instead of `torch.Tensor`.
7
- - Install **NumPy + TensorCodec**. No Torch, PyAV, or FFmpeg CLI at runtime.
8
-
9
- The goal is predictable frame selection, timestamps and audio ranges with a small
10
- runtime dependency set. This is a **CPU decoding subset**, not the entire
11
- TorchCodec package. It does not promise a speedup over PyAV or TorchCodec.
12
-
13
- ## What is available?
14
-
15
- | Capability | TorchCodec 0.17.0 | TensorCodec 0.1.0 |
16
- | --- | --- | --- |
17
- | Python runtime dependency | PyTorch | NumPy |
18
- | Output arrays | `torch.Tensor` | `numpy.ndarray`; array interface + DLPack |
19
- | Video index/slice/batch access | Supported | Supported |
20
- | Playback time/range access | Supported | Supported |
21
- | CFR, VFR, offset PTS, B-frames | Supported | Tested |
22
- | Request ordering and duplicates | Preserved | Preserved |
23
- | Exact / approximate seeking | Supported | Supported; exact is the default |
24
- | NCHW / NHWC RGB | Supported | Supported |
25
- | uint8 / float32 video | Supported | Supported for SDR |
26
- | FPS sampling, custom frame mappings | Supported | Supported |
27
- | Audio ranges, resampling, channel mixing | Supported | Supported; float32 output |
28
- | Paths, URLs, bytes, seekable file objects | Supported | Supported |
29
- | Encoded tensor input | `torch.Tensor` | 1-D uint8 NumPy arrays |
30
- | CUDA decoding | Supported | **Not implemented** |
31
- | Decoder transforms | Supported | **Not implemented** |
32
- | HDR inputs / display rotation | Supported | **Rejected explicitly** |
33
- | Other modules, including samplers/encoders | Available | **Outside the initial scope** |
34
-
35
- ### Compatibility means
36
-
37
- - Match the supported CPU API's frame selection, ordering, timing and metadata.
38
- - Check behavior independently **and** against pinned TorchCodec 0.17.0.
39
- - Allow color-conversion rounding: at most 1 uint8 unit or 1/65535 for float32
40
- in the tested cases. Do not claim identical pixels across every FFmpeg build.
41
- - Accept empty index lists, including the case affected by the reference's
42
- empty-list dtype inference bug.
43
-
44
- Details and the tested scope: [compatibility contract](docs/compatibility.md).
45
-
46
- ### Current limits
47
-
48
- - Binary wheels: **Linux x86_64, glibc 2.28+, CPython 3.10+**.
49
- - No macOS or Windows wheels yet; free-threaded Python is not a release target.
50
- - Exact video seeking scans packet timestamps when opening the decoder.
51
- - Audio range queries currently decode from the beginning; late ranges can be
52
- expensive.
53
- - NumPy return types require caller changes where code expects Torch tensors.
54
- - Historical avdec benchmarks are not TensorCodec performance results.
55
-
56
- ## Install
57
-
58
- ```sh
59
- python -m pip install tensorcodec
60
- ```
61
-
62
- Linux wheels bundle shared FFmpeg libraries. Source builds need Rust, libclang
63
- and FFmpeg 7 development headers/libraries.
64
-
65
- Before the first PyPI upload, install a local wheel:
66
-
67
- ```sh
68
- python -m pip install dist/tensorcodec-*.whl
69
- ```
70
-
71
- ## Use
72
-
73
- ```python
74
- from tensorcodec.decoders import VideoDecoder, AudioDecoder
75
-
76
- with VideoDecoder("video.mp4") as video:
77
- frame = video.get_frame_played_at(1.25) # frame playing at this time
78
- print(frame.data.shape) # CHW NumPy array
79
- batch = video.get_frames_at([4, 0, 4]) # order and duplicates preserved
80
- clip = video.get_frames_played_in_range(0, 1, fps=8)
81
-
82
- with AudioDecoder("audio.wav", sample_rate=16000, num_channels=1) as audio:
83
- samples = audio.get_samples_played_in_range(0, 1)
84
- print(samples.data.shape) # channels × samples, float32
85
- ```
86
-
87
- Decoded arrays keep their storage after the decoder closes. Input file objects
88
- remain caller-owned.
89
-
90
- ## Implementation
91
-
92
- | Layer | Responsibility |
93
- | --- | --- |
94
- | Python | Public API, frame/time selection, validation, result objects |
95
- | Rust + PyO3 | FFmpeg handles, seeking/decoding, color conversion, resampling |
96
- | FFmpeg | Codec and container implementations |
97
-
98
- A batch crosses the Python/Rust boundary once. Native decoding releases the GIL.
99
-
100
- ## Development and verification
101
-
102
- ```sh
103
- # Requires Rust, Clang/libclang, pkg-config and FFmpeg 7 development libraries.
104
- python -m venv .venv
105
- . .venv/bin/activate
106
- python -m pip install numpy pytest ruff 'maturin>=1.8,<2'
107
- maturin develop --locked
108
-
109
- # Reference dependencies are for tests only.
110
- python -m pip install torch==2.14.1 torchcodec==0.17.0 \
111
- --index-url https://download.pytorch.org/whl/cpu
112
- pytest tests/test_video_contract.py tests/test_audio_contract.py --backend torchcodec
113
- pytest --compare
114
- ```
115
-
116
- Tests generate fixtures with FFmpeg/ffprobe and Python's `wave` module.
117
- `--compare` requires the exact oracle version; otherwise differential tests skip.
118
- The old avdec decoder and tests are never executed.
119
-
120
- - [Playback rules](docs/playback_semantics.md)
121
- - [Release builds and PyPI publishing](docs/releasing.md)
122
- - [Native dependency licenses and source/build notices](licenses/README.md)
123
-
124
- TensorCodec's own code is MIT licensed.
File without changes