peg-this 3.0.2__py3-none-any.whl → 4.1.0__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,129 @@
1
+
2
+ import subprocess
3
+ import logging
4
+ import sys
5
+
6
+ import ffmpeg
7
+ from rich.console import Console
8
+ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
9
+
10
+ console = Console()
11
+
12
+
13
+ def check_ffmpeg_ffprobe():
14
+ """
15
+ Checks if ffmpeg and ffprobe executables are available in the system's PATH.
16
+ ffmpeg-python requires this.
17
+ """
18
+ try:
19
+ # The library does this internally, but we can provide a clearer error message.
20
+ subprocess.check_call(['ffmpeg', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
21
+ subprocess.check_call(['ffprobe', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
22
+ except FileNotFoundError:
23
+ console.print("[bold red]Error: ffmpeg and ffprobe not found.[/bold red]")
24
+ if sys.platform == "win32":
25
+ console.print("You can install it using Chocolatey: [bold]choco install ffmpeg[/bold]")
26
+ console.print("Or Scoop: [bold]scoop install ffmpeg[/bold]")
27
+ elif sys.platform == "darwin":
28
+ console.print("You can install it using Homebrew: [bold]brew install ffmpeg[/bold]")
29
+ else:
30
+ console.print("You can install it using your system's package manager, e.g., [bold]sudo apt update && sudo apt install ffmpeg[/bold] on Debian/Ubuntu.")
31
+ console.print("Please ensure its location is in your system's PATH.")
32
+ sys.exit(1)
33
+
34
+
35
+ def run_command(stream_spec, description="Processing...", show_progress=False):
36
+ """
37
+ Runs an ffmpeg command using ffmpeg-python.
38
+ - For simple commands, it runs directly.
39
+ - For commands with a progress bar, it generates the ffmpeg arguments,
40
+ runs them as a subprocess, and parses stderr to show progress.
41
+ Returns True on success, False on failure.
42
+ """
43
+ console.print(f"[bold cyan]{description}[/bold cyan]")
44
+
45
+ args = stream_spec.get_args()
46
+ full_command = ['ffmpeg'] + args
47
+ logging.info(f"Executing command: {' '.join(full_command)}")
48
+
49
+ if not show_progress:
50
+ try:
51
+ # Use ffmpeg.run() for simple, non-progress tasks. It's cleaner.
52
+ ffmpeg.run(stream_spec, capture_stdout=True, capture_stderr=True, quiet=True)
53
+ logging.info("Command successful (no progress bar).")
54
+ return True
55
+ except ffmpeg.Error as e:
56
+ error_message = e.stderr.decode('utf-8')
57
+ console.print("[bold red]An error occurred:[/bold red]")
58
+ console.print(error_message)
59
+ logging.error(f"ffmpeg error:{error_message}")
60
+ return False
61
+ else:
62
+ # For the progress bar, we must run ffmpeg as a subprocess and parse stderr.
63
+ duration = 0
64
+ try:
65
+ input_file_path = None
66
+ for i, arg in enumerate(full_command):
67
+ if arg == '-i' and i + 1 < len(full_command):
68
+ input_file_path = full_command[i+1]
69
+ break
70
+
71
+ if input_file_path:
72
+ probe_info = ffmpeg.probe(input_file_path)
73
+ duration = float(probe_info['format']['duration'])
74
+ else:
75
+ logging.warning("Could not find input file in command to determine duration for progress bar.")
76
+
77
+ except (ffmpeg.Error, KeyError) as e:
78
+ console.print(f"[bold yellow]Warning: Could not determine video duration for progress bar.[/bold yellow]")
79
+ logging.warning(f"Could not probe for duration: {e}")
80
+
81
+ with Progress(
82
+ SpinnerColumn(),
83
+ TextColumn("[progress.description]{task.description}"),
84
+ BarColumn(),
85
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
86
+ console=console,
87
+ ) as progress:
88
+ task = progress.add_task(description, total=100)
89
+
90
+ process = subprocess.Popen(
91
+ full_command,
92
+ stdout=subprocess.PIPE,
93
+ stderr=subprocess.PIPE,
94
+ universal_newlines=True,
95
+ encoding='utf-8'
96
+ )
97
+
98
+ for line in process.stderr:
99
+ logging.debug(f"ffmpeg stderr: {line.strip()}")
100
+ if "time=" in line and duration > 0:
101
+ try:
102
+ time_str = line.split("time=")[1].split(" ")[0].strip()
103
+ h, m, s_parts = time_str.split(':')
104
+ s = float(s_parts)
105
+ elapsed_time = int(h) * 3600 + int(m) * 60 + s
106
+ percent_complete = (elapsed_time / duration) * 100
107
+ progress.update(task, completed=min(percent_complete, 100))
108
+ except Exception:
109
+ pass
110
+
111
+ process.wait()
112
+ progress.update(task, completed=100)
113
+
114
+ if process.returncode != 0:
115
+ log_file = logging.getLogger().handlers[0].baseFilename
116
+ console.print(f"[bold red]An error occurred during processing. Check {log_file} for details.[/bold red]")
117
+ return False
118
+
119
+ logging.info("Command successful (with progress bar).")
120
+ return True
121
+
122
+
123
+ def has_audio_stream(file_path):
124
+ """Check if the media file has an audio stream."""
125
+ try:
126
+ probe = ffmpeg.probe(file_path, select_streams='a')
127
+ return 'streams' in probe and len(probe['streams']) > 0
128
+ except ffmpeg.Error:
129
+ return False
@@ -0,0 +1,52 @@
1
+
2
+ import os
3
+ from pathlib import Path
4
+
5
+ import questionary
6
+ from rich.console import Console
7
+
8
+ try:
9
+ import tkinter as tk
10
+ from tkinter import filedialog
11
+ except ImportError:
12
+ tk = None
13
+
14
+ console = Console()
15
+
16
+
17
+ def get_media_files():
18
+ """Scan the current directory for media files."""
19
+ media_extensions = [
20
+ # Video
21
+ ".mkv", ".mp4", ".avi", ".mov", ".webm", ".flv", ".wmv",
22
+ # Audio
23
+ ".mp3", ".flac", ".wav", ".ogg",
24
+ # GIF
25
+ ".gif",
26
+ # Image
27
+ ".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"
28
+ ]
29
+ files = [f for f in os.listdir('.') if os.path.isfile(f) and Path(f).suffix.lower() in media_extensions]
30
+ return files
31
+
32
+
33
+ def select_media_file():
34
+ """Display a menu to select a media file, or open a file picker."""
35
+ media_files = get_media_files()
36
+ if not media_files:
37
+ console.print("[bold yellow]No media files found in this directory.[/bold yellow]")
38
+ if tk and questionary.confirm("Would you like to select a file from another location?").ask():
39
+ root = tk.Tk()
40
+ root.withdraw()
41
+ file_path = filedialog.askopenfilename(
42
+ title="Select a media file",
43
+ filetypes=[("Media Files", "*.mkv *.mp4 *.avi *.mov *.webm *.flv *.wmv *.mp3 *.flac *.wav *.ogg *.gif *.jpg *.jpeg *.png *.webp *.bmp *.tiff"), ("All Files", "*.*")]
44
+ )
45
+ return file_path if file_path else None
46
+ return None
47
+
48
+ choices = media_files + [questionary.Separator(), "Back"]
49
+ file = questionary.select("Select a media file to process:", choices=choices, use_indicator=True).ask()
50
+
51
+ # Return the absolute path to prevent "file not found" errors
52
+ return os.path.abspath(file) if file and file != "Back" else None
@@ -0,0 +1,228 @@
1
+ import os
2
+ import re
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ import ffmpeg
7
+ import questionary
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ def check_file_exists(file_path):
14
+ if not os.path.exists(file_path):
15
+ console.print("[bold red]Error: File not found.[/bold red]")
16
+ return False
17
+ return True
18
+
19
+
20
+ def check_file_readable(file_path):
21
+ if not os.access(file_path, os.R_OK):
22
+ console.print("[bold red]Error: Cannot read file. Check permissions.[/bold red]")
23
+ return False
24
+ return True
25
+
26
+
27
+ def check_write_permission(directory):
28
+ if not os.access(directory, os.W_OK):
29
+ console.print("[bold red]Error: Cannot write to this location. Check permissions.[/bold red]")
30
+ return False
31
+ return True
32
+
33
+
34
+ def validate_input_file(file_path):
35
+ if not check_file_exists(file_path):
36
+ return False
37
+ if not check_file_readable(file_path):
38
+ return False
39
+ return True
40
+
41
+
42
+ def check_output_file(output_path, file_type="file"):
43
+ if not os.path.exists(output_path):
44
+ return 'proceed', output_path
45
+
46
+ console.print(f"[yellow]Warning: {file_type} already exists:[/yellow]")
47
+ console.print(f"[dim]{output_path}[/dim]")
48
+
49
+ choice = questionary.select(
50
+ "What would you like to do?",
51
+ choices=["Overwrite existing file", "Save with a new name", "Cancel operation"]
52
+ ).ask()
53
+
54
+ if not choice or "Cancel" in choice:
55
+ return 'cancel', None
56
+ elif "Overwrite" in choice:
57
+ return 'overwrite', output_path
58
+ else:
59
+ path = Path(output_path)
60
+ counter = 1
61
+ while True:
62
+ new_name = f"{path.stem}_{counter}{path.suffix}"
63
+ new_path = path.with_name(new_name)
64
+ if not os.path.exists(new_path):
65
+ console.print(f"[cyan]Will save as: {new_path.name}[/cyan]")
66
+ return 'rename', str(new_path)
67
+ counter += 1
68
+
69
+
70
+ def check_disk_space(file_path, multiplier=2):
71
+ try:
72
+ input_size = os.path.getsize(file_path)
73
+ required_space = input_size * multiplier
74
+ total, used, free = shutil.disk_usage(Path(file_path).parent)
75
+ if free < required_space:
76
+ free_gb = free / (1024**3)
77
+ required_gb = required_space / (1024**3)
78
+ console.print(f"[yellow]Warning: Low disk space![/yellow]")
79
+ console.print(f"[dim]Available: {free_gb:.1f} GB, Estimated needed: {required_gb:.1f} GB[/dim]")
80
+ if not questionary.confirm("Continue anyway?", default=False).ask():
81
+ return False
82
+ return True
83
+ except Exception:
84
+ return True
85
+
86
+
87
+ def get_video_duration(file_path):
88
+ try:
89
+ probe = ffmpeg.probe(file_path)
90
+ return float(probe['format'].get('duration', 0))
91
+ except Exception:
92
+ return 0
93
+
94
+
95
+ def format_duration(seconds):
96
+ if seconds < 60:
97
+ return f"{int(seconds)} seconds"
98
+ elif seconds < 3600:
99
+ mins = int(seconds // 60)
100
+ secs = int(seconds % 60)
101
+ return f"{mins}m {secs}s"
102
+ else:
103
+ hours = int(seconds // 3600)
104
+ mins = int((seconds % 3600) // 60)
105
+ return f"{hours}h {mins}m"
106
+
107
+
108
+ def parse_time_to_seconds(time_str):
109
+ time_str = time_str.strip()
110
+
111
+ # Already in seconds (numeric)
112
+ if re.match(r'^\d+(\.\d+)?$', time_str):
113
+ return float(time_str)
114
+
115
+ # HH:MM:SS or MM:SS format
116
+ if re.match(r'^\d{1,2}:\d{2}(:\d{2})?(\.\d+)?$', time_str):
117
+ parts = time_str.split(':')
118
+ if len(parts) == 2:
119
+ mins, secs = parts
120
+ return int(mins) * 60 + float(secs)
121
+ elif len(parts) == 3:
122
+ hours, mins, secs = parts
123
+ return int(hours) * 3600 + int(mins) * 60 + float(secs)
124
+
125
+ return None
126
+
127
+
128
+ def validate_time_input(time_str, max_duration=None, field_name="Time"):
129
+ seconds = parse_time_to_seconds(time_str)
130
+
131
+ if seconds is None:
132
+ console.print(f"[bold red]Invalid {field_name.lower()} format. Use HH:MM:SS, MM:SS, or seconds.[/bold red]")
133
+ return None
134
+
135
+ if seconds < 0:
136
+ console.print(f"[bold red]{field_name} cannot be negative.[/bold red]")
137
+ return None
138
+
139
+ if max_duration and seconds > max_duration:
140
+ console.print(f"[bold red]{field_name} ({format_duration(seconds)}) exceeds video duration ({format_duration(max_duration)}).[/bold red]")
141
+ return None
142
+
143
+ return seconds
144
+
145
+
146
+ def validate_time_range(start_str, end_str, duration):
147
+ start = validate_time_input(start_str, duration, "Start time")
148
+ if start is None:
149
+ return None, None
150
+
151
+ end = validate_time_input(end_str, duration, "End time")
152
+ if end is None:
153
+ return None, None
154
+
155
+ if end <= start:
156
+ console.print("[bold red]End time must be greater than start time.[/bold red]")
157
+ return None, None
158
+
159
+ clip_duration = end - start
160
+ console.print(f"[dim]Clip duration: {format_duration(clip_duration)}[/dim]")
161
+
162
+ return start, end
163
+
164
+
165
+ def check_has_video_stream(file_path):
166
+ try:
167
+ probe = ffmpeg.probe(file_path, select_streams='v')
168
+ return len(probe.get('streams', [])) > 0
169
+ except Exception:
170
+ return False
171
+
172
+
173
+ def check_has_audio_stream(file_path):
174
+ try:
175
+ probe = ffmpeg.probe(file_path, select_streams='a')
176
+ return len(probe.get('streams', [])) > 0
177
+ except Exception:
178
+ return False
179
+
180
+
181
+ def warn_long_operation(duration, threshold=300, operation="This operation"):
182
+ if duration > threshold:
183
+ console.print(f"[yellow]Note: {operation} may take a while for this {format_duration(duration)} video.[/yellow]")
184
+ if not questionary.confirm("Continue?", default=True).ask():
185
+ return False
186
+ return True
187
+
188
+
189
+ def warn_reencode(operation="This operation"):
190
+ console.print(f"[dim]{operation} requires re-encoding and may take a while...[/dim]")
191
+
192
+
193
+ def handle_keyboard_interrupt():
194
+ console.print("\n[yellow]Operation cancelled by user.[/yellow]")
195
+
196
+
197
+ def generate_output_path(input_path, suffix, new_extension=None):
198
+ p = Path(input_path)
199
+ ext = new_extension if new_extension else p.suffix
200
+ return str(p.with_name(f"{p.stem}_{suffix}{ext}"))
201
+
202
+
203
+ def get_file_size_mb(file_path):
204
+ try:
205
+ size = os.path.getsize(file_path)
206
+ return size / (1024 * 1024)
207
+ except Exception:
208
+ return 0
209
+
210
+
211
+ def validate_positive_integer(value, field_name="Value"):
212
+ try:
213
+ num = int(value)
214
+ if num <= 0 and num != -1: # -1 is valid for "auto" in some contexts
215
+ console.print(f"[bold red]{field_name} must be a positive number.[/bold red]")
216
+ return None
217
+ return num
218
+ except ValueError:
219
+ console.print(f"[bold red]{field_name} must be a valid number.[/bold red]")
220
+ return None
221
+
222
+
223
+ def confirm_operation(message, default=True):
224
+ return questionary.confirm(message, default=default).ask()
225
+
226
+
227
+ def press_continue():
228
+ questionary.press_any_key_to_continue().ask()
@@ -0,0 +1,283 @@
1
+ Metadata-Version: 2.4
2
+ Name: peg_this
3
+ Version: 4.1.0
4
+ Summary: A powerful and intuitive command-line video editor, built on FFmpeg.
5
+ Author-email: Hariharen S S <thisishariharen@gmail.com>
6
+ Maintainer-email: Hariharen S S <thisishariharen@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/hariharen9/ffmpeg-this
9
+ Project-URL: Documentation, https://github.com/hariharen9/ffmpeg-this/blob/main/README.md
10
+ Project-URL: Repository, https://github.com/hariharen9/ffmpeg-this
11
+ Project-URL: Changelog, https://github.com/hariharen9/ffmpeg-this/releases
12
+ Project-URL: Bug Tracker, https://github.com/hariharen9/ffmpeg-this/issues
13
+ Project-URL: Funding, https://www.buymeacoffee.com/hariharen
14
+ Project-URL: Sponsor, https://github.com/sponsors/hariharen9
15
+ Keywords: ffmpeg,video,audio,media,converter,editor,cli,subtitles,whisper,transcription,trim,crop,compress,gif,batch
16
+ Classifier: Development Status :: 4 - Beta
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Intended Audience :: End Users/Desktop
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: License :: OSI Approved :: MIT License
27
+ Classifier: Operating System :: OS Independent
28
+ Classifier: Operating System :: MacOS
29
+ Classifier: Operating System :: POSIX :: Linux
30
+ Classifier: Operating System :: Microsoft :: Windows
31
+ Classifier: Topic :: Multimedia
32
+ Classifier: Topic :: Multimedia :: Video
33
+ Classifier: Topic :: Multimedia :: Video :: Conversion
34
+ Classifier: Topic :: Multimedia :: Video :: Non-Linear Editor
35
+ Classifier: Topic :: Multimedia :: Sound/Audio
36
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
37
+ Classifier: Topic :: Utilities
38
+ Requires-Python: >=3.8
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: ffmpeg-python==0.2.0
42
+ Requires-Dist: questionary>=2.0.0
43
+ Requires-Dist: rich>=13.0.0
44
+ Requires-Dist: Pillow>=9.0.0
45
+ Requires-Dist: faster-whisper>=1.0.0
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
48
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
49
+ Requires-Dist: black>=23.0.0; extra == "dev"
50
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
51
+ Dynamic: license-file
52
+
53
+ <h1 align="center">FFm<u><i>PEG</i></u>-this</h1>
54
+
55
+ <p align="center">
56
+ <a href="https://pypi.org/project/peg-this/">
57
+ <img src="https://img.shields.io/pypi/v/peg_this?color=blue&label=version" alt="PyPI Version">
58
+ </a>
59
+ <a href="https://pypi.org/project/peg-this/">
60
+ <img src="https://img.shields.io/pypi/pyversions/peg_this.svg" alt="PyPI Python Versions">
61
+ </a>
62
+ <a href="https://github.com/hariharen9/ffmpeg-this/blob/main/LICENSE">
63
+ <img src="https://img.shields.io/github/license/hariharen9/ffmpeg-this" alt="License">
64
+ </a>
65
+ <a href="https://pepy.tech/project/peg-this">
66
+ <img src="https://static.pepy.tech/badge/peg-this" alt="Downloads">
67
+ </a>
68
+ </p>
69
+
70
+ <p align="center"><b>Your Editor within CLI</b></p>
71
+
72
+ A powerful and user-friendly Python CLI tool for converting, manipulating, and inspecting media files using the power of FFmpeg. This tool provides a simple command-line menu to perform common audio and video tasks without needing to memorize complex FFmpeg commands.
73
+
74
+ <p align="center">
75
+ <img src="/assets/peg.gif" width="720">
76
+ </p>
77
+
78
+ ## Features at a Glance
79
+
80
+ | Category | Feature | Description |
81
+ |----------|---------|-------------|
82
+ | **Inspect** | Media Properties | View detailed codec, resolution, frame rate, bitrate, and stream information |
83
+ | **Convert** | Video Formats | Convert to MP4, MKV, MOV, AVI, WebM with quality presets (CRF 18/23/28) |
84
+ | | Audio Formats | Convert to MP3 (128k-320k bitrate), FLAC, WAV |
85
+ | | GIF Creation | Convert video clips to animated GIFs with optimized palette |
86
+ | | Image Formats | Convert between JPG, PNG, WebP, BMP, TIFF with quality control |
87
+ | **Subtitles** | AI Transcription | Generate subtitles using Whisper AI (7 model sizes available) |
88
+ | | Sidecar Export | Save as `.srt`, `.vtt`, `.txt`, or `.lrc` files |
89
+ | | Soft Subtitles | Embed toggleable subtitle track into video |
90
+ | | Hard Subtitles | Burn permanent subtitles directly into video |
91
+ | | Multi-language | Support for 99+ languages with auto-detection |
92
+ | **Edit** | Trim/Cut | Extract video segments by start/end time (lossless, no re-encoding) |
93
+ | | Visual Crop | Interactive GUI to select crop area on video/image |
94
+ | | Join/Concatenate | Merge multiple videos with automatic resolution matching |
95
+ | **Audio** | Extract Audio | Rip audio track to MP3, FLAC, or WAV |
96
+ | | Remove Audio | Create silent version of video (keeps video intact) |
97
+ | **Image** | Resize | Scale images with aspect ratio preservation |
98
+ | | Rotate | Rotate 90°, 180°, or 270° |
99
+ | | Flip | Flip horizontally or vertically |
100
+ | | Crop | Visual cropping with click-and-drag selection |
101
+ | **Batch** | Batch Convert | Convert all media files in directory at once |
102
+
103
+ ## Detailed Feature Breakdown
104
+
105
+ ### Video Operations
106
+
107
+ | Operation | Input | Output | Method | Re-encoding |
108
+ |-----------|-------|--------|--------|-------------|
109
+ | **Convert** | Any video | MP4, MKV, MOV, AVI, WebM | FFmpeg transcode | Yes (CRF quality) |
110
+ | **Trim** | Any video | Same format | Stream copy | No (lossless) |
111
+ | **Crop** | Any video | Same format | Visual selection + crop filter | Yes |
112
+ | **Join** | Multiple videos | Single MP4 | Concat filter + normalize | Yes |
113
+ | **To GIF** | Any video | Animated GIF | 2-pass palette optimization | Yes |
114
+
115
+ ### Audio Operations
116
+
117
+ | Operation | Input | Output | Notes |
118
+ |-----------|-------|--------|-------|
119
+ | **Extract** | Video with audio | MP3, FLAC, WAV | Preserves original quality for FLAC/WAV |
120
+ | **Remove** | Video with audio | Silent video | Stream copy (fast, no re-encoding) |
121
+ | **Convert** | Audio file | MP3, FLAC, WAV | Bitrate selection for MP3 |
122
+
123
+ ### Subtitle Generation
124
+
125
+ | Model | Size | Speed | Accuracy | Languages |
126
+ |-------|------|-------|----------|-----------|
127
+ | `tiny.en` | ~75 MB | Fastest | Good | English only |
128
+ | `base.en` | ~150 MB | Fast | Better | English only |
129
+ | `small.en` | ~500 MB | Balanced | Great | English only |
130
+ | `medium.en` | ~1.5 GB | Slower | Excellent | English only |
131
+ | `small` | ~500 MB | Balanced | Great | 99+ languages |
132
+ | `medium` | ~1.5 GB | Slower | Excellent | 99+ languages |
133
+ | `large-v3` | ~3 GB | Slowest | Best | 99+ languages |
134
+
135
+ **Output Options:**
136
+ | Type | File Extension | Description |
137
+ |------|----------------|-------------|
138
+ | Sidecar | `.srt` | SubRip - most compatible format |
139
+ | Sidecar | `.vtt` | WebVTT - for web/HTML5 players |
140
+ | Sidecar | `.txt` | Plain text transcript |
141
+ | Sidecar | `.lrc` | Lyrics format with timestamps |
142
+ | Soft Subs | `.mp4/.mkv` | Embedded, toggleable in players |
143
+ | Hard Subs | `.mp4/.mkv` | Burned in, always visible |
144
+
145
+ ### Image Operations
146
+
147
+ | Operation | Options | Notes |
148
+ |-----------|---------|-------|
149
+ | **Convert** | JPG, PNG, WebP, BMP, TIFF | Quality presets (95%, 80%, 60%) |
150
+ | **Resize** | Custom width/height | Use `-1` to preserve aspect ratio |
151
+ | **Rotate** | 90° CW, 90° CCW, 180° | Lossless rotation |
152
+ | **Flip** | Horizontal, Vertical | Mirror image |
153
+ | **Crop** | Visual selection | Interactive GUI with preview |
154
+
155
+ ### Supported Formats
156
+
157
+ | Type | Supported Formats |
158
+ |------|-------------------|
159
+ | **Video Input** | `.mp4`, `.mkv`, `.avi`, `.mov`, `.webm`, `.flv`, `.wmv`, `.gif` |
160
+ | **Video Output** | `.mp4`, `.mkv`, `.mov`, `.avi`, `.webm`, `.gif` |
161
+ | **Audio Input** | `.mp3`, `.flac`, `.wav`, `.ogg`, `.aac`, `.m4a` |
162
+ | **Audio Output** | `.mp3`, `.flac`, `.wav` |
163
+ | **Image Input** | `.jpg`, `.jpeg`, `.png`, `.webp`, `.bmp`, `.tiff` |
164
+ | **Image Output** | `.jpg`, `.png`, `.webp`, `.bmp`, `.tiff` |
165
+ | **Subtitle Output** | `.srt`, `.vtt`, `.txt`, `.lrc` |
166
+
167
+ ## Usage
168
+
169
+ ### Prerequisite: Install FFmpeg
170
+
171
+ > [!NOTE]
172
+ > `peg_this` uses a library called `ffmpeg-python` which acts as a controller for the main FFmpeg program. It does not include FFmpeg itself. Therefore, you must have FFmpeg installed on your system and available in your terminal's PATH.
173
+
174
+ For **macOS** users, the easiest way to install it is with [Homebrew](https://brew.sh/):
175
+ ```bash
176
+ brew install ffmpeg
177
+ ```
178
+
179
+ For **Windows** users, you can use a package manager like [Chocolatey](https://chocolatey.org/) or [Scoop](https://scoop.sh/):
180
+ ```bash
181
+ # Using Chocolatey
182
+ choco install ffmpeg
183
+
184
+ # Using Scoop
185
+ scoop install ffmpeg
186
+ ```
187
+
188
+ For other systems, please see the official download page: **[ffmpeg.org/download.html](https://ffmpeg.org/download.html)**
189
+
190
+ There are three ways to use `peg_this`:
191
+
192
+ ### 1. Pip Install (Recommended)
193
+ This is the easiest way to get started. This will install the tool and all its dependencies.
194
+
195
+ ```bash
196
+ pip install peg_this
197
+ ```
198
+
199
+ Once installed, you can run the tool from your terminal:
200
+
201
+ ```bash
202
+ peg_this
203
+ ```
204
+
205
+ ### 2. Download from Release
206
+ If you prefer not to install the package, you can download a pre-built executable from the [Releases](https://github.com/hariharen9/ffmpeg-this/releases/latest) page.
207
+
208
+ 1. Download the executable for your operating system (Windows, macOS, or Linux).
209
+ 2. Place it in a directory with your media files.
210
+ 3. Run the executable directly from your terminal.
211
+
212
+ ### 3. Run from Source
213
+ If you want to run the tool directly from the source code:
214
+
215
+ 1. **Clone the repository:**
216
+ ```bash
217
+ git clone https://github.com/hariharen9/ffmpeg-this.git
218
+ cd ffmpeg-this
219
+ ```
220
+ 2. **Install dependencies:**
221
+ ```bash
222
+ pip install -r requirements.txt
223
+ ```
224
+ 3. **Run the tool:**
225
+ ```bash
226
+ python -m src.peg_this.peg_this
227
+ ```
228
+
229
+ ## Subtitle Generation
230
+
231
+ The subtitle feature uses [faster-whisper](https://github.com/SYSTRAN/faster-whisper), a fast and accurate speech-to-text engine powered by OpenAI's Whisper model.
232
+
233
+ ### How it works
234
+
235
+ 1. Select a video file
236
+ 2. Choose "Generate Subtitles (Whisper)"
237
+ 3. Pick a model size (tiny to large-v3)
238
+ 4. Select processing mode (Fast or Accurate)
239
+ 5. Choose output type:
240
+ - **Sidecar file**: Export as `.srt`, `.vtt`, `.txt`, or `.lrc`
241
+ - **Soft subtitles**: Embed into video (can be toggled on/off in players)
242
+ - **Hard subtitles**: Burn into video (permanent, always visible)
243
+
244
+ ### Supported Languages
245
+
246
+ Using multilingual models (`small`, `medium`, `large-v3`), you can transcribe audio in 99+ languages including English, Spanish, French, German, Chinese, Japanese, Korean, Hindi, Arabic, and many more.
247
+
248
+ ## Star History
249
+
250
+ <p align="center">
251
+ <a href="https://star-history.com/#hariharen9/ffmpeg-this&Date">
252
+ <img src="https://api.star-history.com/svg?repos=hariharen9/ffmpeg-this&type=Date" alt="Star History Chart">
253
+ </a>
254
+ </p>
255
+
256
+ ## Sponsor
257
+
258
+ <p align="center">
259
+ <a href="https://github.com/sponsors/hariharen9">
260
+ <img src="https://img.shields.io/github/sponsors/hariharen9?style=for-the-badge&logo=github&color=white" alt="GitHub Sponsors">
261
+ </a>
262
+ <a href="https://www.buymeacoffee.com/hariharen">
263
+ <img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me a Coffee">
264
+ </a>
265
+ </p>
266
+
267
+ ## Contributors
268
+
269
+ <a href="https://github.com/hariharen9/ffmpeg-this/graphs/contributors">
270
+ <img src="https://contrib.rocks/image?repo=hariharen9/ffmpeg-this" />
271
+ </a>
272
+
273
+ ## Contributing
274
+
275
+ Contributions are welcome! Please see the [Contributing Guidelines](CONTRIBUTING.md) for more information.
276
+
277
+ ## License
278
+
279
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
280
+
281
+ <p align="center">
282
+ Made with ❤️ by <a href="https://hariharen.site">Hariharen</a>
283
+ </p>