VideoSpeed 1.0.4__tar.gz → 1.0.5__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.
- videospeed-1.0.5/LICENSE.md +21 -0
- videospeed-1.0.5/PKG-INFO +150 -0
- videospeed-1.0.5/README.md +133 -0
- {videospeed-1.0.4 → videospeed-1.0.5}/pyproject.toml +1 -1
- videospeed-1.0.5/src/VideoSpeed/__init__.py +3 -0
- videospeed-1.0.5/src/VideoSpeed/cli.py +489 -0
- videospeed-1.0.5/src/VideoSpeed.egg-info/PKG-INFO +150 -0
- {videospeed-1.0.4 → videospeed-1.0.5}/src/VideoSpeed.egg-info/SOURCES.txt +2 -1
- {videospeed-1.0.4 → videospeed-1.0.5}/src/VideoSpeed.egg-info/top_level.txt +0 -1
- videospeed-1.0.4/PKG-INFO +0 -100
- videospeed-1.0.4/README.md +0 -85
- videospeed-1.0.4/src/VideoSpeed/__init__.py +0 -4
- videospeed-1.0.4/src/VideoSpeed.egg-info/PKG-INFO +0 -100
- videospeed-1.0.4/src/setup.py +0 -7
- {videospeed-1.0.4 → videospeed-1.0.5}/setup.cfg +0 -0
- {videospeed-1.0.4 → videospeed-1.0.5}/src/VideoSpeed/__main__.py +0 -0
- /videospeed-1.0.4/src/VideoSpeed/cli.py → /videospeed-1.0.5/src/VideoSpeed/cli.bootstrap-old.py +0 -0
- {videospeed-1.0.4 → videospeed-1.0.5}/src/VideoSpeed.egg-info/dependency_links.txt +0 -0
- {videospeed-1.0.4 → videospeed-1.0.5}/src/VideoSpeed.egg-info/entry_points.txt +0 -0
- {videospeed-1.0.4 → videospeed-1.0.5}/src/VideoSpeed.egg-info/requires.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wilgat Wong <wilgat.wong@gmail.com>
|
|
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,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: VideoSpeed
|
|
3
|
+
Version: 1.0.5
|
|
4
|
+
Summary: A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos.
|
|
5
|
+
Author-email: Wilgat Wong <wilgat.wong@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Wilgat/VideoSpeed
|
|
8
|
+
Project-URL: Repository, https://github.com/Wilgat/VideoSpeed
|
|
9
|
+
Project-URL: Issues, https://github.com/Wilgat/VideoSpeed/issues
|
|
10
|
+
Keywords: Video Editing,Multimeida
|
|
11
|
+
Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE.md
|
|
14
|
+
Requires-Dist: ChronicleLogger>=1.2.3
|
|
15
|
+
Requires-Dist: opencv-python
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# VideoSpeed - Cut, speed, and boomerang MP4 clips from the CLI
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+

