Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@
"msgspec",
"MSVC",
"musllinux",
"muxed",
"muxing",
"mycli",
"myconfig",
"myprog",
Expand Down
13 changes: 8 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
All notable changes to this project will be documented in this file.

- [Changelog](#changelog)
- [V.V.V - 2026-06-DD - Placeholder](#vvv---2026-06-dd---placeholder)
- [2.1.0 - 2026-06-20](#210---2026-06-20)
- [2.0.0 - 2026-06-19](#200---2026-06-19)
- [1.9.0 - 2026-06-11](#190---2026-06-11)
- [1.8.0 - 2026-06-06](#180---2026-06-06)
Expand All @@ -30,16 +30,19 @@ This project follows a pragmatic versioning approach:
- **Minor**: new features or non-breaking changes.
- **Major**: breaking changes (command renames, incompatible output formats).

## V.V.V - 2026-06-DD - Placeholder
## 2.1.0 - 2026-06-20

- Added
- Placeholder for future additions.
- **Animation metadata hash injection control** (`--inject/--no-inject` flag, `cli/base.py`, `cli/zoomcommand.py`): new `tranz zoom auto` flag allows users to control whether the final hash is re-injected into animation metadata after rendering; `--inject` (default False) re-saves the animation file to include the final computed hash in metadata, which requires re-processing (lossless for MP4, lossy for GIF); `--no-inject` skips this step for faster completion when the final hash in metadata is not critical; useful for testing or when metadata space is constrained; both GIF and MP4 re-saving is expensive but preserves content fidelity for MP4 via ffmpeg re-mux.

- Changed
- Placeholder for future changes.
- **MP4 metadata re-muxing optimization** (`core/zoom.py`): `ReWriteVideoMP4Meta()` completely rewritten to use external ffmpeg re-muxing instead of re-encoding; new implementation calls `imageio_ffmpeg.get_ffmpeg_exe()` with `copy` codec and `map_metadata` flags for lossless stream copying; dramatically reduces MP4 re-processing time from full re-encode to simple container re-mux operation; preserves all video frames and streams without quality loss; uses `subprocess.run()` to invoke ffmpeg directly with proper error handling and metadata injection support.
- **Test animation outputs** (`scripts/make_test_images.sh`, `tests_integration/test_cython_equivalence.py`, `tests_integration/test_installed_cli.py`): updated to account for MP4 re-muxing changes.
- **Example outputs in README**: updated example command outputs to show new "Copy file to destination" message when animations are saved; examples now display Lanczos resampling method in output format `{[PNG*2/Lanczos: ...]}` to indicate interpolation method being used.

- Fixed
- Placeholder for future fixes.
- **MP4 video stability**: MP4 files created with metadata injection are now stable and byte-for-byte identical across multiple re-mux operations (ffmpeg re-mux with same metadata always produces identical output), whereas previous re-encoding approach introduced minor frame variations due to codec variability.
- **Animation rendering consistency**: animations now properly track hash values through metadata injection workflow, ensuring that multiple renders with `--inject` flag produce consistent, reproducible metadata.

## 2.0.0 - 2026-06-19

Expand Down
27 changes: 13 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ Command-level options:
| `--i-frames` | Number of interpolated frames to add between each pair of real frames (0–7); increases effective FPS via temporal interpolation; see [Frame interpolation](#frame-interpolation---i-frames) | `0` |
| `--loop` | Number of GIF loops; `0` = infinite (ignored for MP4) | `0` |
| `--save-frames/--no-save-frames` | Save each intermediate PNG frame to disk | off |
| `--inject/--no-inject` | Whether to re-save the animation after rendering to inject the final hash into metadata; MP4 uses lossless ffmpeg re-mux (fast), GIF requires re-encoding (slow); enable only if final hash in metadata is critical (e.g., for testing or verification); most users should leave off | `--no-inject` |

Mark options (`--mark`, `--mark-color`, `--mark-width`) are **`tranz zoom` subgroup flags** (see [above](#tranz-zoom-subgroup-flags)) and apply to all zoom commands, including `auto`.

Expand Down Expand Up @@ -884,7 +885,7 @@ This section describes the internal pipeline that `tranz zoom auto` follows, ste

8. **Render and animation assembly**: All frames are rendered (applying the color normalization from Step 7) and fed to `imageio` for assembly into the final GIF or MP4. In streaming mode, frames are loaded from disk one at a time so peak RAM equals roughly one frame rather than the whole animation.

9. **Metadata embedding**: The final file receives `tranZoom:zoom:*` metadata tags: frame count, FPS, duration, zoom step, magnification per step, marker frame indices, depth frame data (pre- and post-smoothing depths), and the zoom hash. This metadata can be inspected with `tranz image read`.
9. **Metadata embedding**: The final file receives `tranZoom:zoom:*` metadata tags: frame count, FPS, duration, zoom step, magnification per step, marker frame indices, depth frame data (pre- and post-smoothing depths), and the zoom hash. This metadata can be inspected with `tranz image read`. By default (`--no-inject`), the final hash is computed but not written back to the file to save time. If `--inject` is specified, the animation is re-processed to embed the final hash in the metadata: MP4 files are re-muxed losslessly using ffmpeg (preserving all frame data while updating the metadata container), while GIF files must be re-encoded (slower, slight quality loss). Most users should leave off (`--no-inject`) unless final hash verification is critical.

10. **DB persistence**: A `ZoomData` entry is written to the DB referencing all individual frames, the marker subset, and the depth subset. Future runs with the same parameters will find the cached video file and return it immediately without repeating any computation.

Expand Down Expand Up @@ -995,12 +996,9 @@ You can easily make animations!
```sh
$ poetry run tranz --no-db --no-date zoom -s 220 --mark "(-5578776469/7500000000,8244620127/62500000000)" auto " -5578776469/7500000000" "8244620127/62500000000" "0.00073801" "0.00073801" "1" --fps 5 --duration 4 --i-frames 1

220 × 220 'GIF': 'sahara' 'Mandelbrot' 10^1.0000 magnitude ZOOM, 4.000 s long, at 5.00*2 FPS,
with 20|39 frames (2 markers, 10.00%, and 4 depth frames, 20.00%), 12.8838%/step, CYTHON OPTIMIZED...
ZOOM: <{[MANDELBROT: (-5578776469/7500000000, 8244620127/62500000000) ± 73801/100000000] :
[220 × 220, AUTO]} -> <GIF*2: {[PNG*1: SAHARA, none] +
[MARK: red/1 @ (-5578776469/7500000000, 8244620127/62500000000)]}> /
(mag:1, n:20|39, d:4, fps:(5)*2, l:0)> ... [MANDELBROT: (-5578776469/7500000000, 8244620127/62500000000) ± 73801/1000000000]
220 × 220 'GIF': 'sahara' 'Mandelbrot' 10^1.0000 magnitude ZOOM, 4.000 s long, at 5.00*2 FPS, with 20|39 frames (2 markers, 10.00%, and 4 depth frames, 20.00%), 12.8838%/step, CYTHON OPTIMIZED...
ZOOM: <{[MANDELBROT: (-5578776469/7500000000, 8244620127/62500000000) ± 73801/100000000] : [220 × 220, AUTO]} ->
<GIF*2: {[PNG*1: SAHARA, none] + [MARK: red/1 @ (-5578776469/7500000000, 8244620127/62500000000)]}> / (mag:1, n:20|39, d:4, fps:(5)*2, l:0)> ... [MANDELBROT: (-5578776469/7500000000, 8244620127/62500000000) ± 73801/1000000000]

Making 4 depth computations...
Depth 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4/4 [ 0:00:01 < 0:00:00 , 3 fr/s ]
Expand All @@ -1023,6 +1021,7 @@ ZOOM: Color norm: built from 2 marker frames

Render: <GIF*2: {[PNG*1: SAHARA, none] + [MARK: red/1 @ (-5578776469/7500000000, 8244620127/62500000000)]}>
Render 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 20/20 [ 0:00:03 < 0:00:00 , 5 fr/s ]
Move file to destination
Render: DONE

Success: GIF 'd9204b9c2aec64555ca7ce48226301684737cce8b673febe86629c2e8a36ae19' in 1.498 s (depth) + 14.956 s (frames) + 4.353 s (render)
Expand Down Expand Up @@ -1063,18 +1062,18 @@ $ poetry run tranz --no-db --palette electric --set max --set-palette sunset --n

512 × 377 Julia w/ SET 'max', 10^2.630 magnitude, CYTHON OPTIMIZED...
Compute: {[JULIA: (-313420497/429687500, 6567/10000) ± (17/3125, 1/250) @ (13667/50000, 371/50000)] : [512 × 377, AUTO] : max}
Pre 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 576/576 [ 0:00:00 < 0:00:00 , 799 px/s ]
Pre 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 576/576 [ 0:00:00 < 0:00:00 , 777 px/s ]
Picked depth 1000, histogram {43: 3, 44: 31, 45: 30, ...: 245, 425: 1, 431: 1, 1175: 1}, 264/576 set points
Img 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 193,024/193,024 [ 0:00:02 < 0:00:00 , 64,311 px/s ]
Compute: Julia: DONE, with precision 140 bits, 33.769 MiB, in 4.501 s
Img 100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 193,024/193,024 [ 0:00:02 < 0:00:00 , 67,370 px/s ]
Compute: Julia: DONE, with precision 140 bits, 33.769 MiB, in 4.542 s

Render: {[PNG*2: ELECTRIC, SUNSET]}
Render: {[PNG*2/Lanczos: ELECTRIC, SUNSET]}
Interpolating rendered frame 512 × 377 -> 1024 × 754 (*2)
Render: PNG: DONE (1024 × 754), '95a6acd116fb1ca043f089f093fb0a8c139ffb490a6a24be068fa474c8636871' in 435.756 ms
Saved to 'julia-95a6acd116fb1ca043f0.png' ('2f682ec344a556b1'), 554.419 KiB
Render: PNG: DONE (1024 × 754), 'c748e691dbbfbec2c7008cb902f608e99f11950be2f469f0231a276bc8dbf3a2' in 407.142 ms
Saved to 'julia-c748e691dbbfbec2c700.png' ('ae20f9d115f7940f'), 637.245 KiB
```

Notice the `512 × 377 -> 1024 × 754 (*2)` showing the resolution increase. If the hash of this image changes, remember to change it in `src/tranzoom/cli/base.py`.
Notice the `512 × 377 -> 1024 × 754 (*2)` showing the resolution increase and the `{[PNG*2/Lanczos: ELECTRIC, SUNSET]}` showing the method "Lanczos". If the hash of this image changes, remember to change it in `src/tranzoom/cli/base.py`.

#### Powers of 1000

Expand Down
4 changes: 2 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ build-backend = "poetry.core.masonry.api"
[project]

name = "tranzoom"
version = "2.0.0" # also update src/tranzoom/__init__.py
version = "2.1.0" # also update src/tranzoom/__init__.py

description = "Mandelbrot/Julia Fractal generation and manipulation with AI/LLMs"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ idna==3.18 ; python_version >= "3.12" and python_version < "4.0"
imageio-ffmpeg==0.6.0 ; python_version >= "3.12" and python_version < "4.0"
imageio==2.37.3 ; python_version >= "3.12" and python_version < "4.0"
jinja2==3.1.6 ; python_version >= "3.12" and python_version < "4.0"
llama-cpp-python==0.3.30 ; python_version >= "3.12" and python_version < "4.0"
llama-cpp-python==0.3.31 ; python_version >= "3.12" and python_version < "4.0"
lmstudio==1.5.0 ; python_version >= "3.12" and python_version < "4.0"
markdown-it-py==4.2.0 ; python_version >= "3.12" and python_version < "4.0"
markupsafe==3.0.3 ; python_version >= "3.12" and python_version < "4.0"
Expand Down
12 changes: 6 additions & 6 deletions scripts/make_test_images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ set -euxo pipefail
# Render Seahorse Tail original
poetry run tranz --no-db --force --set imaginary --no-date --no-hash --prefix "demo-mandel-seahorse-tail" -o tests/data/images image mandel " -0.7436499" "0.13188204" "0.00073801"
# Render Animated Seahorse Tail zoom
poetry run tranz --no-db --force --no-date --no-hash --prefix "demo-mandel-seahorse-tail-anim" -o tests/data/images zoom -s 220 --mark "(-5578776469/7500000000,8244620127/62500000000)" auto " -5578776469/7500000000" "8244620127/62500000000" "0.00073801" "0.00073801" "1" --fps 5 --duration 4 --i-frames 1
poetry run tranz --no-db --force --no-date --no-hash --prefix "demo-mandel-seahorse-tail-anim" -o tests/data/images zoom -s 220 --mark "(-5578776469/7500000000,8244620127/62500000000)" auto " -5578776469/7500000000" "8244620127/62500000000" "0.00073801" "0.00073801" "1" --fps 5 --duration 4 --i-frames 1 --inject
# Render Julia Suzana Wave
poetry run tranz --no-db --force --set max --no-date --no-hash --prefix "demo-julia-suzana-wave" -o tests/data/images --palette "electric" --set-palette sunset image -s 512 --i-pixels 1 julia "13667/50000" "371/50000" " -313420497/429687500" "0.6567" "0.00544" "0.004"

Expand All @@ -49,14 +49,14 @@ H300="127/6100000000000000000000000000000000000000000000000000000000000000000000

# these are meant to stress the mandelbrot/julia math python/cython implementations
# 1: superficial Mandelbrot zoom, with set[IMAGINARY]
poetry run tranz --no-db --force --palette "lava" --set imaginary --set-palette "toxic" --no-date --no-hash --prefix "test-mandel-z-auto-seahorse" -o tests/data/images zoom -s 53 --i-pixels 2 --resample bilinear auto " -0.7436499" "0.13188204" "227/193" "167/193" "131/43" --fps 1 --duration "31.7" --i-frames 1
poetry run tranz --no-db --force --palette "lava" --set imaginary --set-palette "toxic" --no-date --no-hash --prefix "test-mandel-z-auto-seahorse" -o tests/data/images zoom -s 53 --i-pixels 2 --resample bilinear auto " -0.7436499" "0.13188204" "227/193" "167/193" "131/43" --fps 1 --duration "31.7" --i-frames 1 --inject
# 2: ultra-deep Mandelbrot zoom, no set
poetry run tranz --no-db --force --palette "electric" --no-date --no-hash --prefix "test-mandel-z-auto-seeds300" -o tests/data/images zoom -s 31 --i-pixels 3 auto "$CX300" "$CY300" "$W300" "$H300" "43/41" --fps 1 --duration "10.1" --i-frames 1
poetry run tranz --no-db --force --palette "electric" --no-date --no-hash --prefix "test-mandel-z-auto-seeds300" -o tests/data/images zoom -s 31 --i-pixels 3 auto "$CX300" "$CY300" "$W300" "$H300" "43/41" --fps 1 --duration "10.1" --i-frames 1 --inject
# 3: ultra-deep Mandelbrot with mini-brot and set coloring
# TODO: still missing a mini-brot ultra zoom so we can stress the set math
# 4: julia zoom, Suzana point, with set[ANGLE]
poetry run tranz --no-db --force --palette "sahara" --set angle --set-palette "iris" --no-date --no-hash --prefix "test-julia-z-auto-suzana" -o tests/data/images zoom -s 59 -f julia --julia-re "13667/50000" --julia-im "371/50000" --i-pixels 1 auto " -313420497/429687500" "0.6567" "167/193" "227/193" "241/139" --fps 2 --duration "10.7" --i-frames 2
poetry run tranz --no-db --force --palette "sahara" --set angle --set-palette "iris" --no-date --no-hash --prefix "test-julia-z-auto-suzana" -o tests/data/images zoom -s 59 -f julia --julia-re "13667/50000" --julia-im "371/50000" --i-pixels 1 auto " -313420497/429687500" "0.6567" "167/193" "227/193" "241/139" --fps 2 --duration "10.7" --i-frames 2 --inject
# 5: julia zoom, different "Dragon" point, with set[MIN]
poetry run tranz --no-db --force --palette "lava" --set min --set-palette "electric" --no-date --no-hash --prefix "test-julia-z-auto-dragon" -o tests/data/images zoom -s 67 -f julia --julia-re " -0.11" --julia-im "0.6557" --i-pixels 1 auto "0" "0" "223/73" "281/71" "37/97" --fps 2 --duration "4.1" --i-frames 3
poetry run tranz --no-db --force --palette "lava" --set min --set-palette "electric" --no-date --no-hash --prefix "test-julia-z-auto-dragon" -o tests/data/images zoom -s 67 -f julia --julia-re " -0.11" --julia-im "0.6557" --i-pixels 1 auto "0" "0" "223/73" "281/71" "37/97" --fps 2 --duration "4.1" --i-frames 3 --inject
# 6: julia zoom, different "Blob" point, with set[MAX]
poetry run tranz --no-db --force --palette "sahara" --set max --set-palette "electric" --no-date --no-hash --prefix "test-julia-z-auto-blob" -o tests/data/images zoom -s 71 -f julia --julia-re " -0.481762" --julia-im " -0.531657" --i-pixels 1 auto "0" "0" "281/71" "223/73" "37/97" --fps 2 --duration "4.3" --i-frames 3
poetry run tranz --no-db --force --palette "sahara" --set max --set-palette "electric" --no-date --no-hash --prefix "test-julia-z-auto-blob" -o tests/data/images zoom -s 71 -f julia --julia-re " -0.481762" --julia-im " -0.531657" --i-pixels 1 auto "0" "0" "281/71" "223/73" "37/97" --fps 2 --duration "4.3" --i-frames 3 --inject
2 changes: 1 addition & 1 deletion src/tranzoom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""TranZoom: Mandelbrot/Julia Fractal generation and manipulation with AI/LLMs."""

__all__: list[str] = ['__author__', '__version__']
__version__ = '2.0.0' # also update pyproject.toml
__version__ = '2.1.0' # also update pyproject.toml
__author__ = 'Daniel Balparda <balparda@github.com>, Bella Keri <BellaKeri@github.com>'

__app__ = 'tranZoom'
Loading