devbits 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.
- devbits-0.1.0/LICENSE +21 -0
- devbits-0.1.0/PKG-INFO +175 -0
- devbits-0.1.0/README.md +159 -0
- devbits-0.1.0/devbits/__init__.py +3 -0
- devbits-0.1.0/devbits/cache.py +32 -0
- devbits-0.1.0/devbits/cli.py +228 -0
- devbits-0.1.0/devbits/image.py +96 -0
- devbits-0.1.0/devbits/media.py +186 -0
- devbits-0.1.0/devbits/project.py +61 -0
- devbits-0.1.0/devbits/scripts.py +75 -0
- devbits-0.1.0/devbits/utils.py +85 -0
- devbits-0.1.0/devbits.egg-info/PKG-INFO +175 -0
- devbits-0.1.0/devbits.egg-info/SOURCES.txt +18 -0
- devbits-0.1.0/devbits.egg-info/dependency_links.txt +1 -0
- devbits-0.1.0/devbits.egg-info/entry_points.txt +18 -0
- devbits-0.1.0/devbits.egg-info/requires.txt +2 -0
- devbits-0.1.0/devbits.egg-info/top_level.txt +1 -0
- devbits-0.1.0/pyproject.toml +42 -0
- devbits-0.1.0/setup.cfg +4 -0
- devbits-0.1.0/tests/test_cli.py +28 -0
devbits-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruce Chuang
|
|
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.
|
devbits-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devbits
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight CLI toolkit for daily development utilities.
|
|
5
|
+
Author: Bruce Chuang
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/<your-github-username>/devbits
|
|
8
|
+
Project-URL: Repository, https://github.com/<your-github-username>/devbits
|
|
9
|
+
Project-URL: Issues, https://github.com/<your-github-username>/devbits/issues
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: opencv-python>=4.8.0
|
|
14
|
+
Requires-Dist: pillow>=10.0.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# devbits
|
|
18
|
+
|
|
19
|
+
## CLI style
|
|
20
|
+
|
|
21
|
+
`devbits` provides both a main command and standalone commands installed into your Python environment's PATH.
|
|
22
|
+
|
|
23
|
+
Main command style:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
devbits clearcache .
|
|
27
|
+
devbits images2video frames/ -o output.mp4 --fps 30
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Standalone command style:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
clearcache .
|
|
34
|
+
images2video frames/ -o output.mp4 --fps 30
|
|
35
|
+
video2gif input.mp4 -o output.gif
|
|
36
|
+
clipvideo input.mp4 --start 3 --end 10 -o clip.mp4
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
After editable install, reopen the terminal or run `hash -r` if your shell does not immediately find the new commands.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
`devbits` is a lightweight CLI toolkit for daily development utilities, including cache cleanup, media conversion, image helpers, dataset-like file operations, and project maintenance tools.
|
|
43
|
+
|
|
44
|
+
The project supports two CLI styles:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
devbits <command> [options]
|
|
48
|
+
<command> [options]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
### Project cleanup
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
devbits clearcache .
|
|
57
|
+
devbits clearcache . --all
|
|
58
|
+
devbits clearcache . --dry-run
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Removes:
|
|
62
|
+
|
|
63
|
+
- `__pycache__/`
|
|
64
|
+
- `*.pyc`
|
|
65
|
+
- `*.pyo`
|
|
66
|
+
- optionally `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`
|
|
67
|
+
|
|
68
|
+
### Image and video conversion
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
devbits images2video ./frames -o output.mp4 --fps 30
|
|
72
|
+
devbits video2images input.mp4 -o ./frames --every 1
|
|
73
|
+
devbits images2gif ./frames -o output.gif --fps 10
|
|
74
|
+
devbits video2gif input.mp4 -o output.gif --fps 10 --start 2 --end 8
|
|
75
|
+
devbits clipvideo input.mp4 -o clip.mp4 --start 3 --end 10
|
|
76
|
+
devbits clipvideo input.mp4 -o clip.mp4 --start-frame 100 --end-frame 500
|
|
77
|
+
devbits resizevideo input.mp4 -o resized.mp4 --size 1280,720
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Image utilities
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
devbits image2ico logo.png -o logo.ico
|
|
84
|
+
devbits resizeimage input.jpg -o output.jpg --size 640,480
|
|
85
|
+
devbits batchimages ./images -o ./resized --size 640,480 --format jpg
|
|
86
|
+
devbits checkimages ./images --recursive
|
|
87
|
+
devbits contactsheet ./images -o sheet.jpg --cols 5 --labels
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Project utilities
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
devbits tree . --depth 3
|
|
94
|
+
devbits size . --top 20
|
|
95
|
+
devbits renamefiles ./images --prefix frame --digits 6
|
|
96
|
+
devbits renamefiles ./images --prefix frame --digits 6 --dry-run
|
|
97
|
+
devbits samplefiles ./images -o ./sample --num 100
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Installation for local development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git clone https://github.com/yourname/devbits.git
|
|
104
|
+
cd devbits
|
|
105
|
+
python -m venv .venv
|
|
106
|
+
source .venv/bin/activate
|
|
107
|
+
pip install -e ".[dev]"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Check the CLI:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
devbits --help
|
|
114
|
+
devbits images2video --help
|
|
115
|
+
clearcache --help
|
|
116
|
+
images2video --help
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Build package
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python -m pip install --upgrade build twine
|
|
123
|
+
python -m build
|
|
124
|
+
python -m twine check dist/*
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Upload to TestPyPI
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python -m twine upload --repository testpypi dist/*
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Upload to PyPI
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m twine upload dist/*
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Commands
|
|
140
|
+
|
|
141
|
+
| Command | Description |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `clearcache` | Clear Python cache files. |
|
|
144
|
+
| `images2video` | Convert image sequence to MP4 video. |
|
|
145
|
+
| `video2images` | Extract frames from a video. |
|
|
146
|
+
| `images2gif` | Convert image sequence to GIF. |
|
|
147
|
+
| `video2gif` | Convert video to GIF. |
|
|
148
|
+
| `clipvideo` | Clip video by seconds or frame indices. |
|
|
149
|
+
| `resizevideo` | Resize a video. |
|
|
150
|
+
| `image2ico` | Convert image to `.ico`. |
|
|
151
|
+
| `resizeimage` | Resize an image. |
|
|
152
|
+
| `batchimages` | Batch resize or convert images. |
|
|
153
|
+
| `checkimages` | Check broken image files. |
|
|
154
|
+
| `contactsheet` | Create a contact sheet. |
|
|
155
|
+
| `tree` | Print a compact project tree. |
|
|
156
|
+
| `size` | Show top-level folder/file sizes. |
|
|
157
|
+
| `renamefiles` | Batch rename files. |
|
|
158
|
+
| `samplefiles` | Copy or move first N files. |
|
|
159
|
+
|
|
160
|
+
## Roadmap
|
|
161
|
+
|
|
162
|
+
- Interactive `clipvideo --gui`
|
|
163
|
+
- `mergevideos`
|
|
164
|
+
- `comparevideos`
|
|
165
|
+
- `concatframes`
|
|
166
|
+
- `annotatevideo`
|
|
167
|
+
- `watermark`
|
|
168
|
+
- `splitdataset`
|
|
169
|
+
- `countdataset`
|
|
170
|
+
- `dedupimages`
|
|
171
|
+
- Optional ROS helpers under `devbits[ros]`
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
devbits-0.1.0/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# devbits
|
|
2
|
+
|
|
3
|
+
## CLI style
|
|
4
|
+
|
|
5
|
+
`devbits` provides both a main command and standalone commands installed into your Python environment's PATH.
|
|
6
|
+
|
|
7
|
+
Main command style:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
devbits clearcache .
|
|
11
|
+
devbits images2video frames/ -o output.mp4 --fps 30
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Standalone command style:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
clearcache .
|
|
18
|
+
images2video frames/ -o output.mp4 --fps 30
|
|
19
|
+
video2gif input.mp4 -o output.gif
|
|
20
|
+
clipvideo input.mp4 --start 3 --end 10 -o clip.mp4
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After editable install, reopen the terminal or run `hash -r` if your shell does not immediately find the new commands.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
`devbits` is a lightweight CLI toolkit for daily development utilities, including cache cleanup, media conversion, image helpers, dataset-like file operations, and project maintenance tools.
|
|
27
|
+
|
|
28
|
+
The project supports two CLI styles:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
devbits <command> [options]
|
|
32
|
+
<command> [options]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
### Project cleanup
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
devbits clearcache .
|
|
41
|
+
devbits clearcache . --all
|
|
42
|
+
devbits clearcache . --dry-run
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Removes:
|
|
46
|
+
|
|
47
|
+
- `__pycache__/`
|
|
48
|
+
- `*.pyc`
|
|
49
|
+
- `*.pyo`
|
|
50
|
+
- optionally `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`
|
|
51
|
+
|
|
52
|
+
### Image and video conversion
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
devbits images2video ./frames -o output.mp4 --fps 30
|
|
56
|
+
devbits video2images input.mp4 -o ./frames --every 1
|
|
57
|
+
devbits images2gif ./frames -o output.gif --fps 10
|
|
58
|
+
devbits video2gif input.mp4 -o output.gif --fps 10 --start 2 --end 8
|
|
59
|
+
devbits clipvideo input.mp4 -o clip.mp4 --start 3 --end 10
|
|
60
|
+
devbits clipvideo input.mp4 -o clip.mp4 --start-frame 100 --end-frame 500
|
|
61
|
+
devbits resizevideo input.mp4 -o resized.mp4 --size 1280,720
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Image utilities
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
devbits image2ico logo.png -o logo.ico
|
|
68
|
+
devbits resizeimage input.jpg -o output.jpg --size 640,480
|
|
69
|
+
devbits batchimages ./images -o ./resized --size 640,480 --format jpg
|
|
70
|
+
devbits checkimages ./images --recursive
|
|
71
|
+
devbits contactsheet ./images -o sheet.jpg --cols 5 --labels
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Project utilities
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
devbits tree . --depth 3
|
|
78
|
+
devbits size . --top 20
|
|
79
|
+
devbits renamefiles ./images --prefix frame --digits 6
|
|
80
|
+
devbits renamefiles ./images --prefix frame --digits 6 --dry-run
|
|
81
|
+
devbits samplefiles ./images -o ./sample --num 100
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Installation for local development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git clone https://github.com/yourname/devbits.git
|
|
88
|
+
cd devbits
|
|
89
|
+
python -m venv .venv
|
|
90
|
+
source .venv/bin/activate
|
|
91
|
+
pip install -e ".[dev]"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Check the CLI:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
devbits --help
|
|
98
|
+
devbits images2video --help
|
|
99
|
+
clearcache --help
|
|
100
|
+
images2video --help
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Build package
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python -m pip install --upgrade build twine
|
|
107
|
+
python -m build
|
|
108
|
+
python -m twine check dist/*
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Upload to TestPyPI
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python -m twine upload --repository testpypi dist/*
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Upload to PyPI
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m twine upload dist/*
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Commands
|
|
124
|
+
|
|
125
|
+
| Command | Description |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `clearcache` | Clear Python cache files. |
|
|
128
|
+
| `images2video` | Convert image sequence to MP4 video. |
|
|
129
|
+
| `video2images` | Extract frames from a video. |
|
|
130
|
+
| `images2gif` | Convert image sequence to GIF. |
|
|
131
|
+
| `video2gif` | Convert video to GIF. |
|
|
132
|
+
| `clipvideo` | Clip video by seconds or frame indices. |
|
|
133
|
+
| `resizevideo` | Resize a video. |
|
|
134
|
+
| `image2ico` | Convert image to `.ico`. |
|
|
135
|
+
| `resizeimage` | Resize an image. |
|
|
136
|
+
| `batchimages` | Batch resize or convert images. |
|
|
137
|
+
| `checkimages` | Check broken image files. |
|
|
138
|
+
| `contactsheet` | Create a contact sheet. |
|
|
139
|
+
| `tree` | Print a compact project tree. |
|
|
140
|
+
| `size` | Show top-level folder/file sizes. |
|
|
141
|
+
| `renamefiles` | Batch rename files. |
|
|
142
|
+
| `samplefiles` | Copy or move first N files. |
|
|
143
|
+
|
|
144
|
+
## Roadmap
|
|
145
|
+
|
|
146
|
+
- Interactive `clipvideo --gui`
|
|
147
|
+
- `mergevideos`
|
|
148
|
+
- `comparevideos`
|
|
149
|
+
- `concatframes`
|
|
150
|
+
- `annotatevideo`
|
|
151
|
+
- `watermark`
|
|
152
|
+
- `splitdataset`
|
|
153
|
+
- `countdataset`
|
|
154
|
+
- `dedupimages`
|
|
155
|
+
- Optional ROS helpers under `devbits[ros]`
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
CACHE_DIR_NAMES = {"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"}
|
|
7
|
+
PYC_PATTERNS = ["*.pyc", "*.pyo"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def clear_cache(root: Path, include_extra: bool = False, dry_run: bool = False) -> list[Path]:
|
|
11
|
+
targets: list[Path] = []
|
|
12
|
+
dir_names = set(CACHE_DIR_NAMES if include_extra else {"__pycache__"})
|
|
13
|
+
|
|
14
|
+
for path in root.rglob("*"):
|
|
15
|
+
if path.is_dir() and path.name in dir_names:
|
|
16
|
+
targets.append(path)
|
|
17
|
+
|
|
18
|
+
for pattern in PYC_PATTERNS:
|
|
19
|
+
targets.extend(root.rglob(pattern))
|
|
20
|
+
|
|
21
|
+
unique_targets = sorted(set(targets), key=lambda p: len(p.parts), reverse=True)
|
|
22
|
+
if dry_run:
|
|
23
|
+
return unique_targets
|
|
24
|
+
|
|
25
|
+
for target in unique_targets:
|
|
26
|
+
if not target.exists():
|
|
27
|
+
continue
|
|
28
|
+
if target.is_dir():
|
|
29
|
+
shutil.rmtree(target)
|
|
30
|
+
else:
|
|
31
|
+
target.unlink()
|
|
32
|
+
return unique_targets
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .cache import clear_cache
|
|
8
|
+
from .image import batch_images, check_images, contact_sheet, image_to_ico, resize_image
|
|
9
|
+
from .media import clip_video, images_to_gif, images_to_video, resize_video, video_to_gif, video_to_images
|
|
10
|
+
from .project import print_tree, rename_files, sample_files, top_sizes
|
|
11
|
+
from .utils import ensure_exists
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
15
|
+
parser = argparse.ArgumentParser(prog="devbits", description="Daily development utility CLI toolkit.")
|
|
16
|
+
parser.add_argument("--version", action="version", version="devbits 0.1.0")
|
|
17
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
18
|
+
|
|
19
|
+
p = sub.add_parser("clearcache", help="Clear Python cache files under a folder.")
|
|
20
|
+
p.add_argument("folder", type=Path)
|
|
21
|
+
p.add_argument("--all", action="store_true", help="Also remove pytest, mypy, and ruff caches.")
|
|
22
|
+
p.add_argument("--dry-run", action="store_true")
|
|
23
|
+
p.set_defaults(func=cmd_clearcache)
|
|
24
|
+
|
|
25
|
+
p = sub.add_parser("images2video", help="Convert image sequence to MP4 video.")
|
|
26
|
+
p.add_argument("folder", type=Path)
|
|
27
|
+
p.add_argument("-o", "--output", type=Path, default=Path("output.mp4"))
|
|
28
|
+
p.add_argument("--fps", type=float, default=30.0)
|
|
29
|
+
p.add_argument("--pattern", default="*")
|
|
30
|
+
p.set_defaults(func=cmd_images2video)
|
|
31
|
+
|
|
32
|
+
p = sub.add_parser("video2images", help="Extract frames from a video.")
|
|
33
|
+
p.add_argument("video", type=Path)
|
|
34
|
+
p.add_argument("-o", "--output", type=Path, default=Path("frames"))
|
|
35
|
+
p.add_argument("--every", type=int, default=1)
|
|
36
|
+
p.add_argument("--prefix", default="frame")
|
|
37
|
+
p.add_argument("--digits", type=int, default=6)
|
|
38
|
+
p.add_argument("--format", default="jpg")
|
|
39
|
+
p.set_defaults(func=cmd_video2images)
|
|
40
|
+
|
|
41
|
+
p = sub.add_parser("images2gif", help="Convert image sequence to GIF.")
|
|
42
|
+
p.add_argument("folder", type=Path)
|
|
43
|
+
p.add_argument("-o", "--output", type=Path, default=Path("output.gif"))
|
|
44
|
+
p.add_argument("--fps", type=float, default=10.0)
|
|
45
|
+
p.add_argument("--pattern", default="*")
|
|
46
|
+
p.set_defaults(func=cmd_images2gif)
|
|
47
|
+
|
|
48
|
+
p = sub.add_parser("video2gif", help="Convert video to GIF.")
|
|
49
|
+
p.add_argument("video", type=Path)
|
|
50
|
+
p.add_argument("-o", "--output", type=Path, default=Path("output.gif"))
|
|
51
|
+
p.add_argument("--fps", type=float, default=10.0)
|
|
52
|
+
p.add_argument("--start", type=float)
|
|
53
|
+
p.add_argument("--end", type=float)
|
|
54
|
+
p.add_argument("--size", help="Output size, e.g. 640,360")
|
|
55
|
+
p.set_defaults(func=cmd_video2gif)
|
|
56
|
+
|
|
57
|
+
p = sub.add_parser("clipvideo", help="Clip video by seconds or frame indices.")
|
|
58
|
+
p.add_argument("video", type=Path)
|
|
59
|
+
p.add_argument("-o", "--output", type=Path, default=Path("clip.mp4"))
|
|
60
|
+
p.add_argument("--start", type=float)
|
|
61
|
+
p.add_argument("--end", type=float)
|
|
62
|
+
p.add_argument("--start-frame", type=int)
|
|
63
|
+
p.add_argument("--end-frame", type=int)
|
|
64
|
+
p.add_argument("--gui", action="store_true", help="Reserved for future GUI clip selection.")
|
|
65
|
+
p.set_defaults(func=cmd_clipvideo)
|
|
66
|
+
|
|
67
|
+
p = sub.add_parser("resizevideo", help="Resize a video.")
|
|
68
|
+
p.add_argument("video", type=Path)
|
|
69
|
+
p.add_argument("-o", "--output", type=Path, default=Path("resized.mp4"))
|
|
70
|
+
p.add_argument("--size", required=True, help="Output size, e.g. 1280,720")
|
|
71
|
+
p.set_defaults(func=cmd_resizevideo)
|
|
72
|
+
|
|
73
|
+
p = sub.add_parser("image2ico", help="Convert an image to ICO.")
|
|
74
|
+
p.add_argument("image", type=Path)
|
|
75
|
+
p.add_argument("-o", "--output", type=Path, default=Path("image.ico"))
|
|
76
|
+
p.add_argument("--sizes", default="16,32,48,64,128,256")
|
|
77
|
+
p.set_defaults(func=cmd_image2ico)
|
|
78
|
+
|
|
79
|
+
p = sub.add_parser("resizeimage", help="Resize an image.")
|
|
80
|
+
p.add_argument("image", type=Path)
|
|
81
|
+
p.add_argument("-o", "--output", type=Path, default=Path("resized.jpg"))
|
|
82
|
+
p.add_argument("--size", required=True)
|
|
83
|
+
p.add_argument("--no-keep-ratio", action="store_true")
|
|
84
|
+
p.set_defaults(func=cmd_resizeimage)
|
|
85
|
+
|
|
86
|
+
p = sub.add_parser("batchimages", help="Batch resize or convert images.")
|
|
87
|
+
p.add_argument("folder", type=Path)
|
|
88
|
+
p.add_argument("-o", "--output", type=Path, required=True)
|
|
89
|
+
p.add_argument("--size")
|
|
90
|
+
p.add_argument("--format")
|
|
91
|
+
p.set_defaults(func=cmd_batchimages)
|
|
92
|
+
|
|
93
|
+
p = sub.add_parser("checkimages", help="Check broken image files.")
|
|
94
|
+
p.add_argument("folder", type=Path)
|
|
95
|
+
p.add_argument("--recursive", action="store_true")
|
|
96
|
+
p.add_argument("--remove-broken", action="store_true")
|
|
97
|
+
p.set_defaults(func=cmd_checkimages)
|
|
98
|
+
|
|
99
|
+
p = sub.add_parser("contactsheet", help="Create a contact sheet from images.")
|
|
100
|
+
p.add_argument("folder", type=Path)
|
|
101
|
+
p.add_argument("-o", "--output", type=Path, default=Path("sheet.jpg"))
|
|
102
|
+
p.add_argument("--cols", type=int, default=5)
|
|
103
|
+
p.add_argument("--thumb-size", default="256,256")
|
|
104
|
+
p.add_argument("--labels", action="store_true")
|
|
105
|
+
p.set_defaults(func=cmd_contactsheet)
|
|
106
|
+
|
|
107
|
+
p = sub.add_parser("tree", help="Print project tree.")
|
|
108
|
+
p.add_argument("folder", type=Path, nargs="?", default=Path("."))
|
|
109
|
+
p.add_argument("--depth", type=int, default=3)
|
|
110
|
+
p.set_defaults(func=cmd_tree)
|
|
111
|
+
|
|
112
|
+
p = sub.add_parser("size", help="Show top-level folder sizes.")
|
|
113
|
+
p.add_argument("folder", type=Path, nargs="?", default=Path("."))
|
|
114
|
+
p.add_argument("--top", type=int, default=20)
|
|
115
|
+
p.set_defaults(func=cmd_size)
|
|
116
|
+
|
|
117
|
+
p = sub.add_parser("renamefiles", help="Batch rename files in a folder.")
|
|
118
|
+
p.add_argument("folder", type=Path)
|
|
119
|
+
p.add_argument("--prefix", default="file")
|
|
120
|
+
p.add_argument("--digits", type=int, default=6)
|
|
121
|
+
p.add_argument("--start", type=int, default=1)
|
|
122
|
+
p.add_argument("--dry-run", action="store_true")
|
|
123
|
+
p.set_defaults(func=cmd_renamefiles)
|
|
124
|
+
|
|
125
|
+
p = sub.add_parser("samplefiles", help="Copy first N files into another folder.")
|
|
126
|
+
p.add_argument("folder", type=Path)
|
|
127
|
+
p.add_argument("-o", "--output", type=Path, required=True)
|
|
128
|
+
p.add_argument("--num", type=int, required=True)
|
|
129
|
+
p.add_argument("--move", action="store_true")
|
|
130
|
+
p.set_defaults(func=cmd_samplefiles)
|
|
131
|
+
return parser
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_clearcache(args: argparse.Namespace) -> None:
|
|
135
|
+
targets = clear_cache(ensure_exists(args.folder), include_extra=args.all, dry_run=args.dry_run)
|
|
136
|
+
for target in targets:
|
|
137
|
+
print(target)
|
|
138
|
+
print(f"{'Found' if args.dry_run else 'Removed'} {len(targets)} cache item(s).")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cmd_images2video(args: argparse.Namespace) -> None:
|
|
142
|
+
print(images_to_video(ensure_exists(args.folder), args.output, args.fps, args.pattern))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_video2images(args: argparse.Namespace) -> None:
|
|
146
|
+
outputs = video_to_images(ensure_exists(args.video), args.output, args.every, args.prefix, args.digits, args.format)
|
|
147
|
+
print(f"Saved {len(outputs)} frame(s) to {args.output}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_images2gif(args: argparse.Namespace) -> None:
|
|
151
|
+
print(images_to_gif(ensure_exists(args.folder), args.output, args.fps, args.pattern))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cmd_video2gif(args: argparse.Namespace) -> None:
|
|
155
|
+
print(video_to_gif(ensure_exists(args.video), args.output, args.fps, args.start, args.end, args.size))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_clipvideo(args: argparse.Namespace) -> None:
|
|
159
|
+
if args.gui:
|
|
160
|
+
print("Warning: --gui is reserved for a future interactive clip selector; using CLI options now.")
|
|
161
|
+
print(clip_video(ensure_exists(args.video), args.output, args.start, args.end, args.start_frame, args.end_frame))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_resizevideo(args: argparse.Namespace) -> None:
|
|
165
|
+
print(resize_video(ensure_exists(args.video), args.output, args.size))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cmd_image2ico(args: argparse.Namespace) -> None:
|
|
169
|
+
print(image_to_ico(ensure_exists(args.image), args.output, args.sizes))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cmd_resizeimage(args: argparse.Namespace) -> None:
|
|
173
|
+
print(resize_image(ensure_exists(args.image), args.output, args.size, not args.no_keep_ratio))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def cmd_batchimages(args: argparse.Namespace) -> None:
|
|
177
|
+
outputs = batch_images(ensure_exists(args.folder), args.output, args.size, args.format)
|
|
178
|
+
print(f"Saved {len(outputs)} image(s) to {args.output}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def cmd_checkimages(args: argparse.Namespace) -> None:
|
|
182
|
+
broken = check_images(ensure_exists(args.folder), args.recursive, args.remove_broken)
|
|
183
|
+
if broken:
|
|
184
|
+
print("Broken images:")
|
|
185
|
+
for path in broken:
|
|
186
|
+
print(path)
|
|
187
|
+
print(f"Found {len(broken)} broken image(s).")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_contactsheet(args: argparse.Namespace) -> None:
|
|
191
|
+
print(contact_sheet(ensure_exists(args.folder), args.output, args.cols, args.thumb_size, labels=args.labels))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def cmd_tree(args: argparse.Namespace) -> None:
|
|
195
|
+
for line in print_tree(ensure_exists(args.folder), args.depth):
|
|
196
|
+
print(line)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_size(args: argparse.Namespace) -> None:
|
|
200
|
+
for path, _, size_text in top_sizes(ensure_exists(args.folder), args.top):
|
|
201
|
+
print(f"{size_text:>10} {path}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def cmd_renamefiles(args: argparse.Namespace) -> None:
|
|
205
|
+
mappings = rename_files(ensure_exists(args.folder), args.prefix, args.digits, args.start, args.dry_run)
|
|
206
|
+
for old, new in mappings:
|
|
207
|
+
print(f"{old.name} -> {new.name}")
|
|
208
|
+
print(f"{'Planned' if args.dry_run else 'Renamed'} {len(mappings)} file(s).")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def cmd_samplefiles(args: argparse.Namespace) -> None:
|
|
212
|
+
outputs = sample_files(ensure_exists(args.folder), args.output, args.num, copy=not args.move)
|
|
213
|
+
print(f"Saved {len(outputs)} file(s) to {args.output}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main(argv: list[str] | None = None) -> int:
|
|
217
|
+
parser = build_parser()
|
|
218
|
+
args = parser.parse_args(argv)
|
|
219
|
+
try:
|
|
220
|
+
args.func(args)
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
223
|
+
return 1
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
raise SystemExit(main())
|