thumby-cli 0.1.0__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.
@@ -0,0 +1,34 @@
1
+ name: Publish Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ build-and-publish:
14
+ runs-on: ubuntu-latest
15
+ environment:
16
+ name: pypi
17
+ url: https://pypi.org/p/thumby-cli
18
+ steps:
19
+ - name: Check out repository
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Set up Python
23
+ uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.12"
26
+
27
+ - name: Install uv
28
+ uses: astral-sh/setup-uv@v5
29
+
30
+ - name: Build distributions
31
+ run: uv build
32
+
33
+ - name: Publish to PyPI
34
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,39 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+
8
+ # Packaging / build artifacts
9
+ build/
10
+ dist/
11
+ *.egg-info/
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Tooling caches
19
+ .ruff_cache/
20
+ .mypy_cache/
21
+ .pytest_cache/
22
+
23
+ # Editors / OS
24
+ .vscode/
25
+ .idea/
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # Local media files (avoid accidental commits)
30
+ *.ts
31
+ *.mp4
32
+ *.mkv
33
+ *.mov
34
+ *.avi
35
+ *.flv
36
+ *.webm
37
+
38
+ # Temporary artifacts produced by app
39
+ filelist.txt
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: thumby-cli
3
+ Version: 0.1.0
4
+ Summary: High-performance CLI tool to generate video thumbnail sheets with technical metadata headers.
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: av>=9.0.0
8
+ Requires-Dist: pillow>=9.2.0
9
+ Requires-Dist: pymediainfo>=4.3
10
+ Requires-Dist: rich>=13.0.0
11
+ Requires-Dist: typer>=0.15.0
@@ -0,0 +1,71 @@
1
+ ![Thumby logo](logo.png)
2
+ # thumby
3
+
4
+ **thumby** is a high-performance media utility designed to create beautiful video thumbnail sheets from a provided video file. It extracts frames at regular intervals and arranges them into a grid with a technical header.
5
+
6
+ ![Python >=3.10](https://img.shields.io/badge/python-3.10%2B-blue)
7
+ ![FFmpeg required](https://img.shields.io/badge/ffmpeg-required-orange)
8
+
9
+ ---
10
+
11
+ ## ✨ Features
12
+
13
+ - 🖼️ **Professional Layout**: Creates a grid of thumbnails with a full technical metadata header (size, duration, resolution, codecs).
14
+ - ⚡ **High Performance**: Uses `PyAV` (FFmpeg bindings) for fast frame extraction and `Pillow` for image processing.
15
+ - 📊 **Progress Bar**: Real-time progress tracking while extracting frames.
16
+ - 🛠️ **Customizable**: Control rows, columns, tile width, skip time, and JPEG quality.
17
+
18
+ ---
19
+
20
+ ## 🚀 Quick Start
21
+
22
+ ### 1. Installation
23
+
24
+ Install **thumby** as a global tool using [uv](https://github.com/astral-sh/uv):
25
+
26
+ ```bash
27
+ uv tool install thumby-cli
28
+ ```
29
+
30
+ ### 2. Usage
31
+
32
+ Generate a thumbnail sheet for a video:
33
+
34
+ ```bash
35
+ thumby "my_video.mp4"
36
+ ```
37
+
38
+ This will create `my_video_preview.jpg` in the same directory.
39
+
40
+ ### 3. Options
41
+
42
+ ```bash
43
+ # Custom grid and tile width
44
+ thumby "video.mp4" --rows 5 --cols 5 --width 300
45
+
46
+ # Specify output path
47
+ thumby "video.mp4" --output "preview.jpg"
48
+
49
+ # Skip the first 30 seconds
50
+ thumby "video.mp4" --skip 30
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 🛠️ Configuration
56
+
57
+ | Option | Shorthand | Default | Description |
58
+ |--------|-----------|---------|-------------|
59
+ | `--output` | `-o` | `auto` | Output path for the JPEG image. |
60
+ | `--rows` | `-r` | `9` | Number of rows in the grid. |
61
+ | `--cols` | `-c` | `3` | Number of columns in the grid. |
62
+ | `--width` | `-w` | `400` | Width of each tile in pixels. |
63
+ | `--skip` | `-s` | `10.0` | Seconds to skip from the start. |
64
+ | `--quality` | `-q` | `95` | JPEG quality (1-100). |
65
+
66
+ ---
67
+
68
+ ## 📝 Requirements
69
+
70
+ - **Python 3.10+**
71
+ - **FFmpeg**: Must be installed and available in your system `PATH`.
Binary file
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "thumby-cli"
3
+ version = "0.1.0"
4
+ description = "High-performance CLI tool to generate video thumbnail sheets with technical metadata headers."
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "typer>=0.15.0",
8
+ "av>=9.0.0",
9
+ "pymediainfo>=4.3",
10
+ "Pillow>=9.2.0",
11
+ "rich>=13.0.0",
12
+ ]
13
+
14
+ [project.scripts]
15
+ thumby = "thumby.cli:run"
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/thumby"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,137 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.progress import BarColumn, Progress, TextColumn
8
+ from rich.text import Text
9
+
10
+ from . import __version__
11
+ from .thumbnailer import Thumbnailer, ThumbnailerParams
12
+
13
+ ASCII_LOGO = r"""
14
+
15
+ ████████╗ ██╗ ██╗ ██╗ ██╗ ███╗ ███╗ ██████╗ ██╗ ██╗
16
+ ╚══██╔══╝ ██║ ██║ ██║ ██║ ████╗ ████║ ██╔══██╗ ╚██╗ ██╔╝
17
+ ██║ ███████║ ██║ ██║ ██╔████╔██║ ██████╔╝ ╚████╔╝
18
+ ██║ ██╔══██║ ██║ ██║ ██║╚██╔╝██║ ██╔══██╗ ╚██╔╝
19
+ ██║ ██║ ██║ ╚██████╔╝ ██║ ╚═╝ ██║ ██████╔╝ ██║
20
+ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝
21
+
22
+
23
+ """
24
+
25
+
26
+ def print_logo():
27
+ console = Console()
28
+ lines = ASCII_LOGO.strip("\n").split("\n")
29
+ # Mint palette colors from oh-my-logo: #00d2ff -> #3a7bd5
30
+ start_color = (0, 210, 255)
31
+ end_color = (58, 123, 213)
32
+
33
+ # empty line padding on top
34
+ console.print(Text("\n"))
35
+
36
+ for i, line in enumerate(lines):
37
+ t = i / (len(lines) - 1) if len(lines) > 1 else 0
38
+ r = int(start_color[0] + (end_color[0] - start_color[0]) * t)
39
+ g = int(start_color[1] + (end_color[1] - start_color[1]) * t)
40
+ b = int(start_color[2] + (end_color[2] - start_color[2]) * t)
41
+
42
+ color = f"rgb({r},{g},{b})"
43
+ console.print(Text(line, style=color))
44
+
45
+
46
+ app = typer.Typer(
47
+ help="Create video thumbnail sheets from a provided video file.",
48
+ pretty_exceptions_show_locals=False,
49
+ rich_markup_mode="rich",
50
+ )
51
+
52
+
53
+ def version_callback(value: bool) -> None:
54
+ if not value:
55
+ return
56
+ print_logo()
57
+ typer.echo(f"thumby {__version__}")
58
+ raise typer.Exit()
59
+
60
+
61
+ @app.command()
62
+ def main(
63
+ video_path: Path = typer.Argument(
64
+ ..., help="Path to the video file.", exists=True, file_okay=True, dir_okay=False
65
+ ),
66
+ output: Optional[Path] = typer.Option(
67
+ None, "--output", "-o", help="Output path for the JPEG image."
68
+ ),
69
+ rows: int = typer.Option(9, "--rows", "-r", help="Number of rows in the grid."),
70
+ cols: int = typer.Option(3, "--cols", "-c", help="Number of columns in the grid."),
71
+ width: int = typer.Option(
72
+ 400, "--width", "-w", help="Width of each tile in pixels."
73
+ ),
74
+ skip: float = typer.Option(
75
+ 10.0, "--skip", "-s", help="Seconds to skip from the start."
76
+ ),
77
+ quality: int = typer.Option(95, "--quality", "-q", help="JPEG quality (1-100)."),
78
+ version: Optional[bool] = typer.Option(
79
+ None,
80
+ "--version",
81
+ help="Show version and exit.",
82
+ callback=version_callback,
83
+ is_eager=True,
84
+ ),
85
+ ) -> None:
86
+ """
87
+ Generate a thumbnail sheet (preview) for a video file.
88
+ """
89
+ print_logo()
90
+ if output is None:
91
+ output = video_path.with_name(f"{video_path.stem}_preview.jpg")
92
+
93
+ params = ThumbnailerParams(
94
+ columns=cols,
95
+ rows=rows,
96
+ tile_width=width,
97
+ skip_seconds=skip,
98
+ jpeg_quality=quality,
99
+ )
100
+
101
+ thumbnailer = Thumbnailer(params)
102
+
103
+ try:
104
+ with Progress(
105
+ TextColumn("[progress.description]{task.description}"),
106
+ BarColumn(),
107
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
108
+ TextColumn("({task.completed}/{task.total} frames)"),
109
+ ) as progress:
110
+ task_id = progress.add_task("Extracting frames", total=rows * cols)
111
+
112
+ def progress_callback(done: int, total: int) -> None:
113
+ progress.update(task_id, completed=done, total=total)
114
+
115
+ thumbnailer.create_and_save_preview_thumbnails_for(
116
+ video_path,
117
+ output,
118
+ progress_callback=progress_callback,
119
+ )
120
+
121
+ typer.secho(
122
+ f"\nSuccess! Thumbnail sheet saved to: {output}", fg=typer.colors.GREEN
123
+ )
124
+ except Exception as exc:
125
+ typer.secho(f"\nError: {exc}", fg=typer.colors.RED)
126
+ raise typer.Exit(code=1)
127
+
128
+
129
+ def run() -> None:
130
+ # Show logo if it's the help command
131
+ if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"]:
132
+ print_logo()
133
+ app()
134
+
135
+
136
+ if __name__ == "__main__":
137
+ run()
@@ -0,0 +1,439 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+ from typing import Callable
7
+ from typing import cast
8
+
9
+ import av
10
+ from PIL import Image, ImageColor, ImageDraw, ImageFont
11
+ from pymediainfo import MediaInfo
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class ThumbnailerParams:
16
+ columns: int = 3
17
+ rows: int = 9
18
+ tile_width: int = 400
19
+ spacing: int = 2
20
+ background_color: str = "black"
21
+ header_font_color: str = "white"
22
+ timestamp_font_color: str = "white"
23
+ timestamp_shadow_color: str = "black"
24
+ skip_seconds: float = 10.0
25
+ jpeg_quality: int = 95
26
+ fast_keyframes: bool = True
27
+
28
+
29
+ class Thumbnailer:
30
+ def __init__(self, params: ThumbnailerParams) -> None:
31
+ self.params = params
32
+ self.background_rgb = ImageColor.getrgb(params.background_color)
33
+ self.header_font_rgb = ImageColor.getrgb(params.header_font_color)
34
+ self.timestamp_font_rgb = ImageColor.getrgb(params.timestamp_font_color)
35
+ self.timestamp_shadow_rgb = ImageColor.getrgb(params.timestamp_shadow_color)
36
+ self.header_font = ImageFont.load_default()
37
+ self.timestamp_font = ImageFont.load_default()
38
+
39
+ def create_and_save_preview_thumbnails_for(
40
+ self,
41
+ video_path: Path,
42
+ output_path: Path,
43
+ progress_callback: Callable[[int, int], None] | None = None,
44
+ ) -> None:
45
+ image = self.create_preview_thumbnails_for(
46
+ video_path,
47
+ progress_callback=progress_callback,
48
+ )
49
+ image.save(output_path, quality=self.params.jpeg_quality)
50
+
51
+ def create_preview_thumbnails_for(
52
+ self,
53
+ video_path: Path,
54
+ progress_callback: Callable[[int, int], None] | None = None,
55
+ ) -> Image.Image:
56
+ metadata = self._read_metadata(video_path)
57
+ duration_seconds = float(cast(float, metadata["duration_seconds"]))
58
+ if duration_seconds <= 0:
59
+ raise ValueError(f"Video has no valid duration: {video_path}")
60
+
61
+ tile_count = self.params.columns * self.params.rows
62
+ if tile_count <= 0:
63
+ raise ValueError("rows and columns must produce at least one thumbnail")
64
+
65
+ skip = min(
66
+ max(self.params.skip_seconds, 0.0), max(duration_seconds - 0.001, 0.0)
67
+ )
68
+ timeline = duration_seconds - skip
69
+ if timeline <= 0:
70
+ skip = 0.0
71
+ timeline = duration_seconds
72
+
73
+ step = timeline / tile_count
74
+ timestamps = [skip + ((i + 0.5) * step) for i in range(tile_count)]
75
+
76
+ thumbnails = self._capture_thumbnails(
77
+ video_path,
78
+ timestamps,
79
+ progress_callback=progress_callback,
80
+ )
81
+ if not thumbnails:
82
+ raise ValueError(f"Could not capture thumbnails from: {video_path}")
83
+
84
+ first_width, first_height = thumbnails[0].size
85
+ if first_width <= 0 or first_height <= 0:
86
+ raise ValueError("Captured thumbnail has invalid dimensions")
87
+
88
+ aspect_ratio = first_width / first_height
89
+ thumb_width = self.params.tile_width
90
+ thumb_height = max(1, int(thumb_width / aspect_ratio))
91
+ image_width = thumb_width * self.params.columns + self.params.spacing * (
92
+ self.params.columns + 1
93
+ )
94
+
95
+ header_lines = self._build_header_lines(video_path, metadata)
96
+ text_line_spacing = 2
97
+ header_height = self.params.spacing
98
+ for line in header_lines:
99
+ header_height += self._font_height(line, self.header_font)
100
+ header_height += text_line_spacing
101
+ if header_lines:
102
+ header_height -= text_line_spacing
103
+
104
+ image_height = (
105
+ header_height
106
+ + thumb_height * self.params.rows
107
+ + self.params.spacing * (self.params.rows + 1)
108
+ )
109
+
110
+ output = Image.new(
111
+ "RGB", (image_width, image_height), color=self.background_rgb
112
+ )
113
+ draw = ImageDraw.Draw(output)
114
+
115
+ x = self.params.spacing
116
+ y = self.params.spacing
117
+ for line in header_lines:
118
+ draw.text((x, y), line, fill=self.header_font_rgb, font=self.header_font)
119
+ y += self._font_height(line, self.header_font) + text_line_spacing
120
+
121
+ y = header_height + self.params.spacing
122
+ idx = 0
123
+ for _ in range(self.params.rows):
124
+ x = self.params.spacing
125
+ for _ in range(self.params.columns):
126
+ if idx >= len(thumbnails):
127
+ break
128
+ resized = thumbnails[idx].resize((thumb_width, thumb_height))
129
+ output.paste(resized, (x, y))
130
+ timestamp_label = self._format_time(timestamps[idx])
131
+ self._draw_timestamp(draw, x, y, thumb_height, timestamp_label)
132
+ x += thumb_width + self.params.spacing
133
+ idx += 1
134
+ y += thumb_height + self.params.spacing
135
+
136
+ return output
137
+
138
+ def _capture_thumbnails(
139
+ self,
140
+ video_path: Path,
141
+ timestamps: list[float],
142
+ progress_callback: Callable[[int, int], None] | None = None,
143
+ ) -> list[Image.Image]:
144
+ images: list[Image.Image] = []
145
+ total = len(timestamps)
146
+ captured = 0
147
+
148
+ if progress_callback is not None:
149
+ progress_callback(0, total)
150
+
151
+ with av.open(str(video_path)) as container:
152
+ stream = container.streams.video[0]
153
+ stream.thread_type = "AUTO"
154
+ stream.thread_count = 0
155
+ filter_graph, graph_src, graph_sink = self._build_scale_filter_graph(stream)
156
+
157
+ for target_second in timestamps:
158
+ image = self._capture_frame(
159
+ container,
160
+ stream,
161
+ filter_graph,
162
+ graph_src,
163
+ graph_sink,
164
+ target_second,
165
+ )
166
+ if image is not None:
167
+ images.append(image)
168
+ captured += 1
169
+ if progress_callback is not None:
170
+ progress_callback(captured, total)
171
+
172
+ if images and len(images) < len(timestamps):
173
+ last = images[-1]
174
+ images.extend(last.copy() for _ in range(len(timestamps) - len(images)))
175
+
176
+ return images
177
+
178
+ def _build_scale_filter_graph(
179
+ self,
180
+ stream: av.video.stream.VideoStream,
181
+ ) -> tuple[av.filter.Graph, Any, Any]:
182
+ graph = av.filter.Graph()
183
+ graph_src = cast(Any, graph.add_buffer(template=stream))
184
+ graph_scale = graph.add("scale", args=f"{self.params.tile_width}:-1")
185
+ graph_sink = cast(Any, graph.add("buffersink"))
186
+ graph_src.link_to(graph_scale)
187
+ graph_scale.link_to(graph_sink)
188
+ graph.configure()
189
+ return graph, graph_src, graph_sink
190
+
191
+ def _capture_frame(
192
+ self,
193
+ container: av.container.input.InputContainer,
194
+ stream: av.video.stream.VideoStream,
195
+ filter_graph: av.filter.Graph,
196
+ graph_src: Any,
197
+ graph_sink: Any,
198
+ target_second: float,
199
+ ) -> Image.Image | None:
200
+ seek_second = max(0.0, target_second)
201
+ time_base = float(stream.time_base) if stream.time_base is not None else 0.0
202
+ if time_base <= 0.0:
203
+ return None
204
+
205
+ seek_position = int(seek_second / time_base)
206
+ if stream.start_time is not None:
207
+ seek_position += int(stream.start_time)
208
+
209
+ try:
210
+ container.seek(seek_position, stream=stream, any_frame=False, backward=True)
211
+ codec_context = getattr(stream, "codec_context", None)
212
+ if codec_context is not None:
213
+ codec_context.flush_buffers()
214
+ except Exception:
215
+ return None
216
+
217
+ try:
218
+ fallback: Image.Image | None = None
219
+ for frame in container.decode(video=stream.index):
220
+ if frame.pts is None:
221
+ continue
222
+
223
+ if self.params.fast_keyframes:
224
+ return self._filter_frame(
225
+ frame, filter_graph, graph_src, graph_sink
226
+ )
227
+
228
+ frame_second = float(frame.pts * stream.time_base)
229
+ filtered = self._filter_frame(
230
+ frame, filter_graph, graph_src, graph_sink
231
+ )
232
+ if fallback is None:
233
+ fallback = filtered
234
+ if frame_second >= target_second:
235
+ return filtered
236
+ if frame_second > target_second + 5.0:
237
+ break
238
+
239
+ return fallback
240
+ except Exception:
241
+ return None
242
+
243
+ def _filter_frame(
244
+ self,
245
+ frame: av.VideoFrame,
246
+ filter_graph: av.filter.Graph,
247
+ graph_src: Any,
248
+ graph_sink: Any,
249
+ ) -> Image.Image:
250
+ graph_src.push(frame)
251
+ filtered_frame = graph_sink.pull()
252
+ return filtered_frame.to_image()
253
+
254
+ def _build_header_lines(
255
+ self, video_path: Path, metadata: dict[str, object]
256
+ ) -> list[str]:
257
+ duration_seconds = float(cast(float, metadata["duration_seconds"]))
258
+ file_size = int(cast(int, metadata["file_size"]))
259
+ width = int(cast(int, metadata["width"]))
260
+ height = int(cast(int, metadata["height"]))
261
+ video_format = str(metadata.get("video_format", "unknown"))
262
+ frame_rate = self._first_numeric(metadata.get("frame_rate"), default=None)
263
+ video_bit_rate = self._first_numeric(
264
+ metadata.get("video_bit_rate"), default=None
265
+ )
266
+ audio_format = metadata.get("audio_format")
267
+ audio_bit_rate = self._first_numeric(
268
+ metadata.get("audio_bit_rate"), default=None
269
+ )
270
+ audio_sampling_rate = self._first_numeric(
271
+ metadata.get("audio_sampling_rate"), default=None
272
+ )
273
+ audio_channels = self._first_numeric(
274
+ metadata.get("audio_channels"), default=None
275
+ )
276
+
277
+ lines = [f"File: {video_path.name}"]
278
+ lines.append(
279
+ f"Size: {file_size} B ({self._format_size(file_size)}), Duration: {self._format_time(duration_seconds)}"
280
+ )
281
+
282
+ video_parts = [video_format, f"{width}x{height}"]
283
+ if width > 0 and height > 0:
284
+ video_parts.append(f"({width / height:.2f}:1)")
285
+ if frame_rate:
286
+ video_parts.append(f"{float(frame_rate):.2f} fps")
287
+ if video_bit_rate:
288
+ video_parts.append(self._format_bit_rate(int(video_bit_rate)))
289
+ lines.append(f"Video: {', '.join(video_parts)}")
290
+
291
+ if not audio_format:
292
+ lines.append("Audio: None")
293
+ else:
294
+ audio_parts = [str(audio_format)]
295
+ if audio_sampling_rate:
296
+ audio_parts.append(f"{int(audio_sampling_rate)} Hz")
297
+ if audio_channels:
298
+ channel_count = int(audio_channels)
299
+ if channel_count == 1:
300
+ audio_parts.append("mono")
301
+ elif channel_count == 2:
302
+ audio_parts.append("stereo")
303
+ else:
304
+ audio_parts.append(f"{channel_count} channels")
305
+ if audio_bit_rate:
306
+ audio_parts.append(self._format_bit_rate(int(audio_bit_rate)))
307
+ lines.append(f"Audio: {', '.join(audio_parts)}")
308
+
309
+ return lines
310
+
311
+ def _read_metadata(self, video_path: Path) -> dict[str, object]:
312
+ general_track = None
313
+ video_track = None
314
+ audio_track = None
315
+
316
+ for track in MediaInfo.parse(str(video_path)).tracks:
317
+ if track.track_type == "General" and general_track is None:
318
+ general_track = track.to_data()
319
+ elif track.track_type == "Video" and video_track is None:
320
+ video_track = track.to_data()
321
+ elif track.track_type == "Audio" and audio_track is None:
322
+ audio_track = track.to_data()
323
+
324
+ if video_track is None:
325
+ raise ValueError(f"Unable to read video metadata: {video_path}")
326
+
327
+ duration_ms = self._first_numeric(
328
+ video_track.get("duration") if video_track else None,
329
+ general_track.get("duration") if general_track else None,
330
+ default=0.0,
331
+ )
332
+ duration_seconds = (duration_ms or 0.0) / 1000.0
333
+
334
+ width = int(self._first_numeric(video_track.get("width"), default=0.0) or 0)
335
+ height = int(self._first_numeric(video_track.get("height"), default=0.0) or 0)
336
+ if width <= 0 or height <= 0:
337
+ with av.open(str(video_path)) as container:
338
+ stream = container.streams.video[0]
339
+ width = int(stream.width or 0)
340
+ height = int(stream.height or 0)
341
+
342
+ file_size = int(
343
+ self._first_numeric(
344
+ general_track.get("file_size") if general_track else None,
345
+ default=float(video_path.stat().st_size),
346
+ )
347
+ or 0
348
+ )
349
+
350
+ return {
351
+ "duration_seconds": duration_seconds,
352
+ "file_size": file_size,
353
+ "width": width,
354
+ "height": height,
355
+ "video_format": video_track.get("format", "unknown"),
356
+ "frame_rate": self._first_numeric(
357
+ video_track.get("frame_rate"), default=None
358
+ ),
359
+ "video_bit_rate": self._first_numeric(
360
+ video_track.get("bit_rate"), default=None
361
+ ),
362
+ "audio_format": audio_track.get("format") if audio_track else None,
363
+ "audio_sampling_rate": self._first_numeric(
364
+ audio_track.get("sampling_rate") if audio_track else None,
365
+ default=None,
366
+ ),
367
+ "audio_channels": self._first_numeric(
368
+ audio_track.get("channel_s") if audio_track else None,
369
+ default=None,
370
+ ),
371
+ "audio_bit_rate": self._first_numeric(
372
+ audio_track.get("bit_rate") if audio_track else None,
373
+ default=None,
374
+ ),
375
+ }
376
+
377
+ @staticmethod
378
+ def _first_numeric(*values: object, default: float | None = None) -> float | None:
379
+ for value in values:
380
+ if value is None:
381
+ continue
382
+ if isinstance(value, (int, float)):
383
+ return float(value)
384
+ if not isinstance(value, str):
385
+ continue
386
+ try:
387
+ return float(value)
388
+ except (TypeError, ValueError):
389
+ continue
390
+ return default
391
+
392
+ @staticmethod
393
+ def _format_size(size: int) -> str:
394
+ value = float(size)
395
+ for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
396
+ if value < 1024.0 or unit == "TiB":
397
+ return f"{value:.2f} {unit}"
398
+ value /= 1024.0
399
+ return f"{value:.2f} TiB"
400
+
401
+ @staticmethod
402
+ def _format_time(duration_in_seconds: float) -> str:
403
+ duration = max(0, int(duration_in_seconds))
404
+ hours = duration // 3600
405
+ minutes = (duration % 3600) // 60
406
+ seconds = duration % 60
407
+ return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
408
+
409
+ @staticmethod
410
+ def _format_bit_rate(bits_per_second: int) -> str:
411
+ return f"{int(round(bits_per_second / 1000.0, 0))} kb/s"
412
+
413
+ @staticmethod
414
+ def _font_height(text: str, font: ImageFont.ImageFont) -> int:
415
+ bbox = font.getbbox(text)
416
+ return int(bbox[3] - bbox[1]) + 1
417
+
418
+ def _draw_timestamp(
419
+ self,
420
+ draw: ImageDraw.ImageDraw,
421
+ x: int,
422
+ y: int,
423
+ thumb_height: int,
424
+ text: str,
425
+ ) -> None:
426
+ text_x = x + 6
427
+ text_y = y + thumb_height - self._font_height(text, self.timestamp_font) - 6
428
+ draw.text(
429
+ (text_x + 1, text_y + 1),
430
+ text,
431
+ fill=self.timestamp_shadow_rgb,
432
+ font=self.timestamp_font,
433
+ )
434
+ draw.text(
435
+ (text_x, text_y),
436
+ text,
437
+ fill=self.timestamp_font_rgb,
438
+ font=self.timestamp_font,
439
+ )
@@ -0,0 +1,263 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "annotated-doc"
7
+ version = "0.0.4"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ 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" }
10
+ wheels = [
11
+ { 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" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "av"
16
+ version = "17.0.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/b2/eb/abca886df3a091bc406feb5ff71b4c4f426beaae6b71b9697264ce8c7211/av-17.0.0.tar.gz", hash = "sha256:c53685df73775a8763c375c7b2d62a6cb149d992a26a4b098204da42ade8c3df", size = 4410769, upload-time = "2026-03-14T14:38:45.868Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/95/4d/ea1ac272eeea83014daca1783679a9e9f894e1e68e5eb4f717dd8813da2a/av-17.0.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:4b21bcff4144acae658c0efb011fa8668c7a9638384f3ae7f5add33f35b907c6", size = 23407827, upload-time = "2026-03-14T14:37:47.337Z" },
21
+ { url = "https://files.pythonhosted.org/packages/54/1a/e433766470c57c9c1c8558021de4d2466b3403ed629e48722d39d12baa6c/av-17.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:17cd518fc88dc449ce9dcfd0b40e9b3530266927375a743efc80d510adfb188b", size = 18829899, upload-time = "2026-03-14T14:37:50.493Z" },
22
+ { url = "https://files.pythonhosted.org/packages/5f/25/95ad714f950c188495ffbfef235d06a332123d6f266026a534801ffc2171/av-17.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9a8b7b63a92d8dc7cbe5000546e4684176124ddd49fdd9c12570e3aa6dadf11a", size = 35348062, upload-time = "2026-03-14T14:37:52.964Z" },
23
+ { url = "https://files.pythonhosted.org/packages/7a/db/7f3f9e92f2ac8dba639ab01d69a33b723aa16b5e3e612dbfe667fbc02dcd/av-17.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8706ce9b5d8d087d093b46a9781e7532c4a9e13874bca1da468be78efc56cecc", size = 37684503, upload-time = "2026-03-14T14:37:55.628Z" },
24
+ { url = "https://files.pythonhosted.org/packages/c1/53/3b356b14ba72354688c8d9777cf67b707769b6e14b63aaeb0cddeeac8d32/av-17.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a074835ce807434451086993fedfb3b223dacedb2119ab9d7a72480f2d77f32", size = 36547601, upload-time = "2026-03-14T14:37:58.465Z" },
25
+ { url = "https://files.pythonhosted.org/packages/cd/8d/f489cd6f9fe9c8b38dca00ecb39dc38836761767a4ec07dd95e62e124ac3/av-17.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8ef8e8f1a0cbb2e0ad49266015e2277801a916e2186ac9451b493ff6dfdec27", size = 38815129, upload-time = "2026-03-14T14:38:01.277Z" },
26
+ { url = "https://files.pythonhosted.org/packages/fb/bd/e42536234e37caffd1a054de1a0e6abca226c5686e9672726a8d95511422/av-17.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a795e153ff31a6430e974b4e6ad0d0fab695b78e3f17812293a0a34cd03ee6a9", size = 28984602, upload-time = "2026-03-14T14:38:03.632Z" },
27
+ { url = "https://files.pythonhosted.org/packages/b1/fb/55e3b5b5d1fc61466292f26fbcbabafa2642f378dc48875f8f554591e1a4/av-17.0.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ed4013fac77c309a4a68141dcf6148f1821bb1073a36d4289379762a6372f711", size = 23238424, upload-time = "2026-03-14T14:38:05.856Z" },
28
+ { url = "https://files.pythonhosted.org/packages/52/03/9ace1acc08bc9ae38c14bf3a4b1360e995e4d999d1d33c2cbd7c9e77582a/av-17.0.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:e44b6c83e9f3be9f79ee87d0b77a27cea9a9cd67bd630362c86b7e56a748dfbb", size = 18709043, upload-time = "2026-03-14T14:38:08.288Z" },
29
+ { url = "https://files.pythonhosted.org/packages/00/c0/637721f3cd5bb8bd16105a1a08efd781fc12f449931bdb3a4d0cfd63fa55/av-17.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b440da6ac47da0629d509316f24bcd858f33158dbdd0f1b7293d71e99beb26de", size = 34018780, upload-time = "2026-03-14T14:38:10.45Z" },
30
+ { url = "https://files.pythonhosted.org/packages/d2/59/d19bc3257dd985d55337d7f0414c019414b97e16cd3690ebf9941a847543/av-17.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1060cba85f97f4a337311169d92c0b5e143452cfa5ca0e65fa499d7955e8592e", size = 36358757, upload-time = "2026-03-14T14:38:13.092Z" },
31
+ { url = "https://files.pythonhosted.org/packages/52/6c/a1f4f2677bae6f2ade7a8a18e90ebdcf70690c9b1c4e40e118aa30fa313f/av-17.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:deda202e6021cfc7ba3e816897760ec5431309d59a4da1f75df3c0e9413d71e7", size = 35195281, upload-time = "2026-03-14T14:38:15.789Z" },
32
+ { url = "https://files.pythonhosted.org/packages/90/ea/52b0fc6f69432c7bf3f5fbe6f707113650aa40a1a05b9096ffc2bba4f77d/av-17.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffaf266a1a9c2148072de0a4b5ae98061465178d2cfaa69ee089761149342974", size = 37444817, upload-time = "2026-03-14T14:38:18.563Z" },
33
+ { url = "https://files.pythonhosted.org/packages/34/ad/d2172966282cb8f146c13b6be7416efefde74186460c5e1708ddfc13dba6/av-17.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:45a35a40b2875bf2f98de7c952d74d960f92f319734e6d28e03b4c62a49e6f49", size = 28888553, upload-time = "2026-03-14T14:38:21.223Z" },
34
+ { url = "https://files.pythonhosted.org/packages/b0/bb/c5a4c4172c514d631fb506e6366b503576b8c7f29809cf42aca73e28ff01/av-17.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:3d32e9b5c5bbcb872a0b6917b352a1db8a42142237826c9b49a36d5dbd9e9c26", size = 21916910, upload-time = "2026-03-14T14:38:23.706Z" },
35
+ { url = "https://files.pythonhosted.org/packages/7f/8e/c40ac08e63f79387c59f6ecc38f47d4c942b549130eee579ec1a91f6a291/av-17.0.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:d13250fb4b4522e9a6bec32da082556d5f257110ea223758151375748d9bbe25", size = 23483029, upload-time = "2026-03-14T14:38:25.758Z" },
36
+ { url = "https://files.pythonhosted.org/packages/a9/fb/b4419494bfc249163ec393c613966d66db7e95c76da3345711cd115a79df/av-17.0.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:dbb56aa3b7ae72451d1bf6e9d37c7d83d39b97af712f73583ff419fbf08fc237", size = 18920446, upload-time = "2026-03-14T14:38:27.905Z" },
37
+ { url = "https://files.pythonhosted.org/packages/30/62/c2306d91602ddad2c56106f21dcb334fd51d5ea2e952f7fa025bb8aa39fc/av-17.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a213ac9e83b7ab12c2e9f277a09cac8e9d85cf0883efdab7a87a60e2e4e48879", size = 37477266, upload-time = "2026-03-14T14:38:30.404Z" },
38
+ { url = "https://files.pythonhosted.org/packages/28/cd/c8510a9607886785c0b3ca019d503e888c3757529be42a7287fe2bfa92d5/av-17.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:e15c88bb0921f9435bcc5a27a0863dba571a80ad5e1389c4fcf2073833bb4a74", size = 39572988, upload-time = "2026-03-14T14:38:32.984Z" },
39
+ { url = "https://files.pythonhosted.org/packages/7d/2d/207d9361e25b5abec9be335bbab4df6b6b838e2214be4b374f4cfb285427/av-17.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:096cfd1e9fc896506726c7c42aaf9b370e78c2f257cde4d6ddb6c889bfcc49ec", size = 38399591, upload-time = "2026-03-14T14:38:35.465Z" },
40
+ { url = "https://files.pythonhosted.org/packages/73/ca/307740c6aa2980966bf11383ffcb04bacc5b13f3d268ab4cfb274ad6f793/av-17.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3649ab3d2c7f58049ded1a36e100c0d8fd529cf258f41dd88678ba824034d8c9", size = 40590681, upload-time = "2026-03-14T14:38:38.269Z" },
41
+ { url = "https://files.pythonhosted.org/packages/35/f2/6fdb26d0651adf409864cb2a0d60da107e467d3d1aabc94b234ead54324a/av-17.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e5002271ab2135b551d980c2db8f3299d452e3b9d3633f24f6bb57fffe91cd10", size = 29216337, upload-time = "2026-03-14T14:38:40.83Z" },
42
+ { url = "https://files.pythonhosted.org/packages/41/0a/0896b829a39b5669a2d811e1a79598de661693685cd62b31f11d0c18e65b/av-17.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dba98603fc4665b4f750de86fbaf6c0cfaece970671a9b529e0e3d1711e8367e", size = 22071058, upload-time = "2026-03-14T14:38:43.663Z" },
43
+ ]
44
+
45
+ [[package]]
46
+ name = "click"
47
+ version = "8.3.1"
48
+ source = { registry = "https://pypi.org/simple" }
49
+ dependencies = [
50
+ { name = "colorama", marker = "sys_platform == 'win32'" },
51
+ ]
52
+ 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" }
53
+ wheels = [
54
+ { 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" },
55
+ ]
56
+
57
+ [[package]]
58
+ name = "colorama"
59
+ version = "0.4.6"
60
+ source = { registry = "https://pypi.org/simple" }
61
+ 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" }
62
+ wheels = [
63
+ { 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" },
64
+ ]
65
+
66
+ [[package]]
67
+ name = "markdown-it-py"
68
+ version = "4.0.0"
69
+ source = { registry = "https://pypi.org/simple" }
70
+ dependencies = [
71
+ { name = "mdurl" },
72
+ ]
73
+ 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" }
74
+ wheels = [
75
+ { 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" },
76
+ ]
77
+
78
+ [[package]]
79
+ name = "mdurl"
80
+ version = "0.1.2"
81
+ source = { registry = "https://pypi.org/simple" }
82
+ 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" }
83
+ wheels = [
84
+ { 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" },
85
+ ]
86
+
87
+ [[package]]
88
+ name = "pillow"
89
+ version = "12.1.1"
90
+ source = { registry = "https://pypi.org/simple" }
91
+ sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
92
+ wheels = [
93
+ { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" },
94
+ { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" },
95
+ { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" },
96
+ { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" },
97
+ { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" },
98
+ { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" },
99
+ { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" },
100
+ { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" },
101
+ { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" },
102
+ { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" },
103
+ { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" },
104
+ { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
105
+ { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
106
+ { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
107
+ { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
108
+ { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
109
+ { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
110
+ { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
111
+ { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
112
+ { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
113
+ { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
114
+ { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
115
+ { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
116
+ { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
117
+ { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
118
+ { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
119
+ { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
120
+ { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
121
+ { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
122
+ { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
123
+ { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
124
+ { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
125
+ { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
126
+ { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
127
+ { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
128
+ { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
129
+ { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
130
+ { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
131
+ { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
132
+ { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
133
+ { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
134
+ { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
135
+ { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
136
+ { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
137
+ { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
138
+ { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
139
+ { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
140
+ { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
141
+ { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
142
+ { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
143
+ { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
144
+ { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
145
+ { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
146
+ { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
147
+ { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
148
+ { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
149
+ { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
150
+ { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
151
+ { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
152
+ { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
153
+ { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
154
+ { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
155
+ { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
156
+ { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
157
+ { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
158
+ { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
159
+ { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
160
+ { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
161
+ { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
162
+ { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
163
+ { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
164
+ { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
165
+ { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
166
+ { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
167
+ { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
168
+ { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
169
+ { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
170
+ { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
171
+ { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
172
+ { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
173
+ { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
174
+ { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
175
+ { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
176
+ { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
177
+ { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
178
+ { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
179
+ { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
180
+ { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
181
+ { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
182
+ { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
183
+ ]
184
+
185
+ [[package]]
186
+ name = "pygments"
187
+ version = "2.19.2"
188
+ source = { registry = "https://pypi.org/simple" }
189
+ 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" }
190
+ wheels = [
191
+ { 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" },
192
+ ]
193
+
194
+ [[package]]
195
+ name = "pymediainfo"
196
+ version = "7.0.1"
197
+ source = { registry = "https://pypi.org/simple" }
198
+ sdist = { url = "https://files.pythonhosted.org/packages/4d/80/80a6fb21005b81e30f6193d45cba13857df09f5d483e0551fa6fbb3aaeed/pymediainfo-7.0.1.tar.gz", hash = "sha256:0d5df59ecc615e24c56f303b8f651579c6accab7265715e5d429186d7ba21514", size = 441563, upload-time = "2025-02-12T14:33:15.038Z" }
199
+ wheels = [
200
+ { url = "https://files.pythonhosted.org/packages/a6/4a/d895646df3d3ff617b54d7f06a02ed9d6f5b86673030a543927310e0f7ed/pymediainfo-7.0.1-py3-none-macosx_10_10_universal2.whl", hash = "sha256:286f3bf6299be0997093254e0f371855bc5cf2aaf8641d19455a011e3ee3a84d", size = 6983332, upload-time = "2025-02-12T14:42:47.412Z" },
201
+ { url = "https://files.pythonhosted.org/packages/77/df/bc6b5a08e908c64a81f6ff169716d408ce7380ceff44e1eceb095f49e0dc/pymediainfo-7.0.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:3648e2379fa67bd02433d1e28c707df3a53834dd480680615a9fefd2266f1182", size = 5768082, upload-time = "2025-02-12T14:33:10.543Z" },
202
+ { url = "https://files.pythonhosted.org/packages/02/10/a9bc1446a48d3a15940eb1af79a71978f368f27e2cc86f9ec3ec2d206a20/pymediainfo-7.0.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:cde98112f1ce486589b17a12e5da42085faea996224f7c67fa45b8c1dca719c6", size = 6001553, upload-time = "2025-02-12T14:33:12.663Z" },
203
+ { url = "https://files.pythonhosted.org/packages/ed/7f/c48f8514cb60c9ff9be81b6f383e73e66c7461ef854a1b62628e3c823f13/pymediainfo-7.0.1-py3-none-win32.whl", hash = "sha256:01bcaf82b72cefbf4b96f13b2547e1b2e0e734bab7173d7c33f7f01acc07c98b", size = 3125046, upload-time = "2025-02-12T15:04:39.89Z" },
204
+ { url = "https://files.pythonhosted.org/packages/e7/26/9d50c2a330541bc36c0ea7ce29eeff5b0c35c2624139660df8bcfa9ae3ce/pymediainfo-7.0.1-py3-none-win_amd64.whl", hash = "sha256:13224fa7590e198763b8baf072e704ea81d334e71aa32a469091460e243893c7", size = 3271232, upload-time = "2025-02-12T15:07:13.672Z" },
205
+ ]
206
+
207
+ [[package]]
208
+ name = "rich"
209
+ version = "14.3.3"
210
+ source = { registry = "https://pypi.org/simple" }
211
+ dependencies = [
212
+ { name = "markdown-it-py" },
213
+ { name = "pygments" },
214
+ ]
215
+ sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
216
+ wheels = [
217
+ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
218
+ ]
219
+
220
+ [[package]]
221
+ name = "shellingham"
222
+ version = "1.5.4"
223
+ source = { registry = "https://pypi.org/simple" }
224
+ 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" }
225
+ wheels = [
226
+ { 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" },
227
+ ]
228
+
229
+ [[package]]
230
+ name = "thumby"
231
+ version = "0.1.0"
232
+ source = { editable = "." }
233
+ dependencies = [
234
+ { name = "av" },
235
+ { name = "pillow" },
236
+ { name = "pymediainfo" },
237
+ { name = "rich" },
238
+ { name = "typer" },
239
+ ]
240
+
241
+ [package.metadata]
242
+ requires-dist = [
243
+ { name = "av", specifier = ">=9.0.0" },
244
+ { name = "pillow", specifier = ">=9.2.0" },
245
+ { name = "pymediainfo", specifier = ">=4.3" },
246
+ { name = "rich", specifier = ">=13.0.0" },
247
+ { name = "typer", specifier = ">=0.15.0" },
248
+ ]
249
+
250
+ [[package]]
251
+ name = "typer"
252
+ version = "0.24.1"
253
+ source = { registry = "https://pypi.org/simple" }
254
+ dependencies = [
255
+ { name = "annotated-doc" },
256
+ { name = "click" },
257
+ { name = "rich" },
258
+ { name = "shellingham" },
259
+ ]
260
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
261
+ wheels = [
262
+ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
263
+ ]