|
|
22
|
+
[-purple.svg)](https://github.com/cloudgen/ciao)
|
|
23
|
+
[](https://github.com/Wilgat/VideoSpeed)
|
|
24
|
+
[]()
|
|
25
|
+
[](https://pypi.org/project/VideoSpeed/)
|
|
26
|
+
|
|
27
|
+
VideoSpeed is an interactive command-line tool for short MP4 edits: pick a folder of videos, cut a time range, change length/speed (percent), and optionally append a boomerang (forward + reverse). Encoding uses **FFmpeg**; duration probing uses **OpenCV**.
|
|
28
|
+
|
|
29
|
+
Package version SSOT: `pyproject.toml` and `src/VideoSpeed/__init__.py` (**1.0.5**). Console entrypoint: `video-speed`.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Recursive **MP4** discovery under a chosen folder
|
|
34
|
+
- Interactive **cut** (start/end seconds) with invalid-range re-prompt
|
|
35
|
+
- **Length/speed** change (**20–200%**) with A/V tempo kept in sync
|
|
36
|
+
- Optional **boomerang** (forward then reverse)
|
|
37
|
+
- **USB-safe** intermediate files (staged next to the output) and final publish via `shutil.move`
|
|
38
|
+
- **`--help`** / **`--version`**; bare invoke starts the interactive session (Type N)
|
|
39
|
+
- Fail-closed when **FFmpeg** is missing from `PATH`
|
|
40
|
+
|
|
41
|
+
## Quick Installation
|
|
42
|
+
|
|
43
|
+
### Prerequisites (system)
|
|
44
|
+
|
|
45
|
+
- **FFmpeg** on `PATH` (`ffmpeg -version`)
|
|
46
|
+
- A supported **Python** environment (see `requires-python` in `pyproject.toml`)
|
|
47
|
+
|
|
48
|
+
### From PyPI (registry channel)
|
|
49
|
+
|
|
50
|
+
Package name SSOT: **`VideoSpeed`** (`pyproject.toml` `[project].name`). Upload path: `build.sh` (`twine upload`).
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install VideoSpeed
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If the index is behind a local release, install from a checkout or wheel instead (below). Live index version is shown by the PyPI badge above.
|
|
57
|
+
|
|
58
|
+
### From a git checkout (development)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone git@github.com:Wilgat/VideoSpeed.git
|
|
62
|
+
cd VideoSpeed
|
|
63
|
+
pip install -e .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This installs the console script **`video-speed`** and pulls Python deps (`opencv-python`, `ChronicleLogger`, …).
|
|
67
|
+
|
|
68
|
+
### From a local wheel / sdist
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# after: python -m build (or ./build.sh build)
|
|
72
|
+
pip install dist/VideoSpeed-1.0.5-py3-none-any.whl
|
|
73
|
+
# or
|
|
74
|
+
pip install dist/VideoSpeed-1.0.5.tar.gz
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Verify
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
video-speed --version
|
|
81
|
+
# or
|
|
82
|
+
python -m VideoSpeed --version
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
Interactive editor (default):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
video-speed
|
|
91
|
+
# or
|
|
92
|
+
python -m VideoSpeed
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
1. Folder containing MP4s (Enter = current directory)
|
|
96
|
+
2. Choose a video by number
|
|
97
|
+
3. Cut start/end seconds
|
|
98
|
+
4. New length percent (default 100; allowed 20–200)
|
|
99
|
+
5. Optional boomerang (y/n)
|
|
100
|
+
6. Output is written **next to the source** with a descriptive name
|
|
101
|
+
|
|
102
|
+
Other:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
video-speed --help
|
|
106
|
+
video-speed --version
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Install editable, then run interactive session in a folder of clips
|
|
113
|
+
cd /path/to/videos
|
|
114
|
+
video-speed
|
|
115
|
+
|
|
116
|
+
# Check identity without starting prompts
|
|
117
|
+
video-speed --version
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Example output name shape:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
clip_cut1.0-5.0s_100pct.mp4
|
|
124
|
+
clip_cut1.0-5.0s_50pct_BOOMERANG.mp4
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Platform Compatibility
|
|
128
|
+
|
|
129
|
+
| Platform | Status |
|
|
130
|
+
|----------|--------|
|
|
131
|
+
| Linux | Primary (tested in development) |
|
|
132
|
+
| macOS / Windows | Expected to work when Python, FFmpeg, and OpenCV are available |
|
|
133
|
+
| Filesystems | Local disk and removable/USB volumes supported for output (temps staged beside final path) |
|
|
134
|
+
|
|
135
|
+
## Related Projects
|
|
136
|
+
|
|
137
|
+
- [VideoSpeed on GitHub](https://github.com/Wilgat/VideoSpeed)
|
|
138
|
+
- [VideoSpeed on PyPI](https://pypi.org/project/VideoSpeed/)
|
|
139
|
+
|
|
140
|
+
## Contributing
|
|
141
|
+
|
|
142
|
+
Fork the repository, use a checkout install (`pip install -e .`), and open a pull request. Prefer small, tested changes. Do not strip defensive temp/publish behavior (cross-filesystem `shutil.move`) without an explicit redesign.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT License — see [`LICENSE.md`](LICENSE.md).
|
|
147
|
+
|
|
148
|
+
## Last Update
|
|
149
|
+
|
|
150
|
+
2026-08-09 — README aligned with product **1.0.5**, write-readme detection-driven banners (Version / License / CIAO / Stars / Python / PyPI), and install truth (PyPI + editable + wheel).
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# VideoSpeed - Cut, speed, and boomerang MP4 clips from the CLI
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
[-purple.svg)](https://github.com/cloudgen/ciao)
|
|
6
|
+
[](https://github.com/Wilgat/VideoSpeed)
|
|
7
|
+
[]()
|
|
8
|
+
[](https://pypi.org/project/VideoSpeed/)
|
|
9
|
+
|
|
10
|
+
VideoSpeed is an interactive command-line tool for short MP4 edits: pick a folder of videos, cut a time range, change length/speed (percent), and optionally append a boomerang (forward + reverse). Encoding uses **FFmpeg**; duration probing uses **OpenCV**.
|
|
11
|
+
|
|
12
|
+
Package version SSOT: `pyproject.toml` and `src/VideoSpeed/__init__.py` (**1.0.5**). Console entrypoint: `video-speed`.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Recursive **MP4** discovery under a chosen folder
|
|
17
|
+
- Interactive **cut** (start/end seconds) with invalid-range re-prompt
|
|
18
|
+
- **Length/speed** change (**20–200%**) with A/V tempo kept in sync
|
|
19
|
+
- Optional **boomerang** (forward then reverse)
|
|
20
|
+
- **USB-safe** intermediate files (staged next to the output) and final publish via `shutil.move`
|
|
21
|
+
- **`--help`** / **`--version`**; bare invoke starts the interactive session (Type N)
|
|
22
|
+
- Fail-closed when **FFmpeg** is missing from `PATH`
|
|
23
|
+
|
|
24
|
+
## Quick Installation
|
|
25
|
+
|
|
26
|
+
### Prerequisites (system)
|
|
27
|
+
|
|
28
|
+
- **FFmpeg** on `PATH` (`ffmpeg -version`)
|
|
29
|
+
- A supported **Python** environment (see `requires-python` in `pyproject.toml`)
|
|
30
|
+
|
|
31
|
+
### From PyPI (registry channel)
|
|
32
|
+
|
|
33
|
+
Package name SSOT: **`VideoSpeed`** (`pyproject.toml` `[project].name`). Upload path: `build.sh` (`twine upload`).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install VideoSpeed
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If the index is behind a local release, install from a checkout or wheel instead (below). Live index version is shown by the PyPI badge above.
|
|
40
|
+
|
|
41
|
+
### From a git checkout (development)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone git@github.com:Wilgat/VideoSpeed.git
|
|
45
|
+
cd VideoSpeed
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This installs the console script **`video-speed`** and pulls Python deps (`opencv-python`, `ChronicleLogger`, …).
|
|
50
|
+
|
|
51
|
+
### From a local wheel / sdist
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# after: python -m build (or ./build.sh build)
|
|
55
|
+
pip install dist/VideoSpeed-1.0.5-py3-none-any.whl
|
|
56
|
+
# or
|
|
57
|
+
pip install dist/VideoSpeed-1.0.5.tar.gz
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Verify
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
video-speed --version
|
|
64
|
+
# or
|
|
65
|
+
python -m VideoSpeed --version
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
Interactive editor (default):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
video-speed
|
|
74
|
+
# or
|
|
75
|
+
python -m VideoSpeed
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
1. Folder containing MP4s (Enter = current directory)
|
|
79
|
+
2. Choose a video by number
|
|
80
|
+
3. Cut start/end seconds
|
|
81
|
+
4. New length percent (default 100; allowed 20–200)
|
|
82
|
+
5. Optional boomerang (y/n)
|
|
83
|
+
6. Output is written **next to the source** with a descriptive name
|
|
84
|
+
|
|
85
|
+
Other:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
video-speed --help
|
|
89
|
+
video-speed --version
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Install editable, then run interactive session in a folder of clips
|
|
96
|
+
cd /path/to/videos
|
|
97
|
+
video-speed
|
|
98
|
+
|
|
99
|
+
# Check identity without starting prompts
|
|
100
|
+
video-speed --version
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Example output name shape:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
clip_cut1.0-5.0s_100pct.mp4
|
|
107
|
+
clip_cut1.0-5.0s_50pct_BOOMERANG.mp4
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Platform Compatibility
|
|
111
|
+
|
|
112
|
+
| Platform | Status |
|
|
113
|
+
|----------|--------|
|
|
114
|
+
| Linux | Primary (tested in development) |
|
|
115
|
+
| macOS / Windows | Expected to work when Python, FFmpeg, and OpenCV are available |
|
|
116
|
+
| Filesystems | Local disk and removable/USB volumes supported for output (temps staged beside final path) |
|
|
117
|
+
|
|
118
|
+
## Related Projects
|
|
119
|
+
|
|
120
|
+
- [VideoSpeed on GitHub](https://github.com/Wilgat/VideoSpeed)
|
|
121
|
+
- [VideoSpeed on PyPI](https://pypi.org/project/VideoSpeed/)
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
Fork the repository, use a checkout install (`pip install -e .`), and open a pull request. Prefer small, tested changes. Do not strip defensive temp/publish behavior (cross-filesystem `shutil.move`) without an explicit redesign.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT License — see [`LICENSE.md`](LICENSE.md).
|
|
130
|
+
|
|
131
|
+
## Last Update
|
|
132
|
+
|
|
133
|
+
2026-08-09 — README aligned with product **1.0.5**, write-readme detection-driven banners (Version / License / CIAO / Stars / Python / PyPI), and install truth (PyPI + editable + wheel).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "VideoSpeed"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.5"
|
|
8
8
|
description = "A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# VideoSpeed CLI — specialized from bootstrap interactive editor (cli.bootstrap-old)
|
|
4
|
+
# Domain: cut → speed/length → optional boomerang (MP4)
|
|
5
|
+
# Law keys: requirement-domain-videospeed, requirement-video-ffmpeg-pipeline,
|
|
6
|
+
# requirement-python-cli-interface, requirement-python-error-handling,
|
|
7
|
+
# requirement-runtime-prerequisites
|
|
8
|
+
# CIAO-Lite: Caution • Intentional • Anti-fragile • Over-protect
|
|
9
|
+
# =============================================================================
|
|
10
|
+
from __future__ import print_function, unicode_literals
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# -----------------------------------------------------------------------------
|
|
21
|
+
# Version (package SSOT lives in __init__.__version__; fallback for direct run)
|
|
22
|
+
# -----------------------------------------------------------------------------
|
|
23
|
+
try:
|
|
24
|
+
from . import __version__ as _PKG_VERSION
|
|
25
|
+
except Exception:
|
|
26
|
+
_PKG_VERSION = "1.0.5"
|
|
27
|
+
|
|
28
|
+
APP_NAME = "VideoSpeed"
|
|
29
|
+
CONSOLE_NAME = "video-speed"
|
|
30
|
+
|
|
31
|
+
# Length percent bounds (domain / pipeline guidance made fail-closed)
|
|
32
|
+
RATIO_MIN = 20.0
|
|
33
|
+
RATIO_MAX = 200.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def out_info(msg):
|
|
37
|
+
"""User-facing informational line (stdout)."""
|
|
38
|
+
print(msg)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def out_err(msg):
|
|
42
|
+
"""User-facing error line (stderr)."""
|
|
43
|
+
print(msg, file=sys.stderr)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# CIAO-Lite Protection Zone — temp staging + publish (USB / multi-mount)
|
|
48
|
+
# System TMPDIR and removable media are often different filesystems.
|
|
49
|
+
# Bare os.rename / os.replace / Path.rename / Path.replace do NOT cross mounts
|
|
50
|
+
# (Linux EXDEV). Publish completed intermediates with shutil.move (rename same
|
|
51
|
+
# FS; copy+delete on EXDEV). Prefer temps next to final output when writable.
|
|
52
|
+
# Do NOT "simplify" back to bare os.replace from default /tmp onto USB paths.
|
|
53
|
+
# Law: requirement-video-ffmpeg-pipeline, requirement-python-coding-style
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def staging_dir_for(dest_path):
|
|
58
|
+
"""
|
|
59
|
+
General Purpose: Choose a directory for intermediate files on the same
|
|
60
|
+
filesystem as dest when possible (USB-safe). Falls back to system temp.
|
|
61
|
+
"""
|
|
62
|
+
dest_path = Path(dest_path)
|
|
63
|
+
parent = dest_path.parent if dest_path.suffix else dest_path
|
|
64
|
+
if not parent.is_dir():
|
|
65
|
+
parent = dest_path.parent
|
|
66
|
+
try:
|
|
67
|
+
if parent.is_dir() and os.access(str(parent), os.W_OK):
|
|
68
|
+
return parent
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
return Path(tempfile.gettempdir())
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_temp_path(suffix, near_path):
|
|
75
|
+
"""
|
|
76
|
+
General Purpose: Create a unique temp file path near near_path's directory
|
|
77
|
+
(same FS when writable). File is created empty and closed; caller overwrites.
|
|
78
|
+
"""
|
|
79
|
+
d = staging_dir_for(near_path)
|
|
80
|
+
fd, name = tempfile.mkstemp(suffix=suffix, prefix="videospeed_", dir=str(d))
|
|
81
|
+
os.close(fd)
|
|
82
|
+
return Path(name)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def promote_file(src, dest):
|
|
86
|
+
"""
|
|
87
|
+
General Purpose: Publish src → dest using shutil.move (stdlib cross-FS safe).
|
|
88
|
+
|
|
89
|
+
Same mount: rename. Different mount (USB, etc.): copy then remove source.
|
|
90
|
+
Never use bare os.replace alone when src may live under system TMPDIR.
|
|
91
|
+
"""
|
|
92
|
+
src = Path(src)
|
|
93
|
+
dest = Path(dest)
|
|
94
|
+
if not src.is_file():
|
|
95
|
+
raise FileNotFoundError("promote source missing: {}".format(src))
|
|
96
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
# shutil.move: try rename; on EXDEV copy + unlink source
|
|
98
|
+
shutil.move(str(src), str(dest))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def ensure_ffmpeg():
|
|
102
|
+
"""
|
|
103
|
+
General Purpose: Fail closed if the system ffmpeg binary is not on PATH.
|
|
104
|
+
requirement-runtime-prerequisites / requirement-video-ffmpeg-pipeline
|
|
105
|
+
"""
|
|
106
|
+
if shutil.which("ffmpeg") is None:
|
|
107
|
+
out_err("ERROR: ffmpeg not found on PATH.")
|
|
108
|
+
out_err(" Install FFmpeg and ensure `ffmpeg` is available.")
|
|
109
|
+
out_err(" → https://ffmpeg.org/download.html")
|
|
110
|
+
return False
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_ffmpeg(cmd):
|
|
115
|
+
"""
|
|
116
|
+
General Purpose: Run one FFmpeg command; fail closed on non-zero exit.
|
|
117
|
+
Does not modify the user's source media path.
|
|
118
|
+
"""
|
|
119
|
+
out_info(" Running → {}".format(" ".join(str(c) for c in cmd)))
|
|
120
|
+
try:
|
|
121
|
+
subprocess.run(cmd, check=True)
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
out_err("ERROR: ffmpeg not found while running a encode step.")
|
|
124
|
+
raise
|
|
125
|
+
except subprocess.CalledProcessError as exc:
|
|
126
|
+
out_err("ERROR: ffmpeg failed (exit {}).".format(exc.returncode))
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_mp4_files(folder="."):
|
|
131
|
+
"""General Purpose: Recursively list MP4 files under folder (case variants)."""
|
|
132
|
+
folder = Path(folder)
|
|
133
|
+
if not folder.is_dir():
|
|
134
|
+
return []
|
|
135
|
+
files = list(folder.rglob("*.mp4")) + list(folder.rglob("*.MP4"))
|
|
136
|
+
files = [f for f in files if f.is_file()]
|
|
137
|
+
# de-dupe same path if filesystem is case-insensitive
|
|
138
|
+
seen = set()
|
|
139
|
+
unique = []
|
|
140
|
+
for f in files:
|
|
141
|
+
key = str(f.resolve()) if f.exists() else str(f)
|
|
142
|
+
if key not in seen:
|
|
143
|
+
seen.add(key)
|
|
144
|
+
unique.append(f)
|
|
145
|
+
unique.sort(key=lambda x: x.name.lower())
|
|
146
|
+
return unique
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_duration_cv2(video_path):
|
|
150
|
+
"""
|
|
151
|
+
General Purpose: Probe duration via OpenCV (lazy import).
|
|
152
|
+
Returns seconds; 0.0 if unreadable.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
import cv2
|
|
156
|
+
except ImportError:
|
|
157
|
+
out_err("ERROR: OpenCV (cv2) is not installed.")
|
|
158
|
+
out_err(" Install with: pip install opencv-python")
|
|
159
|
+
return 0.0
|
|
160
|
+
|
|
161
|
+
cap = cv2.VideoCapture(str(video_path))
|
|
162
|
+
if not cap.isOpened():
|
|
163
|
+
cap.release()
|
|
164
|
+
return 0.0
|
|
165
|
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
166
|
+
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
167
|
+
cap.release()
|
|
168
|
+
if fps is None or fps <= 0 or frame_count <= 0:
|
|
169
|
+
return 0.0
|
|
170
|
+
return float(frame_count) / float(fps)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def format_time(seconds):
|
|
174
|
+
"""General Purpose: Format seconds as H:MM:SS.mmm or M:SS.mmm."""
|
|
175
|
+
h = int(seconds // 3600)
|
|
176
|
+
m = int((seconds % 3600) // 60)
|
|
177
|
+
s = seconds % 60
|
|
178
|
+
if h == 0:
|
|
179
|
+
return "{:02d}:{:06.3f}".format(m, s)
|
|
180
|
+
return "{:02d}:{:02d}:{:06.3f}".format(h, m, s)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def cut_clip(src, start, end, output_path):
|
|
184
|
+
"""
|
|
185
|
+
General Purpose: Extract [start, end] segment to output_path via FFmpeg.
|
|
186
|
+
Source file is read-only input.
|
|
187
|
+
"""
|
|
188
|
+
cmd = [
|
|
189
|
+
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
|
|
190
|
+
"-i", str(src),
|
|
191
|
+
"-vf", "trim=start={}:end={},setpts=PTS-STARTPTS".format(start, end),
|
|
192
|
+
"-af", "atrim=start={}:end={},asetpts=PTS-STARTPTS".format(start, end),
|
|
193
|
+
"-c:v", "libx264", "-preset", "ultrafast", "-crf", "17",
|
|
194
|
+
"-c:a", "aac", "-avoid_negative_ts", "make_zero",
|
|
195
|
+
str(output_path),
|
|
196
|
+
]
|
|
197
|
+
run_ffmpeg(cmd)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _atempo_filters(speed):
|
|
201
|
+
"""
|
|
202
|
+
General Purpose: Build atempo chain. FFmpeg single atempo accepts 0.5–100.
|
|
203
|
+
speed = playback rate ( >1 faster, shorter wall time for same content).
|
|
204
|
+
"""
|
|
205
|
+
filters = []
|
|
206
|
+
remaining = float(speed)
|
|
207
|
+
# Speed up: atempo max 100; slow down: min 0.5 — chain when needed
|
|
208
|
+
if remaining <= 0:
|
|
209
|
+
raise ValueError("speed must be positive")
|
|
210
|
+
while remaining > 100.0:
|
|
211
|
+
filters.append("atempo=100.0")
|
|
212
|
+
remaining /= 100.0
|
|
213
|
+
while remaining < 0.5:
|
|
214
|
+
filters.append("atempo=0.5")
|
|
215
|
+
remaining /= 0.5
|
|
216
|
+
filters.append("atempo={:.6f}".format(remaining))
|
|
217
|
+
return ",".join(filters)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def speed_change(input_path, ratio_percent, output_path):
|
|
221
|
+
"""
|
|
222
|
+
General Purpose: Change clip length to ratio_percent of original cut length.
|
|
223
|
+
100 = unchanged duration intent; 50 ≈ half length (faster); 200 ≈ double length.
|
|
224
|
+
"""
|
|
225
|
+
speed = 100.0 / float(ratio_percent)
|
|
226
|
+
atempo = _atempo_filters(speed)
|
|
227
|
+
cmd = [
|
|
228
|
+
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
|
|
229
|
+
"-i", str(input_path),
|
|
230
|
+
"-filter_complex",
|
|
231
|
+
"[0:v]setpts={:.6f}*PTS[v];[0:a]{}[a]".format(1.0 / speed, atempo),
|
|
232
|
+
"-map", "[v]", "-map", "[a]",
|
|
233
|
+
"-c:v", "libx264", "-preset", "medium", "-crf", "18",
|
|
234
|
+
"-c:a", "aac", "-b:a", "192k",
|
|
235
|
+
"-movflags", "+faststart",
|
|
236
|
+
str(output_path),
|
|
237
|
+
]
|
|
238
|
+
run_ffmpeg(cmd)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def add_boomerang(forward_clip, final_output):
|
|
242
|
+
"""
|
|
243
|
+
General Purpose: Concat forward + reverse of forward_clip into final_output.
|
|
244
|
+
Intermediate reverse/list files are staged near final_output (USB-safe).
|
|
245
|
+
Cleans reverse and concat-list temps on all paths.
|
|
246
|
+
"""
|
|
247
|
+
reverse_clip = None
|
|
248
|
+
concat_list = None
|
|
249
|
+
try:
|
|
250
|
+
# Stage reverse next to final output so reverse+concat stay on one mount.
|
|
251
|
+
reverse_clip = make_temp_path(".mp4", final_output)
|
|
252
|
+
|
|
253
|
+
run_ffmpeg([
|
|
254
|
+
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
|
|
255
|
+
"-i", str(forward_clip),
|
|
256
|
+
"-vf", "reverse", "-af", "areverse",
|
|
257
|
+
str(reverse_clip),
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
# Concat list file also near dest FS; paths absolute for demuxer.
|
|
261
|
+
concat_list = make_temp_path(".txt", final_output)
|
|
262
|
+
with open(str(concat_list), "w", encoding="utf-8") as f:
|
|
263
|
+
f.write("file '{}'\n".format(Path(forward_clip).resolve().as_posix()))
|
|
264
|
+
f.write("file '{}'\n".format(Path(reverse_clip).resolve().as_posix()))
|
|
265
|
+
|
|
266
|
+
run_ffmpeg([
|
|
267
|
+
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
|
|
268
|
+
"-f", "concat", "-safe", "0",
|
|
269
|
+
"-i", str(concat_list),
|
|
270
|
+
"-c", "copy",
|
|
271
|
+
str(final_output),
|
|
272
|
+
])
|
|
273
|
+
finally:
|
|
274
|
+
for p in (reverse_clip, concat_list):
|
|
275
|
+
if p is not None and Path(p).exists():
|
|
276
|
+
try:
|
|
277
|
+
os.unlink(p)
|
|
278
|
+
except OSError:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def choose_index(prompt, count):
|
|
283
|
+
"""General Purpose: Prompt for 1-based index in [1, count]; re-prompt on error."""
|
|
284
|
+
while True:
|
|
285
|
+
raw = input(prompt).strip()
|
|
286
|
+
try:
|
|
287
|
+
idx = int(raw)
|
|
288
|
+
except ValueError:
|
|
289
|
+
out_info(" → Please type a number")
|
|
290
|
+
continue
|
|
291
|
+
if 1 <= idx <= count:
|
|
292
|
+
return idx - 1
|
|
293
|
+
out_info(" → Enter 1–{}".format(count))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def prompt_float(prompt, default=None):
|
|
297
|
+
"""General Purpose: Prompt for float; empty uses default if provided."""
|
|
298
|
+
while True:
|
|
299
|
+
raw = input(prompt).strip()
|
|
300
|
+
if raw == "" and default is not None:
|
|
301
|
+
return float(default)
|
|
302
|
+
try:
|
|
303
|
+
return float(raw.replace("%", ""))
|
|
304
|
+
except ValueError:
|
|
305
|
+
out_info(" → Please enter a number")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def prompt_yes_no(prompt, default_no=True):
|
|
309
|
+
"""General Purpose: y/n prompt; empty → default."""
|
|
310
|
+
raw = input(prompt).strip().lower()
|
|
311
|
+
if raw == "":
|
|
312
|
+
return not default_no
|
|
313
|
+
return raw in ("y", "yes")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def build_output_name(video_path, start, end, ratio, boomerang):
|
|
317
|
+
"""General Purpose: Domain output naming pattern beside source."""
|
|
318
|
+
name = "{}_cut{:.1f}-{:.1f}s_{:d}pct".format(
|
|
319
|
+
video_path.stem, start, end, int(ratio)
|
|
320
|
+
)
|
|
321
|
+
if boomerang:
|
|
322
|
+
name += "_BOOMERANG"
|
|
323
|
+
name += ".mp4"
|
|
324
|
+
return video_path.parent / name
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def process_job(video_path, start, end, ratio, boomerang):
|
|
328
|
+
"""
|
|
329
|
+
General Purpose: Run cut → speed → optional boomerang pipeline.
|
|
330
|
+
Never overwrites source; cleans intermediate temps.
|
|
331
|
+
Temps are staged on the destination filesystem when possible so USB /
|
|
332
|
+
removable media does not hit cross-device os.replace failures.
|
|
333
|
+
Returns final Path or None on failure.
|
|
334
|
+
"""
|
|
335
|
+
final_out = build_output_name(video_path, start, end, ratio, boomerang)
|
|
336
|
+
out_info("\nProcessing → {}\n".format(final_out))
|
|
337
|
+
out_info(" Staging dir → {}".format(staging_dir_for(final_out)))
|
|
338
|
+
|
|
339
|
+
cut_temp = None
|
|
340
|
+
speed_temp = None
|
|
341
|
+
try:
|
|
342
|
+
# Same-FS temps next to final_out (e.g. on the USB volume).
|
|
343
|
+
cut_temp = make_temp_path(".mp4", final_out)
|
|
344
|
+
speed_temp = make_temp_path(".mp4", final_out)
|
|
345
|
+
|
|
346
|
+
out_info("1. Cutting segment...")
|
|
347
|
+
cut_clip(video_path, start, end, cut_temp)
|
|
348
|
+
out_info("2. Changing length to {}%...".format(int(ratio) if ratio == int(ratio) else ratio))
|
|
349
|
+
speed_change(cut_temp, ratio, speed_temp)
|
|
350
|
+
if boomerang:
|
|
351
|
+
out_info("3. Creating boomerang...")
|
|
352
|
+
add_boomerang(speed_temp, final_out)
|
|
353
|
+
else:
|
|
354
|
+
# Publish with shutil.move (same-FS rename or cross-FS copy+delete).
|
|
355
|
+
promote_file(speed_temp, final_out)
|
|
356
|
+
speed_temp = None # moved away
|
|
357
|
+
out_info("\nSUCCESS → {}\n".format(final_out))
|
|
358
|
+
return final_out
|
|
359
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError, ValueError) as exc:
|
|
360
|
+
err = getattr(exc, "errno", None)
|
|
361
|
+
out_err("Job failed: {}".format(exc))
|
|
362
|
+
if err in (getattr(os, "EXDEV", 18), 18):
|
|
363
|
+
out_err(
|
|
364
|
+
" Hint: cross-filesystem rename failed (USB vs system temp). "
|
|
365
|
+
"Temps should stage next to the output; publish uses shutil.move. "
|
|
366
|
+
"Check USB write permission and free space."
|
|
367
|
+
)
|
|
368
|
+
return None
|
|
369
|
+
finally:
|
|
370
|
+
for p in (cut_temp, speed_temp):
|
|
371
|
+
if p is not None and Path(p).exists():
|
|
372
|
+
try:
|
|
373
|
+
os.unlink(p)
|
|
374
|
+
except OSError:
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def interactive_session():
|
|
379
|
+
"""
|
|
380
|
+
General Purpose: Type N interactive domain session (empty argv default).
|
|
381
|
+
requirement-domain-videospeed steps D-01..D-08
|
|
382
|
+
"""
|
|
383
|
+
out_info("{} v{} — Cut → Speed → Optional Boomerang".format(APP_NAME, _PKG_VERSION))
|
|
384
|
+
out_info("=" * 62)
|
|
385
|
+
|
|
386
|
+
if not ensure_ffmpeg():
|
|
387
|
+
return 1
|
|
388
|
+
|
|
389
|
+
folder = input("Folder (Enter = current): ").strip() or "."
|
|
390
|
+
folder_path = Path(folder)
|
|
391
|
+
if not folder_path.is_dir():
|
|
392
|
+
out_err("ERROR: Not a directory: {}".format(folder))
|
|
393
|
+
return 1
|
|
394
|
+
|
|
395
|
+
files = get_mp4_files(folder_path)
|
|
396
|
+
if not files:
|
|
397
|
+
out_err("No MP4 files found!")
|
|
398
|
+
return 1
|
|
399
|
+
|
|
400
|
+
for i, f in enumerate(files, 1):
|
|
401
|
+
out_info("{:2d}. {}".format(i, f.name))
|
|
402
|
+
|
|
403
|
+
sel = choose_index("\nChoose video (1–{}): ".format(len(files)), len(files))
|
|
404
|
+
video_path = files[sel]
|
|
405
|
+
|
|
406
|
+
duration = get_duration_cv2(video_path)
|
|
407
|
+
if duration <= 0:
|
|
408
|
+
out_err("ERROR: Could not read duration for {}".format(video_path.name))
|
|
409
|
+
return 1
|
|
410
|
+
|
|
411
|
+
out_info("\nSelected: {}".format(video_path.name))
|
|
412
|
+
out_info("Duration: {} ({:.3f}s)\n".format(format_time(duration), duration))
|
|
413
|
+
|
|
414
|
+
while True:
|
|
415
|
+
out_info("Step 1/4 – Cut segment")
|
|
416
|
+
start = prompt_float(" Start seconds (default 0.0): ", default=0.0)
|
|
417
|
+
out_info(" → Using start: {:.3f}s".format(start))
|
|
418
|
+
end = prompt_float(
|
|
419
|
+
" End seconds [{:.3f}]: ".format(duration),
|
|
420
|
+
default=duration,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if not (0 <= start < end <= duration):
|
|
424
|
+
out_info("Invalid range! Need 0 ≤ start < end ≤ duration.\n")
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
out_info("\nStep 2/4 – Resize length ({}–{}%)".format(int(RATIO_MIN), int(RATIO_MAX)))
|
|
428
|
+
ratio = prompt_float(" New length % [100%]: ", default=100.0)
|
|
429
|
+
if not (RATIO_MIN <= ratio <= RATIO_MAX):
|
|
430
|
+
out_info(
|
|
431
|
+
"Invalid percent! Use {}–{} (got {}).\n".format(
|
|
432
|
+
int(RATIO_MIN), int(RATIO_MAX), ratio
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
out_info("\nStep 3/4 – Add boomerang effect?")
|
|
438
|
+
boomerang = prompt_yes_no(
|
|
439
|
+
" Make it go forward + backward (y/n) [n]: ",
|
|
440
|
+
default_no=True,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
process_job(video_path, start, end, ratio, boomerang)
|
|
444
|
+
|
|
445
|
+
if not prompt_yes_no("Again? (y/n): ", default_no=True):
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
out_info("Done.")
|
|
449
|
+
return 0
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def build_parser():
|
|
453
|
+
"""General Purpose: Optional flags; bare invoke remains interactive (Type N)."""
|
|
454
|
+
p = argparse.ArgumentParser(
|
|
455
|
+
prog=CONSOLE_NAME,
|
|
456
|
+
description=(
|
|
457
|
+
"{} — interactive MP4 cut, length/speed change, optional boomerang."
|
|
458
|
+
.format(APP_NAME)
|
|
459
|
+
),
|
|
460
|
+
)
|
|
461
|
+
p.add_argument(
|
|
462
|
+
"--version",
|
|
463
|
+
action="version",
|
|
464
|
+
version="{} {}".format(APP_NAME, _PKG_VERSION),
|
|
465
|
+
)
|
|
466
|
+
return p
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def main(argv=None):
|
|
470
|
+
"""
|
|
471
|
+
General Purpose: Package entry for video-speed / python -m VideoSpeed.
|
|
472
|
+
Empty argv → interactive domain session (Type N).
|
|
473
|
+
"""
|
|
474
|
+
if argv is None:
|
|
475
|
+
argv = sys.argv[1:]
|
|
476
|
+
|
|
477
|
+
# Allow --help / --version without starting prompts
|
|
478
|
+
if argv:
|
|
479
|
+
parser = build_parser()
|
|
480
|
+
parser.parse_args(argv)
|
|
481
|
+
# No non-interactive job flags yet → if user passed only unknown, argparse exits
|
|
482
|
+
# If parse succeeds with no action flags, fall through to interactive
|
|
483
|
+
# (version/help already exited via argparse)
|
|
484
|
+
|
|
485
|
+
return interactive_session()
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
if __name__ == "__main__":
|
|
489
|
+
sys.exit(main() or 0)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: VideoSpeed
|
|
3
|
+
Version: 1.0.5
|
|
4
|
+
Summary: A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos.
|
|
5
|
+
Author-email: Wilgat Wong <wilgat.wong@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Wilgat/VideoSpeed
|
|
8
|
+
Project-URL: Repository, https://github.com/Wilgat/VideoSpeed
|
|
9
|
+
Project-URL: Issues, https://github.com/Wilgat/VideoSpeed/issues
|
|
10
|
+
Keywords: Video Editing,Multimeida
|
|
11
|
+
Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE.md
|
|
14
|
+
Requires-Dist: ChronicleLogger>=1.2.3
|
|
15
|
+
Requires-Dist: opencv-python
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# VideoSpeed - Cut, speed, and boomerang MP4 clips from the CLI
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+

|
|
22
|
+
[-purple.svg)](https://github.com/cloudgen/ciao)
|
|
23
|
+
[](https://github.com/Wilgat/VideoSpeed)
|
|
24
|
+
[]()
|
|
25
|
+
[](https://pypi.org/project/VideoSpeed/)
|
|
26
|
+
|
|
27
|
+
VideoSpeed is an interactive command-line tool for short MP4 edits: pick a folder of videos, cut a time range, change length/speed (percent), and optionally append a boomerang (forward + reverse). Encoding uses **FFmpeg**; duration probing uses **OpenCV**.
|
|
28
|
+
|
|
29
|
+
Package version SSOT: `pyproject.toml` and `src/VideoSpeed/__init__.py` (**1.0.5**). Console entrypoint: `video-speed`.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Recursive **MP4** discovery under a chosen folder
|
|
34
|
+
- Interactive **cut** (start/end seconds) with invalid-range re-prompt
|
|
35
|
+
- **Length/speed** change (**20–200%**) with A/V tempo kept in sync
|
|
36
|
+
- Optional **boomerang** (forward then reverse)
|
|
37
|
+
- **USB-safe** intermediate files (staged next to the output) and final publish via `shutil.move`
|
|
38
|
+
- **`--help`** / **`--version`**; bare invoke starts the interactive session (Type N)
|
|
39
|
+
- Fail-closed when **FFmpeg** is missing from `PATH`
|
|
40
|
+
|
|
41
|
+
## Quick Installation
|
|
42
|
+
|
|
43
|
+
### Prerequisites (system)
|
|
44
|
+
|
|
45
|
+
- **FFmpeg** on `PATH` (`ffmpeg -version`)
|
|
46
|
+
- A supported **Python** environment (see `requires-python` in `pyproject.toml`)
|
|
47
|
+
|
|
48
|
+
### From PyPI (registry channel)
|
|
49
|
+
|
|
50
|
+
Package name SSOT: **`VideoSpeed`** (`pyproject.toml` `[project].name`). Upload path: `build.sh` (`twine upload`).
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install VideoSpeed
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If the index is behind a local release, install from a checkout or wheel instead (below). Live index version is shown by the PyPI badge above.
|
|
57
|
+
|
|
58
|
+
### From a git checkout (development)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone git@github.com:Wilgat/VideoSpeed.git
|
|
62
|
+
cd VideoSpeed
|
|
63
|
+
pip install -e .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This installs the console script **`video-speed`** and pulls Python deps (`opencv-python`, `ChronicleLogger`, …).
|
|
67
|
+
|
|
68
|
+
### From a local wheel / sdist
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# after: python -m build (or ./build.sh build)
|
|
72
|
+
pip install dist/VideoSpeed-1.0.5-py3-none-any.whl
|
|
73
|
+
# or
|
|
74
|
+
pip install dist/VideoSpeed-1.0.5.tar.gz
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Verify
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
video-speed --version
|
|
81
|
+
# or
|
|
82
|
+
python -m VideoSpeed --version
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
Interactive editor (default):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
video-speed
|
|
91
|
+
# or
|
|
92
|
+
python -m VideoSpeed
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
1. Folder containing MP4s (Enter = current directory)
|
|
96
|
+
2. Choose a video by number
|
|
97
|
+
3. Cut start/end seconds
|
|
98
|
+
4. New length percent (default 100; allowed 20–200)
|
|
99
|
+
5. Optional boomerang (y/n)
|
|
100
|
+
6. Output is written **next to the source** with a descriptive name
|
|
101
|
+
|
|
102
|
+
Other:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
video-speed --help
|
|
106
|
+
video-speed --version
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Install editable, then run interactive session in a folder of clips
|
|
113
|
+
cd /path/to/videos
|
|
114
|
+
video-speed
|
|
115
|
+
|
|
116
|
+
# Check identity without starting prompts
|
|
117
|
+
video-speed --version
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Example output name shape:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
clip_cut1.0-5.0s_100pct.mp4
|
|
124
|
+
clip_cut1.0-5.0s_50pct_BOOMERANG.mp4
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Platform Compatibility
|
|
128
|
+
|
|
129
|
+
| Platform | Status |
|
|
130
|
+
|----------|--------|
|
|
131
|
+
| Linux | Primary (tested in development) |
|
|
132
|
+
| macOS / Windows | Expected to work when Python, FFmpeg, and OpenCV are available |
|
|
133
|
+
| Filesystems | Local disk and removable/USB volumes supported for output (temps staged beside final path) |
|
|
134
|
+
|
|
135
|
+
## Related Projects
|
|
136
|
+
|
|
137
|
+
- [VideoSpeed on GitHub](https://github.com/Wilgat/VideoSpeed)
|
|
138
|
+
- [VideoSpeed on PyPI](https://pypi.org/project/VideoSpeed/)
|
|
139
|
+
|
|
140
|
+
## Contributing
|
|
141
|
+
|
|
142
|
+
Fork the repository, use a checkout install (`pip install -e .`), and open a pull request. Prefer small, tested changes. Do not strip defensive temp/publish behavior (cross-filesystem `shutil.move`) without an explicit redesign.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT License — see [`LICENSE.md`](LICENSE.md).
|
|
147
|
+
|
|
148
|
+
## Last Update
|
|
149
|
+
|
|
150
|
+
2026-08-09 — README aligned with product **1.0.5**, write-readme detection-driven banners (Version / License / CIAO / Stars / Python / PyPI), and install truth (PyPI + editable + wheel).
|
videospeed-1.0.4/PKG-INFO
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: VideoSpeed
|
|
3
|
-
Version: 1.0.4
|
|
4
|
-
Summary: A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos.
|
|
5
|
-
Author-email: Wilgat Wong <wilgat.wong@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/Wilgat/VideoSpeed
|
|
8
|
-
Project-URL: Repository, https://github.com/Wilgat/VideoSpeed
|
|
9
|
-
Project-URL: Issues, https://github.com/Wilgat/VideoSpeed/issues
|
|
10
|
-
Keywords: Video Editing,Multimeida
|
|
11
|
-
Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: ChronicleLogger>=1.2.3
|
|
14
|
-
Requires-Dist: opencv-python
|
|
15
|
-
|
|
16
|
-
# VideoSpeed
|
|
17
|
-
|
|
18
|
-
A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos. This project leverages FFmpeg for video processing and OpenCV to handle video file properties.
|
|
19
|
-
|
|
20
|
-
## Features
|
|
21
|
-
|
|
22
|
-
- **Cut segments** from MP4 videos.
|
|
23
|
-
- Change the **speed** of video clips.
|
|
24
|
-
- Apply a **boomerang effect** by reversing video segments.
|
|
25
|
-
- Simple command-line interface for user interaction.
|
|
26
|
-
|
|
27
|
-
## Installation
|
|
28
|
-
|
|
29
|
-
To install the VideoSpeed package, use the provided distributions:
|
|
30
|
-
|
|
31
|
-
1. Download the wheel or tar.gz file from the `dist` directory:
|
|
32
|
-
- `VideoSpeed-1.0.4-py3-none-any.whl`
|
|
33
|
-
- `VideoSpeed-1.0.4.tar.gz`
|
|
34
|
-
|
|
35
|
-
2. Install via pip:
|
|
36
|
-
```bash
|
|
37
|
-
pip install dist/VideoSpeed-1.0.4-py3-none-any.whl
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Or
|
|
41
|
-
```bash
|
|
42
|
-
pip install dist/VideoSpeed-1.0.4.tar.gz
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Usage
|
|
46
|
-
|
|
47
|
-
You can run the tool directly from the command line:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
python -m VideoSpeed
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Steps:
|
|
54
|
-
1. Select the folder containing your MP4 files (default is the current directory).
|
|
55
|
-
2. Choose the video file from the listed options.
|
|
56
|
-
3. Specify the start and end time for the cut.
|
|
57
|
-
4. Enter the desired speed percentage for the clip.
|
|
58
|
-
5. Optionally, choose to add a boomerang effect.
|
|
59
|
-
6. The processed video will be saved in the same directory as the source file.
|
|
60
|
-
|
|
61
|
-
## Folder Structure
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
VideoSpeed
|
|
65
|
-
├── build/ # Contains build artifacts
|
|
66
|
-
├── dist/ # Distributions for installation
|
|
67
|
-
├── docs/ # Documentation files
|
|
68
|
-
│ ├── CHANGELOG.md # Changes in each version
|
|
69
|
-
│ ├── folder-structure.md# Describes folder structure
|
|
70
|
-
│ └── LoggedExample-spec.md # Specifications for examples
|
|
71
|
-
├── src/ # Source code directory
|
|
72
|
-
│ ├── VideoSpeed/ # Main package
|
|
73
|
-
│ ├── setup.py # Installation script
|
|
74
|
-
└── README.md # Project documentation
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Requirements
|
|
78
|
-
|
|
79
|
-
- Python 3.6 or higher
|
|
80
|
-
- FFmpeg installed on your system
|
|
81
|
-
- OpenCV library (`opencv-python`)
|
|
82
|
-
|
|
83
|
-
You can install OpenCV using pip:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
pip install opencv-python
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Contributing
|
|
90
|
-
|
|
91
|
-
Contributions are welcome! Please fork the repository and submit a pull request.
|
|
92
|
-
|
|
93
|
-
## License
|
|
94
|
-
|
|
95
|
-
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
96
|
-
|
|
97
|
-
## Acknowledgements
|
|
98
|
-
|
|
99
|
-
- **FFmpeg**: A powerful multimedia framework for handling video files.
|
|
100
|
-
- **OpenCV**: A library for computer vision tasks.
|
videospeed-1.0.4/README.md
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# VideoSpeed
|
|
2
|
-
|
|
3
|
-
A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos. This project leverages FFmpeg for video processing and OpenCV to handle video file properties.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Cut segments** from MP4 videos.
|
|
8
|
-
- Change the **speed** of video clips.
|
|
9
|
-
- Apply a **boomerang effect** by reversing video segments.
|
|
10
|
-
- Simple command-line interface for user interaction.
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
To install the VideoSpeed package, use the provided distributions:
|
|
15
|
-
|
|
16
|
-
1. Download the wheel or tar.gz file from the `dist` directory:
|
|
17
|
-
- `VideoSpeed-1.0.4-py3-none-any.whl`
|
|
18
|
-
- `VideoSpeed-1.0.4.tar.gz`
|
|
19
|
-
|
|
20
|
-
2. Install via pip:
|
|
21
|
-
```bash
|
|
22
|
-
pip install dist/VideoSpeed-1.0.4-py3-none-any.whl
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Or
|
|
26
|
-
```bash
|
|
27
|
-
pip install dist/VideoSpeed-1.0.4.tar.gz
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Usage
|
|
31
|
-
|
|
32
|
-
You can run the tool directly from the command line:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
python -m VideoSpeed
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Steps:
|
|
39
|
-
1. Select the folder containing your MP4 files (default is the current directory).
|
|
40
|
-
2. Choose the video file from the listed options.
|
|
41
|
-
3. Specify the start and end time for the cut.
|
|
42
|
-
4. Enter the desired speed percentage for the clip.
|
|
43
|
-
5. Optionally, choose to add a boomerang effect.
|
|
44
|
-
6. The processed video will be saved in the same directory as the source file.
|
|
45
|
-
|
|
46
|
-
## Folder Structure
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
VideoSpeed
|
|
50
|
-
├── build/ # Contains build artifacts
|
|
51
|
-
├── dist/ # Distributions for installation
|
|
52
|
-
├── docs/ # Documentation files
|
|
53
|
-
│ ├── CHANGELOG.md # Changes in each version
|
|
54
|
-
│ ├── folder-structure.md# Describes folder structure
|
|
55
|
-
│ └── LoggedExample-spec.md # Specifications for examples
|
|
56
|
-
├── src/ # Source code directory
|
|
57
|
-
│ ├── VideoSpeed/ # Main package
|
|
58
|
-
│ ├── setup.py # Installation script
|
|
59
|
-
└── README.md # Project documentation
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Requirements
|
|
63
|
-
|
|
64
|
-
- Python 3.6 or higher
|
|
65
|
-
- FFmpeg installed on your system
|
|
66
|
-
- OpenCV library (`opencv-python`)
|
|
67
|
-
|
|
68
|
-
You can install OpenCV using pip:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
pip install opencv-python
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Contributing
|
|
75
|
-
|
|
76
|
-
Contributions are welcome! Please fork the repository and submit a pull request.
|
|
77
|
-
|
|
78
|
-
## License
|
|
79
|
-
|
|
80
|
-
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
81
|
-
|
|
82
|
-
## Acknowledgements
|
|
83
|
-
|
|
84
|
-
- **FFmpeg**: A powerful multimedia framework for handling video files.
|
|
85
|
-
- **OpenCV**: A library for computer vision tasks.
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: VideoSpeed
|
|
3
|
-
Version: 1.0.4
|
|
4
|
-
Summary: A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos.
|
|
5
|
-
Author-email: Wilgat Wong <wilgat.wong@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/Wilgat/VideoSpeed
|
|
8
|
-
Project-URL: Repository, https://github.com/Wilgat/VideoSpeed
|
|
9
|
-
Project-URL: Issues, https://github.com/Wilgat/VideoSpeed/issues
|
|
10
|
-
Keywords: Video Editing,Multimeida
|
|
11
|
-
Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: ChronicleLogger>=1.2.3
|
|
14
|
-
Requires-Dist: opencv-python
|
|
15
|
-
|
|
16
|
-
# VideoSpeed
|
|
17
|
-
|
|
18
|
-
A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos. This project leverages FFmpeg for video processing and OpenCV to handle video file properties.
|
|
19
|
-
|
|
20
|
-
## Features
|
|
21
|
-
|
|
22
|
-
- **Cut segments** from MP4 videos.
|
|
23
|
-
- Change the **speed** of video clips.
|
|
24
|
-
- Apply a **boomerang effect** by reversing video segments.
|
|
25
|
-
- Simple command-line interface for user interaction.
|
|
26
|
-
|
|
27
|
-
## Installation
|
|
28
|
-
|
|
29
|
-
To install the VideoSpeed package, use the provided distributions:
|
|
30
|
-
|
|
31
|
-
1. Download the wheel or tar.gz file from the `dist` directory:
|
|
32
|
-
- `VideoSpeed-1.0.4-py3-none-any.whl`
|
|
33
|
-
- `VideoSpeed-1.0.4.tar.gz`
|
|
34
|
-
|
|
35
|
-
2. Install via pip:
|
|
36
|
-
```bash
|
|
37
|
-
pip install dist/VideoSpeed-1.0.4-py3-none-any.whl
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Or
|
|
41
|
-
```bash
|
|
42
|
-
pip install dist/VideoSpeed-1.0.4.tar.gz
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Usage
|
|
46
|
-
|
|
47
|
-
You can run the tool directly from the command line:
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
python -m VideoSpeed
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Steps:
|
|
54
|
-
1. Select the folder containing your MP4 files (default is the current directory).
|
|
55
|
-
2. Choose the video file from the listed options.
|
|
56
|
-
3. Specify the start and end time for the cut.
|
|
57
|
-
4. Enter the desired speed percentage for the clip.
|
|
58
|
-
5. Optionally, choose to add a boomerang effect.
|
|
59
|
-
6. The processed video will be saved in the same directory as the source file.
|
|
60
|
-
|
|
61
|
-
## Folder Structure
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
VideoSpeed
|
|
65
|
-
├── build/ # Contains build artifacts
|
|
66
|
-
├── dist/ # Distributions for installation
|
|
67
|
-
├── docs/ # Documentation files
|
|
68
|
-
│ ├── CHANGELOG.md # Changes in each version
|
|
69
|
-
│ ├── folder-structure.md# Describes folder structure
|
|
70
|
-
│ └── LoggedExample-spec.md # Specifications for examples
|
|
71
|
-
├── src/ # Source code directory
|
|
72
|
-
│ ├── VideoSpeed/ # Main package
|
|
73
|
-
│ ├── setup.py # Installation script
|
|
74
|
-
└── README.md # Project documentation
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Requirements
|
|
78
|
-
|
|
79
|
-
- Python 3.6 or higher
|
|
80
|
-
- FFmpeg installed on your system
|
|
81
|
-
- OpenCV library (`opencv-python`)
|
|
82
|
-
|
|
83
|
-
You can install OpenCV using pip:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
pip install opencv-python
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Contributing
|
|
90
|
-
|
|
91
|
-
Contributions are welcome! Please fork the repository and submit a pull request.
|
|
92
|
-
|
|
93
|
-
## License
|
|
94
|
-
|
|
95
|
-
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
96
|
-
|
|
97
|
-
## Acknowledgements
|
|
98
|
-
|
|
99
|
-
- **FFmpeg**: A powerful multimedia framework for handling video files.
|
|
100
|
-
- **OpenCV**: A library for computer vision tasks.
|
videospeed-1.0.4/src/setup.py
DELETED
|
File without changes
|
|
File without changes
|
/videospeed-1.0.4/src/VideoSpeed/cli.py → /videospeed-1.0.5/src/VideoSpeed/cli.bootstrap-old.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|