airclip 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.
- airclip-0.1.0/PKG-INFO +159 -0
- airclip-0.1.0/README.md +138 -0
- airclip-0.1.0/airclip/__init__.py +1 -0
- airclip-0.1.0/airclip/__main__.py +3 -0
- airclip-0.1.0/airclip/airclip.py +200 -0
- airclip-0.1.0/airclip.egg-info/PKG-INFO +159 -0
- airclip-0.1.0/airclip.egg-info/SOURCES.txt +11 -0
- airclip-0.1.0/airclip.egg-info/dependency_links.txt +1 -0
- airclip-0.1.0/airclip.egg-info/entry_points.txt +2 -0
- airclip-0.1.0/airclip.egg-info/requires.txt +3 -0
- airclip-0.1.0/airclip.egg-info/top_level.txt +1 -0
- airclip-0.1.0/pyproject.toml +36 -0
- airclip-0.1.0/setup.cfg +4 -0
airclip-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: airclip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert any video to an ultra-lightweight WebM that blends seamlessly into web pages.
|
|
5
|
+
Author: Akash Chekka
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/akashchekka/airclip
|
|
8
|
+
Project-URL: Repository, https://github.com/akashchekka/airclip
|
|
9
|
+
Keywords: video,webm,vp9,compression,web,ffmpeg
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Multimedia :: Video :: Conversion
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Provides-Extra: bundled-ffmpeg
|
|
20
|
+
Requires-Dist: imageio-ffmpeg; extra == "bundled-ffmpeg"
|
|
21
|
+
|
|
22
|
+
# airclip
|
|
23
|
+
|
|
24
|
+
Convert any video to an ultra-lightweight WebM that blends seamlessly into web pages.
|
|
25
|
+
|
|
26
|
+
**30 seconds of video → ~50-200 KB** without visible quality loss.
|
|
27
|
+
|
|
28
|
+
## Why
|
|
29
|
+
|
|
30
|
+
Embedding videos on the web usually means large files, slow loads, and visible player chrome. `airclip` solves this for animation and diagram content:
|
|
31
|
+
|
|
32
|
+
| Metric | Before | After |
|
|
33
|
+
|--------|--------|-------|
|
|
34
|
+
| 30s video | ~1-3 MB | ~50-200 KB |
|
|
35
|
+
| Format | MP4 (H.264) | WebM (VP9) |
|
|
36
|
+
| FPS | 60 | 24 |
|
|
37
|
+
| Resolution | 1080p | 720p |
|
|
38
|
+
| Audio | Included | Stripped |
|
|
39
|
+
|
|
40
|
+
The output blends into dark backgrounds — no borders, no player UI, just content that looks native to the page.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
1. **VP9 encoding** — modern codec designed for web, much better compression than H.264
|
|
45
|
+
2. **High CRF** — animation content (solid backgrounds, vector shapes) compresses extremely well at CRF 35-45
|
|
46
|
+
3. **Reduced framerate** — 24fps is visually identical to 60fps for slides and diagrams
|
|
47
|
+
4. **Downscale to 720p** — on web, nobody notices the difference from 1080p
|
|
48
|
+
5. **Two-pass encoding** — analyzes content first, then allocates bits where they matter
|
|
49
|
+
6. **No audio** — animations don't need it, saves ~30% file size
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install imageio-ffmpeg
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or have `ffmpeg` available on your PATH.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Single file
|
|
63
|
+
python -m airclip video.mp4
|
|
64
|
+
|
|
65
|
+
# Entire directory
|
|
66
|
+
python -m airclip videos/
|
|
67
|
+
|
|
68
|
+
# Custom settings
|
|
69
|
+
python -m airclip video.mp4 --fps 15 --crf 42 --height 480
|
|
70
|
+
|
|
71
|
+
# Fast mode (skip 2-pass, ~2x faster)
|
|
72
|
+
python -m airclip video.mp4 --no-2pass
|
|
73
|
+
|
|
74
|
+
# Output to a different directory
|
|
75
|
+
python -m airclip videos/ --outdir dist/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### As a library
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from airclip import convert_lightweight
|
|
82
|
+
|
|
83
|
+
result = convert_lightweight(
|
|
84
|
+
"input.mp4",
|
|
85
|
+
output_path="output.webm",
|
|
86
|
+
target_fps=24,
|
|
87
|
+
crf=38,
|
|
88
|
+
max_height=720,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
print(f"{result['input_kb']:.0f} KB → {result['output_kb']:.0f} KB")
|
|
92
|
+
print(f"{result['ratio']:.1f}x smaller")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Options
|
|
96
|
+
|
|
97
|
+
| Flag | Default | Description |
|
|
98
|
+
|------|---------|-------------|
|
|
99
|
+
| `--fps` | 24 | Target framerate. Use 15 for near-static content. |
|
|
100
|
+
| `--crf` | 38 | Quality level. Higher = smaller. 35-45 works well for animations. |
|
|
101
|
+
| `--height` | 720 | Max output height in pixels. |
|
|
102
|
+
| `--no-2pass` | off | Skip 2-pass encoding (faster, slightly larger output). |
|
|
103
|
+
| `--outdir` | same as input | Output directory for converted files. |
|
|
104
|
+
|
|
105
|
+
## CRF guide
|
|
106
|
+
|
|
107
|
+
| CRF | Quality | Best for |
|
|
108
|
+
|-----|---------|----------|
|
|
109
|
+
| 30-34 | High | Live action, complex motion |
|
|
110
|
+
| 35-38 | Good | Animations with fine detail |
|
|
111
|
+
| 39-42 | Small | Diagrams, slides, code demos |
|
|
112
|
+
| 43-50 | Tiny | Static content, simple shapes |
|
|
113
|
+
|
|
114
|
+
## Embedding on web
|
|
115
|
+
|
|
116
|
+
The output is designed to look native on dark-themed pages:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<video autoplay muted loop playsinline>
|
|
120
|
+
<source src="animation.webm" type="video/webm">
|
|
121
|
+
<source src="animation.mp4" type="video/mp4"> <!-- fallback -->
|
|
122
|
+
</video>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```css
|
|
126
|
+
video {
|
|
127
|
+
width: 100%;
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: none;
|
|
130
|
+
border-radius: 12px;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For autoplay-on-scroll:
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const observer = new IntersectionObserver((entries) => {
|
|
138
|
+
entries.forEach(e => {
|
|
139
|
+
e.isIntersecting ? e.target.play() : e.target.pause();
|
|
140
|
+
});
|
|
141
|
+
}, { threshold: 0.5 });
|
|
142
|
+
|
|
143
|
+
document.querySelectorAll('video').forEach(v => observer.observe(v));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Best results when
|
|
147
|
+
|
|
148
|
+
- Background is a solid or near-solid color (dark themes work great)
|
|
149
|
+
- Content is vector-like: shapes, text, diagrams, code
|
|
150
|
+
- Motion is smooth and predictable (not chaotic)
|
|
151
|
+
- No audio needed
|
|
152
|
+
|
|
153
|
+
## Results
|
|
154
|
+
|
|
155
|
+
Across 6 test videos: **~1.2 MB → ~93 KB average (12x compression)**. No visible quality loss in browser playback.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
airclip-0.1.0/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# airclip
|
|
2
|
+
|
|
3
|
+
Convert any video to an ultra-lightweight WebM that blends seamlessly into web pages.
|
|
4
|
+
|
|
5
|
+
**30 seconds of video → ~50-200 KB** without visible quality loss.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Embedding videos on the web usually means large files, slow loads, and visible player chrome. `airclip` solves this for animation and diagram content:
|
|
10
|
+
|
|
11
|
+
| Metric | Before | After |
|
|
12
|
+
|--------|--------|-------|
|
|
13
|
+
| 30s video | ~1-3 MB | ~50-200 KB |
|
|
14
|
+
| Format | MP4 (H.264) | WebM (VP9) |
|
|
15
|
+
| FPS | 60 | 24 |
|
|
16
|
+
| Resolution | 1080p | 720p |
|
|
17
|
+
| Audio | Included | Stripped |
|
|
18
|
+
|
|
19
|
+
The output blends into dark backgrounds — no borders, no player UI, just content that looks native to the page.
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
1. **VP9 encoding** — modern codec designed for web, much better compression than H.264
|
|
24
|
+
2. **High CRF** — animation content (solid backgrounds, vector shapes) compresses extremely well at CRF 35-45
|
|
25
|
+
3. **Reduced framerate** — 24fps is visually identical to 60fps for slides and diagrams
|
|
26
|
+
4. **Downscale to 720p** — on web, nobody notices the difference from 1080p
|
|
27
|
+
5. **Two-pass encoding** — analyzes content first, then allocates bits where they matter
|
|
28
|
+
6. **No audio** — animations don't need it, saves ~30% file size
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install imageio-ffmpeg
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or have `ffmpeg` available on your PATH.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Single file
|
|
42
|
+
python -m airclip video.mp4
|
|
43
|
+
|
|
44
|
+
# Entire directory
|
|
45
|
+
python -m airclip videos/
|
|
46
|
+
|
|
47
|
+
# Custom settings
|
|
48
|
+
python -m airclip video.mp4 --fps 15 --crf 42 --height 480
|
|
49
|
+
|
|
50
|
+
# Fast mode (skip 2-pass, ~2x faster)
|
|
51
|
+
python -m airclip video.mp4 --no-2pass
|
|
52
|
+
|
|
53
|
+
# Output to a different directory
|
|
54
|
+
python -m airclip videos/ --outdir dist/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### As a library
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from airclip import convert_lightweight
|
|
61
|
+
|
|
62
|
+
result = convert_lightweight(
|
|
63
|
+
"input.mp4",
|
|
64
|
+
output_path="output.webm",
|
|
65
|
+
target_fps=24,
|
|
66
|
+
crf=38,
|
|
67
|
+
max_height=720,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
print(f"{result['input_kb']:.0f} KB → {result['output_kb']:.0f} KB")
|
|
71
|
+
print(f"{result['ratio']:.1f}x smaller")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
| Flag | Default | Description |
|
|
77
|
+
|------|---------|-------------|
|
|
78
|
+
| `--fps` | 24 | Target framerate. Use 15 for near-static content. |
|
|
79
|
+
| `--crf` | 38 | Quality level. Higher = smaller. 35-45 works well for animations. |
|
|
80
|
+
| `--height` | 720 | Max output height in pixels. |
|
|
81
|
+
| `--no-2pass` | off | Skip 2-pass encoding (faster, slightly larger output). |
|
|
82
|
+
| `--outdir` | same as input | Output directory for converted files. |
|
|
83
|
+
|
|
84
|
+
## CRF guide
|
|
85
|
+
|
|
86
|
+
| CRF | Quality | Best for |
|
|
87
|
+
|-----|---------|----------|
|
|
88
|
+
| 30-34 | High | Live action, complex motion |
|
|
89
|
+
| 35-38 | Good | Animations with fine detail |
|
|
90
|
+
| 39-42 | Small | Diagrams, slides, code demos |
|
|
91
|
+
| 43-50 | Tiny | Static content, simple shapes |
|
|
92
|
+
|
|
93
|
+
## Embedding on web
|
|
94
|
+
|
|
95
|
+
The output is designed to look native on dark-themed pages:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<video autoplay muted loop playsinline>
|
|
99
|
+
<source src="animation.webm" type="video/webm">
|
|
100
|
+
<source src="animation.mp4" type="video/mp4"> <!-- fallback -->
|
|
101
|
+
</video>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```css
|
|
105
|
+
video {
|
|
106
|
+
width: 100%;
|
|
107
|
+
background: transparent;
|
|
108
|
+
border: none;
|
|
109
|
+
border-radius: 12px;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
For autoplay-on-scroll:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const observer = new IntersectionObserver((entries) => {
|
|
117
|
+
entries.forEach(e => {
|
|
118
|
+
e.isIntersecting ? e.target.play() : e.target.pause();
|
|
119
|
+
});
|
|
120
|
+
}, { threshold: 0.5 });
|
|
121
|
+
|
|
122
|
+
document.querySelectorAll('video').forEach(v => observer.observe(v));
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Best results when
|
|
126
|
+
|
|
127
|
+
- Background is a solid or near-solid color (dark themes work great)
|
|
128
|
+
- Content is vector-like: shapes, text, diagrams, code
|
|
129
|
+
- Motion is smooth and predictable (not chaotic)
|
|
130
|
+
- No audio needed
|
|
131
|
+
|
|
132
|
+
## Results
|
|
133
|
+
|
|
134
|
+
Across 6 test videos: **~1.2 MB → ~93 KB average (12x compression)**. No visible quality loss in browser playback.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .airclip import convert_lightweight, get_video_info
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""airclip — Convert videos to ultra-lightweight web-embeddable format.
|
|
2
|
+
|
|
3
|
+
Takes any video and produces a tiny WebM (VP9) that blends seamlessly
|
|
4
|
+
into web pages. Optimized for animation/diagram content where
|
|
5
|
+
solid backgrounds and limited motion allow extreme compression.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m airclip video.mp4 # Single file
|
|
9
|
+
python -m airclip video.mp4 --fps 15 --crf 40 # Custom
|
|
10
|
+
"""
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# imageio-ffmpeg bundles ffmpeg so we don't need a system install
|
|
18
|
+
try:
|
|
19
|
+
import imageio_ffmpeg
|
|
20
|
+
FFMPEG = imageio_ffmpeg.get_ffmpeg_exe()
|
|
21
|
+
except ImportError:
|
|
22
|
+
FFMPEG = "ffmpeg"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_video_info(path: str) -> dict:
|
|
26
|
+
"""Get duration and resolution using ffprobe."""
|
|
27
|
+
ffprobe = Path(FFMPEG).parent / Path(FFMPEG).name.replace("ffmpeg", "ffprobe")
|
|
28
|
+
if not ffprobe.exists():
|
|
29
|
+
# Fall back: try without info
|
|
30
|
+
return {"duration": 0, "width": 0, "height": 0, "fps": 30}
|
|
31
|
+
cmd = [
|
|
32
|
+
ffprobe, "-v", "quiet",
|
|
33
|
+
"-print_format", "json",
|
|
34
|
+
"-show_format", "-show_streams",
|
|
35
|
+
path,
|
|
36
|
+
]
|
|
37
|
+
import json
|
|
38
|
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
39
|
+
if r.returncode != 0:
|
|
40
|
+
return {}
|
|
41
|
+
data = json.loads(r.stdout)
|
|
42
|
+
stream = next((s for s in data.get("streams", []) if s["codec_type"] == "video"), {})
|
|
43
|
+
return {
|
|
44
|
+
"duration": float(data.get("format", {}).get("duration", 0)),
|
|
45
|
+
"width": int(stream.get("width", 0)),
|
|
46
|
+
"height": int(stream.get("height", 0)),
|
|
47
|
+
"fps": eval(stream.get("r_frame_rate", "30/1")),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def convert_lightweight(
|
|
52
|
+
input_path: str,
|
|
53
|
+
output_path: str | None = None,
|
|
54
|
+
target_fps: int = 24,
|
|
55
|
+
crf: int = 38,
|
|
56
|
+
max_height: int = 720,
|
|
57
|
+
two_pass: bool = True,
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""Convert a video to ultra-lightweight WebM VP9.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
input_path: Source video file.
|
|
63
|
+
output_path: Output path. Default: same name with .webm extension.
|
|
64
|
+
target_fps: Target framerate. 15-24 is ideal for animations.
|
|
65
|
+
crf: Constant Rate Factor. Higher = smaller. 35-45 for animations.
|
|
66
|
+
max_height: Max output height. 720 is good for web.
|
|
67
|
+
two_pass: Use 2-pass encoding for better quality/size ratio.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Dict with input_size, output_size, ratio, duration.
|
|
71
|
+
"""
|
|
72
|
+
inp = Path(input_path)
|
|
73
|
+
if output_path is None:
|
|
74
|
+
output_path = str(inp.with_suffix(".webm"))
|
|
75
|
+
out = Path(output_path)
|
|
76
|
+
|
|
77
|
+
input_size = inp.stat().st_size
|
|
78
|
+
|
|
79
|
+
# Scale filter: only downscale, keep aspect ratio
|
|
80
|
+
scale_filter = f"scale=-2:'min({max_height},ih)'"
|
|
81
|
+
vf = f"{scale_filter},fps={target_fps}"
|
|
82
|
+
|
|
83
|
+
if two_pass:
|
|
84
|
+
# Pass 1: analyze
|
|
85
|
+
pass1 = [
|
|
86
|
+
FFMPEG, "-y", "-i", str(inp),
|
|
87
|
+
"-vf", vf,
|
|
88
|
+
"-c:v", "libvpx-vp9",
|
|
89
|
+
"-b:v", "0", "-crf", str(crf),
|
|
90
|
+
"-pass", "1",
|
|
91
|
+
"-an",
|
|
92
|
+
"-f", "null",
|
|
93
|
+
"NUL" if sys.platform == "win32" else "/dev/null",
|
|
94
|
+
]
|
|
95
|
+
subprocess.run(pass1, capture_output=True)
|
|
96
|
+
|
|
97
|
+
# Pass 2: encode
|
|
98
|
+
pass2 = [
|
|
99
|
+
FFMPEG, "-y", "-i", str(inp),
|
|
100
|
+
"-vf", vf,
|
|
101
|
+
"-c:v", "libvpx-vp9",
|
|
102
|
+
"-b:v", "0", "-crf", str(crf),
|
|
103
|
+
"-pass", "2",
|
|
104
|
+
"-an", # no audio for animations
|
|
105
|
+
"-auto-alt-ref", "1",
|
|
106
|
+
"-lag-in-frames", "25",
|
|
107
|
+
"-row-mt", "1",
|
|
108
|
+
str(out),
|
|
109
|
+
]
|
|
110
|
+
subprocess.run(pass2, capture_output=True)
|
|
111
|
+
|
|
112
|
+
# Cleanup pass log files
|
|
113
|
+
for f in Path(".").glob("ffmpeg2pass-0*"):
|
|
114
|
+
f.unlink(missing_ok=True)
|
|
115
|
+
else:
|
|
116
|
+
cmd = [
|
|
117
|
+
FFMPEG, "-y", "-i", str(inp),
|
|
118
|
+
"-vf", vf,
|
|
119
|
+
"-c:v", "libvpx-vp9",
|
|
120
|
+
"-b:v", "0", "-crf", str(crf),
|
|
121
|
+
"-an",
|
|
122
|
+
"-row-mt", "1",
|
|
123
|
+
str(out),
|
|
124
|
+
]
|
|
125
|
+
subprocess.run(cmd, capture_output=True)
|
|
126
|
+
|
|
127
|
+
output_size = out.stat().st_size if out.exists() else 0
|
|
128
|
+
info = get_video_info(str(inp))
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"input": str(inp.name),
|
|
132
|
+
"output": str(out.name),
|
|
133
|
+
"input_size": input_size,
|
|
134
|
+
"output_size": output_size,
|
|
135
|
+
"ratio": input_size / output_size if output_size else 0,
|
|
136
|
+
"duration": info.get("duration", 0),
|
|
137
|
+
"input_kb": input_size / 1024,
|
|
138
|
+
"output_kb": output_size / 1024,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main():
|
|
143
|
+
parser = argparse.ArgumentParser(description="Convert videos to lightweight web-embeddable format")
|
|
144
|
+
parser.add_argument("input", help="Video file or directory of videos")
|
|
145
|
+
parser.add_argument("--fps", type=int, default=24, help="Target FPS (default: 24)")
|
|
146
|
+
parser.add_argument("--crf", type=int, default=38, help="Quality (higher=smaller, default: 38)")
|
|
147
|
+
parser.add_argument("--height", type=int, default=720, help="Max height in px (default: 720)")
|
|
148
|
+
parser.add_argument("--no-2pass", action="store_true", help="Skip 2-pass encoding (faster)")
|
|
149
|
+
parser.add_argument("--outdir", type=str, default=None, help="Output directory")
|
|
150
|
+
args = parser.parse_args()
|
|
151
|
+
|
|
152
|
+
target = Path(args.input)
|
|
153
|
+
if target.is_dir():
|
|
154
|
+
files = sorted(target.glob("*.mp4"))
|
|
155
|
+
else:
|
|
156
|
+
files = [target]
|
|
157
|
+
|
|
158
|
+
if not files:
|
|
159
|
+
print("No MP4 files found.")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
print(f"\n Converting {len(files)} video(s) to lightweight WebM")
|
|
163
|
+
print(f" Settings: {args.fps}fps, CRF {args.crf}, max {args.height}p, 2-pass={not args.no_2pass}")
|
|
164
|
+
print(f" {'─' * 60}")
|
|
165
|
+
|
|
166
|
+
total_in = 0
|
|
167
|
+
total_out = 0
|
|
168
|
+
|
|
169
|
+
for f in files:
|
|
170
|
+
out_dir = Path(args.outdir) if args.outdir else f.parent
|
|
171
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
out_path = out_dir / f"{f.stem}.webm"
|
|
173
|
+
|
|
174
|
+
result = convert_lightweight(
|
|
175
|
+
str(f), str(out_path),
|
|
176
|
+
target_fps=args.fps,
|
|
177
|
+
crf=args.crf,
|
|
178
|
+
max_height=args.height,
|
|
179
|
+
two_pass=not args.no_2pass,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
total_in += result["input_size"]
|
|
183
|
+
total_out += result["output_size"]
|
|
184
|
+
|
|
185
|
+
print(
|
|
186
|
+
f" {result['input']:40s} "
|
|
187
|
+
f"{result['input_kb']:7.0f} KB → {result['output_kb']:6.0f} KB "
|
|
188
|
+
f"({result['ratio']:5.1f}x smaller)"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
print(f" {'─' * 60}")
|
|
192
|
+
print(
|
|
193
|
+
f" Total: {total_in/1024:,.0f} KB → {total_out/1024:,.0f} KB "
|
|
194
|
+
f"({total_in/total_out:.1f}x compression)"
|
|
195
|
+
)
|
|
196
|
+
print()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: airclip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert any video to an ultra-lightweight WebM that blends seamlessly into web pages.
|
|
5
|
+
Author: Akash Chekka
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/akashchekka/airclip
|
|
8
|
+
Project-URL: Repository, https://github.com/akashchekka/airclip
|
|
9
|
+
Keywords: video,webm,vp9,compression,web,ffmpeg
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Multimedia :: Video :: Conversion
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Provides-Extra: bundled-ffmpeg
|
|
20
|
+
Requires-Dist: imageio-ffmpeg; extra == "bundled-ffmpeg"
|
|
21
|
+
|
|
22
|
+
# airclip
|
|
23
|
+
|
|
24
|
+
Convert any video to an ultra-lightweight WebM that blends seamlessly into web pages.
|
|
25
|
+
|
|
26
|
+
**30 seconds of video → ~50-200 KB** without visible quality loss.
|
|
27
|
+
|
|
28
|
+
## Why
|
|
29
|
+
|
|
30
|
+
Embedding videos on the web usually means large files, slow loads, and visible player chrome. `airclip` solves this for animation and diagram content:
|
|
31
|
+
|
|
32
|
+
| Metric | Before | After |
|
|
33
|
+
|--------|--------|-------|
|
|
34
|
+
| 30s video | ~1-3 MB | ~50-200 KB |
|
|
35
|
+
| Format | MP4 (H.264) | WebM (VP9) |
|
|
36
|
+
| FPS | 60 | 24 |
|
|
37
|
+
| Resolution | 1080p | 720p |
|
|
38
|
+
| Audio | Included | Stripped |
|
|
39
|
+
|
|
40
|
+
The output blends into dark backgrounds — no borders, no player UI, just content that looks native to the page.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
1. **VP9 encoding** — modern codec designed for web, much better compression than H.264
|
|
45
|
+
2. **High CRF** — animation content (solid backgrounds, vector shapes) compresses extremely well at CRF 35-45
|
|
46
|
+
3. **Reduced framerate** — 24fps is visually identical to 60fps for slides and diagrams
|
|
47
|
+
4. **Downscale to 720p** — on web, nobody notices the difference from 1080p
|
|
48
|
+
5. **Two-pass encoding** — analyzes content first, then allocates bits where they matter
|
|
49
|
+
6. **No audio** — animations don't need it, saves ~30% file size
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install imageio-ffmpeg
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or have `ffmpeg` available on your PATH.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Single file
|
|
63
|
+
python -m airclip video.mp4
|
|
64
|
+
|
|
65
|
+
# Entire directory
|
|
66
|
+
python -m airclip videos/
|
|
67
|
+
|
|
68
|
+
# Custom settings
|
|
69
|
+
python -m airclip video.mp4 --fps 15 --crf 42 --height 480
|
|
70
|
+
|
|
71
|
+
# Fast mode (skip 2-pass, ~2x faster)
|
|
72
|
+
python -m airclip video.mp4 --no-2pass
|
|
73
|
+
|
|
74
|
+
# Output to a different directory
|
|
75
|
+
python -m airclip videos/ --outdir dist/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### As a library
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from airclip import convert_lightweight
|
|
82
|
+
|
|
83
|
+
result = convert_lightweight(
|
|
84
|
+
"input.mp4",
|
|
85
|
+
output_path="output.webm",
|
|
86
|
+
target_fps=24,
|
|
87
|
+
crf=38,
|
|
88
|
+
max_height=720,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
print(f"{result['input_kb']:.0f} KB → {result['output_kb']:.0f} KB")
|
|
92
|
+
print(f"{result['ratio']:.1f}x smaller")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Options
|
|
96
|
+
|
|
97
|
+
| Flag | Default | Description |
|
|
98
|
+
|------|---------|-------------|
|
|
99
|
+
| `--fps` | 24 | Target framerate. Use 15 for near-static content. |
|
|
100
|
+
| `--crf` | 38 | Quality level. Higher = smaller. 35-45 works well for animations. |
|
|
101
|
+
| `--height` | 720 | Max output height in pixels. |
|
|
102
|
+
| `--no-2pass` | off | Skip 2-pass encoding (faster, slightly larger output). |
|
|
103
|
+
| `--outdir` | same as input | Output directory for converted files. |
|
|
104
|
+
|
|
105
|
+
## CRF guide
|
|
106
|
+
|
|
107
|
+
| CRF | Quality | Best for |
|
|
108
|
+
|-----|---------|----------|
|
|
109
|
+
| 30-34 | High | Live action, complex motion |
|
|
110
|
+
| 35-38 | Good | Animations with fine detail |
|
|
111
|
+
| 39-42 | Small | Diagrams, slides, code demos |
|
|
112
|
+
| 43-50 | Tiny | Static content, simple shapes |
|
|
113
|
+
|
|
114
|
+
## Embedding on web
|
|
115
|
+
|
|
116
|
+
The output is designed to look native on dark-themed pages:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<video autoplay muted loop playsinline>
|
|
120
|
+
<source src="animation.webm" type="video/webm">
|
|
121
|
+
<source src="animation.mp4" type="video/mp4"> <!-- fallback -->
|
|
122
|
+
</video>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```css
|
|
126
|
+
video {
|
|
127
|
+
width: 100%;
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: none;
|
|
130
|
+
border-radius: 12px;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For autoplay-on-scroll:
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const observer = new IntersectionObserver((entries) => {
|
|
138
|
+
entries.forEach(e => {
|
|
139
|
+
e.isIntersecting ? e.target.play() : e.target.pause();
|
|
140
|
+
});
|
|
141
|
+
}, { threshold: 0.5 });
|
|
142
|
+
|
|
143
|
+
document.querySelectorAll('video').forEach(v => observer.observe(v));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Best results when
|
|
147
|
+
|
|
148
|
+
- Background is a solid or near-solid color (dark themes work great)
|
|
149
|
+
- Content is vector-like: shapes, text, diagrams, code
|
|
150
|
+
- Motion is smooth and predictable (not chaotic)
|
|
151
|
+
- No audio needed
|
|
152
|
+
|
|
153
|
+
## Results
|
|
154
|
+
|
|
155
|
+
Across 6 test videos: **~1.2 MB → ~93 KB average (12x compression)**. No visible quality loss in browser playback.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
airclip/__init__.py
|
|
4
|
+
airclip/__main__.py
|
|
5
|
+
airclip/airclip.py
|
|
6
|
+
airclip.egg-info/PKG-INFO
|
|
7
|
+
airclip.egg-info/SOURCES.txt
|
|
8
|
+
airclip.egg-info/dependency_links.txt
|
|
9
|
+
airclip.egg-info/entry_points.txt
|
|
10
|
+
airclip.egg-info/requires.txt
|
|
11
|
+
airclip.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
airclip
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "airclip"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Convert any video to an ultra-lightweight WebM that blends seamlessly into web pages."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Akash Chekka" }]
|
|
13
|
+
keywords = ["video", "webm", "vp9", "compression", "web", "ffmpeg"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Multimedia :: Video :: Conversion",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
bundled-ffmpeg = ["imageio-ffmpeg"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
airclip = "airclip.airclip:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/akashchekka/airclip"
|
|
33
|
+
Repository = "https://github.com/akashchekka/airclip"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
include = ["airclip*"]
|
airclip-0.1.0/setup.cfg
ADDED