1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
|
#!/usr/bin/env python3
"""
Video Clip Extractor GUI
A simple GUI application for extracting video clips using ffmpeg
"""
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import subprocess
import os
import threading
from pathlib import Path
import re
class VideoClipperGUI:
def __init__(self, root):
self.root = root
self.root.title("Video Clip Extractor")
self.root.geometry("600x400")
# Variables
self.input_file = tk.StringVar()
self.start_time = tk.StringVar(value="00:00:00")
self.end_time = tk.StringVar(value="00:00:10")
self.output_dir = tk.StringVar(value=os.getcwd())
self.setup_ui()
def setup_ui(self):
# Main frame
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# Input file selection
ttk.Label(main_frame, text="Input Video File:").grid(row=0, column=0, sticky=tk.W, pady=5)
file_frame = ttk.Frame(main_frame)
file_frame.grid(row=0, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5)
file_frame.columnconfigure(0, weight=1)
self.file_entry = ttk.Entry(file_frame, textvariable=self.input_file, state="readonly")
self.file_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
ttk.Button(file_frame, text="Browse", command=self.browse_file).grid(row=0, column=1)
# Time inputs
ttk.Label(main_frame, text="Start Time:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.start_entry = ttk.Entry(main_frame, textvariable=self.start_time, width=15)
self.start_entry.grid(row=1, column=1, sticky=tk.W, pady=5)
ttk.Label(main_frame, text="(HH:MM:SS or seconds)").grid(row=1, column=2, sticky=tk.W, padx=(5, 0))
ttk.Label(main_frame, text="End Time:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.end_entry = ttk.Entry(main_frame, textvariable=self.end_time, width=15)
self.end_entry.grid(row=2, column=1, sticky=tk.W, pady=5)
ttk.Label(main_frame, text="(HH:MM:SS or seconds)").grid(row=2, column=2, sticky=tk.W, padx=(5, 0))
# Output directory
ttk.Label(main_frame, text="Output Directory:").grid(row=3, column=0, sticky=tk.W, pady=5)
output_frame = ttk.Frame(main_frame)
output_frame.grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5)
output_frame.columnconfigure(0, weight=1)
self.output_entry = ttk.Entry(output_frame, textvariable=self.output_dir)
self.output_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
ttk.Button(output_frame, text="Browse", command=self.browse_output_dir).grid(row=0, column=1)
# Options frame
options_frame = ttk.LabelFrame(main_frame, text="Options", padding="5")
options_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
options_frame.columnconfigure(1, weight=1)
# Quality/encoding options
ttk.Label(options_frame, text="Quality:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
self.quality_var = tk.StringVar(value="copy")
quality_combo = ttk.Combobox(options_frame, textvariable=self.quality_var, width=15)
quality_combo['values'] = ("copy (fastest)", "high", "medium", "low")
quality_combo.grid(row=0, column=1, sticky=tk.W)
# Extract button
self.extract_btn = ttk.Button(main_frame, text="Extract Clip", command=self.extract_clip)
self.extract_btn.grid(row=5, column=0, columnspan=3, pady=20)
# Progress bar
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
# Status/output text
text_frame = ttk.LabelFrame(main_frame, text="Output", padding="5")
text_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
text_frame.columnconfigure(0, weight=1)
text_frame.rowconfigure(0, weight=1)
main_frame.rowconfigure(7, weight=1)
self.output_text = tk.Text(text_frame, height=8, wrap=tk.WORD)
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.output_text.yview)
self.output_text.configure(yscrollcommand=scrollbar.set)
self.output_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
# Initial message
self.log_message("Ready to extract video clips. Select a video file to begin.")
def browse_file(self):
"""Open file dialog to select input video file"""
filetypes = [
("Video files", "*.mp4 *.avi *.mkv *.mov *.wmv *.flv *.webm *.m4v"),
("All files", "*.*")
]
filename = filedialog.askopenfilename(
title="Select Video File",
filetypes=filetypes
)
if filename:
self.input_file.set(filename)
self.log_message(f"Selected input file: {os.path.basename(filename)}")
def browse_output_dir(self):
"""Open directory dialog to select output directory"""
directory = filedialog.askdirectory(title="Select Output Directory")
if directory:
self.output_dir.set(directory)
def validate_time_format(self, time_str):
"""Validate and normalize time format"""
time_str = time_str.strip()
# If it's just numbers, treat as seconds
if re.match(r'^\d+(\.\d+)?$', time_str):
return float(time_str)
# If it's HH:MM:SS format
if re.match(r'^\d{1,2}:\d{2}:\d{2}(\.\d+)?$', time_str):
return time_str
# If it's MM:SS format, convert to HH:MM:SS
if re.match(r'^\d{1,2}:\d{2}(\.\d+)?$', time_str):
return f"00:{time_str}"
raise ValueError(f"Invalid time format: {time_str}")
def log_message(self, message):
"""Add message to output text widget"""
self.output_text.insert(tk.END, f"{message}\n")
self.output_text.see(tk.END)
self.root.update_idletasks()
def get_ffmpeg_quality_params(self):
"""Get ffmpeg parameters based on quality setting"""
quality = self.quality_var.get()
if "copy" in quality:
return ["-c", "copy"]
elif "high" in quality:
return ["-c:v", "libx264", "-crf", "18", "-c:a", "aac", "-b:a", "192k"]
elif "medium" in quality:
return ["-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-b:a", "128k"]
elif "low" in quality:
return ["-c:v", "libx264", "-crf", "28", "-c:a", "aac", "-b:a", "96k"]
else:
return ["-c", "copy"]
def extract_clip(self):
"""Extract video clip using ffmpeg"""
# Validation
if not self.input_file.get():
messagebox.showerror("Error", "Please select an input video file")
return
if not os.path.isfile(self.input_file.get()):
messagebox.showerror("Error", "Input file does not exist")
return
try:
start_time = self.validate_time_format(self.start_time.get())
end_time = self.validate_time_format(self.end_time.get())
except ValueError as e:
messagebox.showerror("Error", str(e))
return
# Disable extract button and start progress
self.extract_btn.config(state="disabled")
self.progress.start()
# Run extraction in separate thread
thread = threading.Thread(target=self._run_extraction, args=(start_time, end_time))
thread.daemon = True
thread.start()
def _run_extraction(self, start_time, end_time):
"""Run ffmpeg extraction in background thread"""
try:
# Generate output filename
input_path = Path(self.input_file.get())
timestamp = str(int(os.path.getctime(self.input_file.get())))
output_filename = f"{input_path.stem}_clip_{timestamp}{input_path.suffix}"
output_path = os.path.join(self.output_dir.get(), output_filename)
# Build ffmpeg command
cmd = ["ffmpeg", "-i", self.input_file.get()]
# Add start time
if isinstance(start_time, (int, float)) and start_time > 0:
cmd.extend(["-ss", str(start_time)])
elif isinstance(start_time, str) and start_time != "00:00:00":
cmd.extend(["-ss", start_time])
# Add duration or end time
if isinstance(end_time, (int, float)) and isinstance(start_time, (int, float)):
duration = end_time - start_time
cmd.extend(["-t", str(duration)])
elif isinstance(end_time, str):
cmd.extend(["-to", end_time])
# Add quality parameters
cmd.extend(self.get_ffmpeg_quality_params())
# Add output file
cmd.append(output_path)
self.log_message(f"Starting extraction...")
self.log_message(f"Command: {' '.join(cmd)}")
# Run ffmpeg
process = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=self.output_dir.get()
)
# Handle results
if process.returncode == 0:
self.log_message(f"✓ Successfully extracted clip to: {output_filename}")
self.log_message(f"Output file size: {self._get_file_size(output_path)}")
else:
self.log_message(f"✗ Error during extraction:")
self.log_message(process.stderr)
except Exception as e:
self.log_message(f"✗ Exception occurred: {str(e)}")
finally:
# Re-enable button and stop progress
self.root.after(0, self._extraction_finished)
def _extraction_finished(self):
"""Called when extraction is finished"""
self.progress.stop()
self.extract_btn.config(state="normal")
def _get_file_size(self, filepath):
"""Get human-readable file size"""
try:
size = os.path.getsize(filepath)
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
except:
return "Unknown"
def main():
# Check if ffmpeg is available
try:
subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
messagebox.showerror("Error", "ffmpeg not found. Please install ffmpeg and ensure it's in your PATH.")
return
root = tk.Tk()
app = VideoClipperGUI(root)
root.mainloop()
if __name__ == "__main__":
main()
|