talks-reducer 0.2.24__tar.gz → 0.3.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.
- {talks_reducer-0.2.24/talks_reducer.egg-info → talks_reducer-0.3.0}/PKG-INFO +7 -1
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/README.md +6 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/pyproject.toml +1 -1
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/ffmpeg.py +13 -1
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/gui.py +451 -72
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/pipeline.py +5 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0/talks_reducer.egg-info}/PKG-INFO +7 -1
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/LICENSE +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/setup.cfg +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/__init__.py +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/__main__.py +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/audio.py +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/chunks.py +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/cli.py +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/models.py +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer/progress.py +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer.egg-info/SOURCES.txt +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer.egg-info/dependency_links.txt +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer.egg-info/entry_points.txt +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer.egg-info/requires.txt +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/talks_reducer.egg-info/top_level.txt +0 -0
- {talks_reducer-0.2.24 → talks_reducer-0.3.0}/tests/test_pipeline_service.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: talks-reducer
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: CLI for speeding up long-form talks by removing silence
|
5
5
|
Author: Talks Reducer Maintainers
|
6
6
|
License-Expression: MIT
|
@@ -85,6 +85,8 @@ continues to work unchanged for local development.
|
|
85
85
|
- **Input drop zone** — drag files or folders from your desktop or add them via
|
86
86
|
the Explorer/Finder dialog; duplicates are ignored.
|
87
87
|
- **Small video** — toggles the `--small` preset used by the CLI.
|
88
|
+
- **Open after convert** — controls whether the exported file is revealed in
|
89
|
+
your system file manager as soon as each job finishes.
|
88
90
|
- **Advanced** — reveals optional controls for the output path, temp folder,
|
89
91
|
timing/audio knobs mirrored from the command line, and an appearance picker
|
90
92
|
that can force dark or light mode or follow your operating system.
|
@@ -94,6 +96,10 @@ a background thread. Once every queued job succeeds an **Open last output**
|
|
94
96
|
button appears so you can jump straight to the exported file in your system
|
95
97
|
file manager.
|
96
98
|
|
99
|
+
The GUI stores your last-used Simple mode, Small video, Open after convert, and
|
100
|
+
theme preferences in a cross-platform configuration file so they persist across
|
101
|
+
launches.
|
102
|
+
|
97
103
|
## Repository Structure
|
98
104
|
- `talks_reducer/` — Python package that exposes the CLI and reusable pipeline:
|
99
105
|
- `cli.py` parses arguments and dispatches to the pipeline.
|
@@ -60,6 +60,8 @@ continues to work unchanged for local development.
|
|
60
60
|
- **Input drop zone** — drag files or folders from your desktop or add them via
|
61
61
|
the Explorer/Finder dialog; duplicates are ignored.
|
62
62
|
- **Small video** — toggles the `--small` preset used by the CLI.
|
63
|
+
- **Open after convert** — controls whether the exported file is revealed in
|
64
|
+
your system file manager as soon as each job finishes.
|
63
65
|
- **Advanced** — reveals optional controls for the output path, temp folder,
|
64
66
|
timing/audio knobs mirrored from the command line, and an appearance picker
|
65
67
|
that can force dark or light mode or follow your operating system.
|
@@ -69,6 +71,10 @@ a background thread. Once every queued job succeeds an **Open last output**
|
|
69
71
|
button appears so you can jump straight to the exported file in your system
|
70
72
|
file manager.
|
71
73
|
|
74
|
+
The GUI stores your last-used Simple mode, Small video, Open after convert, and
|
75
|
+
theme preferences in a cross-platform configuration file so they persist across
|
76
|
+
launches.
|
77
|
+
|
72
78
|
## Repository Structure
|
73
79
|
- `talks_reducer/` — Python package that exposes the CLI and reusable pipeline:
|
74
80
|
- `cli.py` parses arguments and dispatches to the pipeline.
|
@@ -190,8 +190,13 @@ def run_timed_ffmpeg_command(
|
|
190
190
|
desc: str = "",
|
191
191
|
total: Optional[int] = None,
|
192
192
|
unit: str = "frames",
|
193
|
+
process_callback: Optional[callable] = None,
|
193
194
|
) -> None:
|
194
|
-
"""Execute an FFmpeg command while streaming progress information.
|
195
|
+
"""Execute an FFmpeg command while streaming progress information.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
process_callback: Optional callback that receives the subprocess.Popen object
|
199
|
+
"""
|
195
200
|
|
196
201
|
import shlex
|
197
202
|
|
@@ -221,6 +226,10 @@ def run_timed_ffmpeg_command(
|
|
221
226
|
print(f"Error starting FFmpeg: {exc}", file=sys.stderr)
|
222
227
|
raise
|
223
228
|
|
229
|
+
# Notify callback with process object
|
230
|
+
if process_callback:
|
231
|
+
process_callback(process)
|
232
|
+
|
224
233
|
progress_reporter = reporter or TqdmProgressReporter()
|
225
234
|
task_manager = progress_reporter.task(desc=desc, total=total, unit=unit)
|
226
235
|
with task_manager as progress:
|
@@ -234,6 +243,9 @@ def run_timed_ffmpeg_command(
|
|
234
243
|
|
235
244
|
sys.stderr.write(line)
|
236
245
|
sys.stderr.flush()
|
246
|
+
|
247
|
+
# Send FFmpeg output to reporter for GUI display
|
248
|
+
progress_reporter.log(line.strip())
|
237
249
|
|
238
250
|
match = re.search(r"frame=\s*(\d+)", line)
|
239
251
|
if match:
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import json
|
6
6
|
import os
|
7
|
+
import re
|
7
8
|
import subprocess
|
8
9
|
import sys
|
9
10
|
import threading
|
@@ -117,9 +118,10 @@ except ModuleNotFoundError: # pragma: no cover - runtime dependency
|
|
117
118
|
|
118
119
|
STATUS_COLORS = {
|
119
120
|
"idle": "#9ca3af",
|
120
|
-
"processing": "#
|
121
|
-
"success": "#
|
122
|
-
"error": "#
|
121
|
+
"processing": "#af8e0e",
|
122
|
+
"success": "#178941",
|
123
|
+
"error": "#ad4f4f",
|
124
|
+
"aborted": "#6d727a",
|
123
125
|
}
|
124
126
|
|
125
127
|
LIGHT_THEME = {
|
@@ -128,7 +130,8 @@ LIGHT_THEME = {
|
|
128
130
|
"accent": "#2563eb",
|
129
131
|
"surface": "#ffffff",
|
130
132
|
"border": "#cbd5e1",
|
131
|
-
"hover": "#
|
133
|
+
"hover": "#efefef",
|
134
|
+
"hover_text": "#000000",
|
132
135
|
"selection_background": "#2563eb",
|
133
136
|
"selection_foreground": "#ffffff",
|
134
137
|
}
|
@@ -140,6 +143,7 @@ DARK_THEME = {
|
|
140
143
|
"surface": "#2b2b3c",
|
141
144
|
"border": "#4b5563",
|
142
145
|
"hover": "#333333",
|
146
|
+
"hover_text": "#ffffff",
|
143
147
|
"selection_background": "#333333",
|
144
148
|
"selection_foreground": "#f3f4f6",
|
145
149
|
}
|
@@ -186,8 +190,13 @@ class _GuiProgressHandle(ProgressHandle):
|
|
186
190
|
class _TkProgressReporter(SignalProgressReporter):
|
187
191
|
"""Progress reporter that forwards updates to the GUI thread."""
|
188
192
|
|
189
|
-
def __init__(
|
193
|
+
def __init__(
|
194
|
+
self,
|
195
|
+
log_callback: Callable[[str], None],
|
196
|
+
process_callback: Optional[Callable] = None,
|
197
|
+
) -> None:
|
190
198
|
self._log_callback = log_callback
|
199
|
+
self.process_callback = process_callback
|
191
200
|
|
192
201
|
def log(self, message: str) -> None:
|
193
202
|
self._log_callback(message)
|
@@ -202,7 +211,55 @@ class _TkProgressReporter(SignalProgressReporter):
|
|
202
211
|
class TalksReducerGUI:
|
203
212
|
"""Tkinter application mirroring the CLI options with form controls."""
|
204
213
|
|
214
|
+
PADDING = 10
|
215
|
+
|
216
|
+
def _determine_config_path(self) -> Path:
|
217
|
+
if sys.platform == "win32":
|
218
|
+
appdata = os.environ.get("APPDATA")
|
219
|
+
base = Path(appdata) if appdata else Path.home() / "AppData" / "Roaming"
|
220
|
+
elif sys.platform == "darwin":
|
221
|
+
base = Path.home() / "Library" / "Application Support"
|
222
|
+
else:
|
223
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME")
|
224
|
+
base = Path(xdg_config) if xdg_config else Path.home() / ".config"
|
225
|
+
return base / "talks-reducer" / "settings.json"
|
226
|
+
|
227
|
+
def _load_settings(self) -> dict[str, object]:
|
228
|
+
try:
|
229
|
+
with self._config_path.open("r", encoding="utf-8") as handle:
|
230
|
+
data = json.load(handle)
|
231
|
+
if isinstance(data, dict):
|
232
|
+
return data
|
233
|
+
except FileNotFoundError:
|
234
|
+
return {}
|
235
|
+
except (OSError, json.JSONDecodeError):
|
236
|
+
return {}
|
237
|
+
return {}
|
238
|
+
|
239
|
+
def _save_settings(self) -> None:
|
240
|
+
try:
|
241
|
+
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
242
|
+
with self._config_path.open("w", encoding="utf-8") as handle:
|
243
|
+
json.dump(self._settings, handle, indent=2, sort_keys=True)
|
244
|
+
except OSError:
|
245
|
+
pass
|
246
|
+
|
247
|
+
def _get_setting(self, key: str, default: object) -> object:
|
248
|
+
value = self._settings.get(key, default)
|
249
|
+
if key not in self._settings:
|
250
|
+
self._settings[key] = value
|
251
|
+
return value
|
252
|
+
|
253
|
+
def _update_setting(self, key: str, value: object) -> None:
|
254
|
+
if self._settings.get(key) == value:
|
255
|
+
return
|
256
|
+
self._settings[key] = value
|
257
|
+
self._save_settings()
|
258
|
+
|
205
259
|
def __init__(self) -> None:
|
260
|
+
self._config_path = self._determine_config_path()
|
261
|
+
self._settings = self._load_settings()
|
262
|
+
|
206
263
|
# Import tkinter here to avoid loading it at module import time
|
207
264
|
import tkinter as tk
|
208
265
|
from tkinter import filedialog, messagebox, ttk
|
@@ -217,18 +274,18 @@ class TalksReducerGUI:
|
|
217
274
|
self.root = TkinterDnD.Tk() # type: ignore[call-arg]
|
218
275
|
else:
|
219
276
|
self.root = tk.Tk()
|
220
|
-
|
277
|
+
|
221
278
|
# Set window title with version
|
222
279
|
try:
|
223
280
|
app_version = version("talks-reducer")
|
224
281
|
self.root.title(f"Talks Reducer v{app_version}")
|
225
282
|
except Exception:
|
226
283
|
self.root.title("Talks Reducer")
|
227
|
-
|
284
|
+
|
228
285
|
self._apply_window_icon()
|
229
286
|
|
230
287
|
self._full_size = (760, 680)
|
231
|
-
self._simple_size = (255,
|
288
|
+
self._simple_size = (255, 330)
|
232
289
|
self.root.geometry(f"{self._full_size[0]}x{self._full_size[1]}")
|
233
290
|
self.style = self.ttk.Style(self.root)
|
234
291
|
|
@@ -238,21 +295,36 @@ class TalksReducerGUI:
|
|
238
295
|
self.status_var = tk.StringVar(value=self._status_state)
|
239
296
|
self._status_animation_job: Optional[str] = None
|
240
297
|
self._status_animation_phase = 0
|
298
|
+
self._video_duration_seconds: Optional[float] = None
|
299
|
+
self.progress_var = tk.IntVar(value=0)
|
300
|
+
self._ffmpeg_process: Optional[subprocess.Popen] = None
|
301
|
+
self._stop_requested = False
|
241
302
|
|
242
303
|
self.input_files: List[str] = []
|
243
304
|
|
244
305
|
self._dnd_available = TkinterDnD is not None and DND_FILES is not None
|
245
306
|
|
246
|
-
self.simple_mode_var = tk.BooleanVar(
|
307
|
+
self.simple_mode_var = tk.BooleanVar(
|
308
|
+
value=self._get_setting("simple_mode", True)
|
309
|
+
)
|
247
310
|
self.run_after_drop_var = tk.BooleanVar(value=True)
|
248
|
-
self.small_var = tk.BooleanVar(value=True)
|
249
|
-
self.
|
311
|
+
self.small_var = tk.BooleanVar(value=self._get_setting("small_video", True))
|
312
|
+
self.open_after_convert_var = tk.BooleanVar(
|
313
|
+
value=self._get_setting("open_after_convert", True)
|
314
|
+
)
|
315
|
+
self.theme_var = tk.StringVar(value=self._get_setting("theme", "os"))
|
250
316
|
self.theme_var.trace_add("write", self._on_theme_change)
|
317
|
+
self.small_var.trace_add("write", self._on_small_video_change)
|
318
|
+
self.open_after_convert_var.trace_add(
|
319
|
+
"write", self._on_open_after_convert_change
|
320
|
+
)
|
251
321
|
|
252
322
|
self._build_layout()
|
253
323
|
self._apply_simple_mode(initial=True)
|
254
324
|
self._apply_status_style(self._status_state)
|
255
325
|
self._apply_theme()
|
326
|
+
self._save_settings()
|
327
|
+
self._hide_stop_button()
|
256
328
|
|
257
329
|
if not self._dnd_available:
|
258
330
|
self._append_log(
|
@@ -289,16 +361,17 @@ class TalksReducerGUI:
|
|
289
361
|
continue
|
290
362
|
|
291
363
|
def _build_layout(self) -> None:
|
292
|
-
main = self.ttk.Frame(self.root, padding=
|
364
|
+
main = self.ttk.Frame(self.root, padding=self.PADDING)
|
293
365
|
main.grid(row=0, column=0, sticky="nsew")
|
294
366
|
self.root.columnconfigure(0, weight=1)
|
295
367
|
self.root.rowconfigure(0, weight=1)
|
296
368
|
|
297
369
|
# Input selection frame
|
298
|
-
input_frame = self.ttk.
|
370
|
+
input_frame = self.ttk.Frame(main, padding=self.PADDING)
|
299
371
|
input_frame.grid(row=0, column=0, sticky="nsew")
|
300
372
|
main.rowconfigure(0, weight=1)
|
301
|
-
|
373
|
+
main.columnconfigure(0, weight=1)
|
374
|
+
for column in range(5):
|
302
375
|
input_frame.columnconfigure(column, weight=1)
|
303
376
|
|
304
377
|
self.input_list = self.tk.Listbox(input_frame, height=5)
|
@@ -312,13 +385,13 @@ class TalksReducerGUI:
|
|
312
385
|
self.drop_zone = self.tk.Label(
|
313
386
|
input_frame,
|
314
387
|
text="Drop files or folders here",
|
315
|
-
relief=self.tk.
|
316
|
-
borderwidth=
|
317
|
-
padx=
|
318
|
-
pady=
|
319
|
-
highlightthickness=
|
388
|
+
relief=self.tk.FLAT,
|
389
|
+
borderwidth=0,
|
390
|
+
padx=self.PADDING,
|
391
|
+
pady=self.PADDING,
|
392
|
+
highlightthickness=0,
|
320
393
|
)
|
321
|
-
self.drop_zone.grid(row=1, column=0, columnspan=
|
394
|
+
self.drop_zone.grid(row=1, column=0, columnspan=5, sticky="nsew")
|
322
395
|
input_frame.rowconfigure(1, weight=1)
|
323
396
|
self._configure_drop_targets(self.drop_zone)
|
324
397
|
self._configure_drop_targets(self.input_list)
|
@@ -343,8 +416,8 @@ class TalksReducerGUI:
|
|
343
416
|
self.run_after_drop_check.grid(row=2, column=3, pady=8, sticky="e")
|
344
417
|
|
345
418
|
# Options frame
|
346
|
-
options = self.ttk.
|
347
|
-
options.grid(row=
|
419
|
+
options = self.ttk.Frame(main, padding=self.PADDING)
|
420
|
+
options.grid(row=2, column=0, pady=(16, 0), sticky="ew")
|
348
421
|
options.columnconfigure(0, weight=1)
|
349
422
|
|
350
423
|
self.simple_mode_check = self.ttk.Checkbutton(
|
@@ -359,6 +432,12 @@ class TalksReducerGUI:
|
|
359
432
|
row=1, column=0, sticky="w", pady=(8, 0)
|
360
433
|
)
|
361
434
|
|
435
|
+
self.ttk.Checkbutton(
|
436
|
+
options,
|
437
|
+
text="Open after convert",
|
438
|
+
variable=self.open_after_convert_var,
|
439
|
+
).grid(row=2, column=0, sticky="w", pady=(8, 0))
|
440
|
+
|
362
441
|
self.advanced_visible = self.tk.BooleanVar(value=False)
|
363
442
|
self.advanced_button = self.ttk.Button(
|
364
443
|
options,
|
@@ -367,8 +446,8 @@ class TalksReducerGUI:
|
|
367
446
|
)
|
368
447
|
self.advanced_button.grid(row=0, column=1, sticky="e")
|
369
448
|
|
370
|
-
self.advanced_frame = self.ttk.Frame(options, padding=
|
371
|
-
self.advanced_frame.grid(row=
|
449
|
+
self.advanced_frame = self.ttk.Frame(options, padding=self.PADDING)
|
450
|
+
self.advanced_frame.grid(row=3, column=0, columnspan=2, sticky="nsew")
|
372
451
|
self.advanced_frame.columnconfigure(1, weight=1)
|
373
452
|
|
374
453
|
self.output_var = self.tk.StringVar()
|
@@ -424,33 +503,54 @@ class TalksReducerGUI:
|
|
424
503
|
self._toggle_advanced(initial=True)
|
425
504
|
|
426
505
|
# Action buttons and log output
|
427
|
-
|
428
|
-
|
429
|
-
|
506
|
+
status_frame = self.ttk.Frame(main, padding=self.PADDING)
|
507
|
+
status_frame.grid(row=1, column=0, sticky="ew")
|
508
|
+
status_frame.columnconfigure(0, weight=0)
|
509
|
+
status_frame.columnconfigure(1, weight=1)
|
510
|
+
status_frame.columnconfigure(2, weight=0)
|
511
|
+
|
512
|
+
|
513
|
+
self.ttk.Label(status_frame, text="Status:").grid(row=0, column=0, sticky="w")
|
514
|
+
self.status_label = self.tk.Label(status_frame, textvariable=self.status_var, anchor="e")
|
515
|
+
self.status_label.grid(row=0, column=1, sticky="e")
|
516
|
+
|
517
|
+
# Progress bar
|
518
|
+
self.progress_bar = self.ttk.Progressbar(
|
519
|
+
status_frame,
|
520
|
+
variable=self.progress_var,
|
521
|
+
maximum=100,
|
522
|
+
mode="determinate",
|
523
|
+
style="Idle.Horizontal.TProgressbar",
|
524
|
+
)
|
525
|
+
self.progress_bar.grid(row=1, column=0, columnspan=3, sticky="ew", pady=(0, 0))
|
430
526
|
|
431
|
-
self.
|
432
|
-
|
527
|
+
self.stop_button = self.ttk.Button(
|
528
|
+
status_frame, text="Stop", command=self._stop_processing
|
433
529
|
)
|
434
|
-
self.
|
530
|
+
self.stop_button.grid(row=2, column=0, columnspan=3, sticky="ew", pady=self.PADDING)
|
531
|
+
self.stop_button.grid_remove() # Hidden by default
|
435
532
|
|
436
533
|
self.open_button = self.ttk.Button(
|
437
|
-
|
438
|
-
text="Open last
|
534
|
+
status_frame,
|
535
|
+
text="Open last",
|
439
536
|
command=self._open_last_output,
|
440
537
|
state=self.tk.DISABLED,
|
441
538
|
)
|
442
|
-
self.open_button.grid(row=
|
539
|
+
self.open_button.grid(row=2, column=0, columnspan=3, sticky="ew", pady=self.PADDING)
|
443
540
|
self.open_button.grid_remove()
|
444
541
|
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
542
|
+
# Button shown when no other action buttons are visible
|
543
|
+
self.drop_hint_button = self.ttk.Button(
|
544
|
+
status_frame,
|
545
|
+
text="Drop video to convert",
|
546
|
+
state=self.tk.DISABLED,
|
547
|
+
)
|
548
|
+
self.drop_hint_button.grid(row=2, column=0, columnspan=3, sticky="ew", pady=self.PADDING)
|
549
|
+
self.drop_hint_button.grid_remove() # Hidden by default
|
550
|
+
self._configure_drop_targets(self.drop_hint_button)
|
451
551
|
|
452
|
-
self.log_frame = self.ttk.
|
453
|
-
self.log_frame.grid(row=
|
552
|
+
self.log_frame = self.ttk.Frame(main, padding=self.PADDING)
|
553
|
+
self.log_frame.grid(row=3, column=0, pady=(16, 0), sticky="nsew")
|
454
554
|
main.rowconfigure(4, weight=1)
|
455
555
|
self.log_frame.columnconfigure(0, weight=1)
|
456
556
|
self.log_frame.rowconfigure(0, weight=1)
|
@@ -486,6 +586,7 @@ class TalksReducerGUI:
|
|
486
586
|
button.grid(row=row, column=2, padx=(8, 0))
|
487
587
|
|
488
588
|
def _toggle_simple_mode(self) -> None:
|
589
|
+
self._update_setting("simple_mode", self.simple_mode_var.get())
|
489
590
|
self._apply_simple_mode()
|
490
591
|
|
491
592
|
def _apply_simple_mode(self, *, initial: bool = False) -> None:
|
@@ -503,21 +604,23 @@ class TalksReducerGUI:
|
|
503
604
|
for widget in widgets:
|
504
605
|
widget.grid_remove()
|
505
606
|
self.log_frame.grid_remove()
|
506
|
-
self.
|
607
|
+
self.stop_button.grid_remove()
|
507
608
|
self.advanced_button.grid_remove()
|
508
609
|
self.advanced_frame.grid_remove()
|
509
|
-
self
|
610
|
+
if hasattr(self, 'status_frame'):
|
611
|
+
self.status_frame.grid_remove()
|
510
612
|
self.run_after_drop_var.set(True)
|
511
613
|
self._apply_window_size(simple=True)
|
512
|
-
if self.status_var.get().lower() == "success":
|
513
|
-
self.
|
614
|
+
if self.status_var.get().lower() == "success" and hasattr(self, 'status_frame'):
|
615
|
+
self.status_frame.grid()
|
514
616
|
self.open_button.grid()
|
617
|
+
self.drop_hint_button.grid_remove()
|
515
618
|
else:
|
516
619
|
for widget in widgets:
|
517
620
|
widget.grid()
|
518
621
|
self.log_frame.grid()
|
519
|
-
self
|
520
|
-
|
622
|
+
if hasattr(self, 'status_frame'):
|
623
|
+
self.status_frame.grid()
|
521
624
|
self.advanced_button.grid()
|
522
625
|
if self.advanced_visible.get():
|
523
626
|
self.advanced_frame.grid()
|
@@ -551,8 +654,17 @@ class TalksReducerGUI:
|
|
551
654
|
self.advanced_button.configure(text="Advanced")
|
552
655
|
|
553
656
|
def _on_theme_change(self, *_: object) -> None:
|
657
|
+
self._update_setting("theme", self.theme_var.get())
|
554
658
|
self._apply_theme()
|
555
659
|
|
660
|
+
def _on_small_video_change(self, *_: object) -> None:
|
661
|
+
self._update_setting("small_video", bool(self.small_var.get()))
|
662
|
+
|
663
|
+
def _on_open_after_convert_change(self, *_: object) -> None:
|
664
|
+
self._update_setting(
|
665
|
+
"open_after_convert", bool(self.open_after_convert_var.get())
|
666
|
+
)
|
667
|
+
|
556
668
|
def _apply_theme(self) -> None:
|
557
669
|
preference = self.theme_var.get().lower()
|
558
670
|
if preference not in {"light", "dark"}:
|
@@ -572,6 +684,8 @@ class TalksReducerGUI:
|
|
572
684
|
"TLabelframe",
|
573
685
|
background=palette["background"],
|
574
686
|
foreground=palette["foreground"],
|
687
|
+
borderwidth=0,
|
688
|
+
relief="flat",
|
575
689
|
)
|
576
690
|
self.style.configure(
|
577
691
|
"TLabelframe.Label",
|
@@ -586,11 +700,19 @@ class TalksReducerGUI:
|
|
586
700
|
background=palette["background"],
|
587
701
|
foreground=palette["foreground"],
|
588
702
|
)
|
703
|
+
self.style.map(
|
704
|
+
"TCheckbutton",
|
705
|
+
background=[("active", palette.get("hover", palette["background"]))],
|
706
|
+
)
|
589
707
|
self.style.configure(
|
590
708
|
"TRadiobutton",
|
591
709
|
background=palette["background"],
|
592
710
|
foreground=palette["foreground"],
|
593
711
|
)
|
712
|
+
self.style.map(
|
713
|
+
"TRadiobutton",
|
714
|
+
background=[("active", palette.get("hover", palette["background"]))],
|
715
|
+
)
|
594
716
|
self.style.configure(
|
595
717
|
"TButton",
|
596
718
|
background=palette["surface"],
|
@@ -604,7 +726,7 @@ class TalksReducerGUI:
|
|
604
726
|
("disabled", palette["surface"]),
|
605
727
|
],
|
606
728
|
foreground=[
|
607
|
-
("active", palette
|
729
|
+
("active", palette.get("hover_text", "#000000")),
|
608
730
|
("disabled", palette["foreground"]),
|
609
731
|
],
|
610
732
|
)
|
@@ -619,11 +741,47 @@ class TalksReducerGUI:
|
|
619
741
|
foreground=palette["foreground"],
|
620
742
|
)
|
621
743
|
|
744
|
+
# Configure progress bar styles for different states
|
745
|
+
self.style.configure(
|
746
|
+
"Idle.Horizontal.TProgressbar",
|
747
|
+
background=STATUS_COLORS["idle"],
|
748
|
+
troughcolor=palette["surface"],
|
749
|
+
borderwidth=0,
|
750
|
+
thickness=20,
|
751
|
+
)
|
752
|
+
self.style.configure(
|
753
|
+
"Processing.Horizontal.TProgressbar",
|
754
|
+
background=STATUS_COLORS["processing"],
|
755
|
+
troughcolor=palette["surface"],
|
756
|
+
borderwidth=0,
|
757
|
+
thickness=20,
|
758
|
+
)
|
759
|
+
self.style.configure(
|
760
|
+
"Success.Horizontal.TProgressbar",
|
761
|
+
background=STATUS_COLORS["success"],
|
762
|
+
troughcolor=palette["surface"],
|
763
|
+
borderwidth=0,
|
764
|
+
thickness=20,
|
765
|
+
)
|
766
|
+
self.style.configure(
|
767
|
+
"Error.Horizontal.TProgressbar",
|
768
|
+
background=STATUS_COLORS["error"],
|
769
|
+
troughcolor=palette["surface"],
|
770
|
+
borderwidth=0,
|
771
|
+
thickness=20,
|
772
|
+
)
|
773
|
+
self.style.configure(
|
774
|
+
"Aborted.Horizontal.TProgressbar",
|
775
|
+
background=STATUS_COLORS["aborted"],
|
776
|
+
troughcolor=palette["surface"],
|
777
|
+
borderwidth=0,
|
778
|
+
thickness=20,
|
779
|
+
)
|
780
|
+
|
622
781
|
self.drop_zone.configure(
|
623
782
|
bg=palette["surface"],
|
624
783
|
fg=palette["foreground"],
|
625
|
-
|
626
|
-
highlightcolor=palette["border"],
|
784
|
+
highlightthickness=0,
|
627
785
|
)
|
628
786
|
self.input_list.configure(
|
629
787
|
bg=palette["surface"],
|
@@ -714,12 +872,20 @@ class TalksReducerGUI:
|
|
714
872
|
self.input_list.delete(index)
|
715
873
|
del self.input_files[index]
|
716
874
|
|
875
|
+
def _clear_input_files(self) -> None:
|
876
|
+
"""Clear all input files from the list."""
|
877
|
+
self.input_files.clear()
|
878
|
+
self.input_list.delete(0, self.tk.END)
|
879
|
+
|
717
880
|
def _on_drop(self, event: object) -> None:
|
718
881
|
data = getattr(event, "data", "")
|
719
882
|
if not data:
|
720
883
|
return
|
721
884
|
paths = self.root.tk.splitlist(data)
|
722
885
|
cleaned = [path.strip("{}") for path in paths]
|
886
|
+
# Clear existing files before adding dropped files
|
887
|
+
self.input_files.clear()
|
888
|
+
self.input_list.delete(0, self.tk.END)
|
723
889
|
self._extend_inputs(cleaned, auto_run=True)
|
724
890
|
|
725
891
|
def _browse_path(
|
@@ -753,10 +919,16 @@ class TalksReducerGUI:
|
|
753
919
|
return
|
754
920
|
|
755
921
|
self._append_log("Starting processing…")
|
756
|
-
self.
|
922
|
+
self._stop_requested = False
|
923
|
+
open_after_convert = bool(self.open_after_convert_var.get())
|
757
924
|
|
758
925
|
def worker() -> None:
|
759
|
-
|
926
|
+
def set_process(proc: subprocess.Popen) -> None:
|
927
|
+
self._ffmpeg_process = proc
|
928
|
+
|
929
|
+
reporter = _TkProgressReporter(
|
930
|
+
self._append_log, process_callback=set_process
|
931
|
+
)
|
760
932
|
try:
|
761
933
|
files = gather_input_files(self.input_files)
|
762
934
|
if not files:
|
@@ -776,30 +948,75 @@ class TalksReducerGUI:
|
|
776
948
|
result = speed_up_video(options, reporter=reporter)
|
777
949
|
self._last_output = result.output_file
|
778
950
|
self._append_log(f"Completed: {result.output_file}")
|
779
|
-
|
780
|
-
|
781
|
-
|
951
|
+
if open_after_convert:
|
952
|
+
self._notify(
|
953
|
+
lambda path=result.output_file: self._open_in_file_manager(
|
954
|
+
path
|
955
|
+
)
|
956
|
+
)
|
782
957
|
|
783
958
|
self._append_log("All jobs finished successfully.")
|
784
959
|
self._notify(lambda: self.open_button.configure(state=self.tk.NORMAL))
|
960
|
+
self._notify(self._clear_input_files)
|
785
961
|
except FFmpegNotFoundError as exc:
|
786
962
|
self._notify(
|
787
963
|
lambda: self.messagebox.showerror("FFmpeg not found", str(exc))
|
788
964
|
)
|
789
965
|
self._set_status("Error")
|
790
966
|
except Exception as exc: # pragma: no cover - GUI level safeguard
|
791
|
-
|
792
|
-
|
793
|
-
|
967
|
+
# If stop was requested, don't show error (FFmpeg termination is expected)
|
968
|
+
if self._stop_requested:
|
969
|
+
self._append_log("Processing aborted by user.")
|
970
|
+
self._set_status("Aborted")
|
971
|
+
else:
|
972
|
+
self._notify(
|
973
|
+
lambda: self.messagebox.showerror(
|
974
|
+
"Error", f"Processing failed: {exc}"
|
975
|
+
)
|
794
976
|
)
|
795
|
-
|
796
|
-
self._set_status("Error")
|
977
|
+
self._set_status("Error")
|
797
978
|
finally:
|
798
|
-
self._notify(
|
979
|
+
self._notify(self._hide_stop_button)
|
799
980
|
|
800
981
|
self._processing_thread = threading.Thread(target=worker, daemon=True)
|
801
982
|
self._processing_thread.start()
|
802
983
|
|
984
|
+
# Show Stop button when processing starts
|
985
|
+
self.stop_button.grid()
|
986
|
+
|
987
|
+
def _stop_processing(self) -> None:
|
988
|
+
"""Stop the currently running processing by terminating FFmpeg."""
|
989
|
+
import signal
|
990
|
+
|
991
|
+
self._stop_requested = True
|
992
|
+
if self._ffmpeg_process and self._ffmpeg_process.poll() is None:
|
993
|
+
self._append_log("Stopping FFmpeg process...")
|
994
|
+
try:
|
995
|
+
# Send SIGTERM to FFmpeg process
|
996
|
+
if sys.platform == "win32":
|
997
|
+
# Windows doesn't have SIGTERM, use terminate()
|
998
|
+
self._ffmpeg_process.terminate()
|
999
|
+
else:
|
1000
|
+
# Unix-like systems can use SIGTERM
|
1001
|
+
self._ffmpeg_process.send_signal(signal.SIGTERM)
|
1002
|
+
|
1003
|
+
self._append_log("FFmpeg process stopped.")
|
1004
|
+
except Exception as e:
|
1005
|
+
self._append_log(f"Error stopping process: {e}")
|
1006
|
+
else:
|
1007
|
+
self._append_log("No active FFmpeg process to stop.")
|
1008
|
+
|
1009
|
+
self._hide_stop_button()
|
1010
|
+
|
1011
|
+
def _hide_stop_button(self) -> None:
|
1012
|
+
"""Hide Stop button."""
|
1013
|
+
self.stop_button.grid_remove()
|
1014
|
+
# Show drop hint when stop button is hidden and no other buttons are visible
|
1015
|
+
if (not self.open_button.winfo_viewable() and
|
1016
|
+
hasattr(self, 'drop_hint_button') and
|
1017
|
+
not self.drop_hint_button.winfo_viewable()):
|
1018
|
+
self.drop_hint_button.grid()
|
1019
|
+
|
803
1020
|
def _collect_arguments(self) -> dict[str, object]:
|
804
1021
|
args: dict[str, object] = {}
|
805
1022
|
|
@@ -884,17 +1101,59 @@ class TalksReducerGUI:
|
|
884
1101
|
normalized = message.strip().lower()
|
885
1102
|
if "all jobs finished successfully" in normalized:
|
886
1103
|
self._set_status("Success")
|
1104
|
+
self._set_progress(100) # 100% on success
|
1105
|
+
self._video_duration_seconds = None # Reset for next video
|
1106
|
+
elif normalized.startswith("extracting audio"):
|
1107
|
+
self._set_status("Extracting audio...")
|
1108
|
+
self._set_progress(0) # 0% on start
|
1109
|
+
self._video_duration_seconds = None # Reset for new processing
|
887
1110
|
elif normalized.startswith("starting processing") or normalized.startswith(
|
888
1111
|
"processing"
|
889
1112
|
):
|
890
1113
|
self._set_status("Processing")
|
1114
|
+
self._set_progress(0) # 0% on start
|
1115
|
+
self._video_duration_seconds = None # Reset for new processing
|
1116
|
+
|
1117
|
+
# Parse video duration from FFmpeg output
|
1118
|
+
duration_match = re.search(r"Duration:\s*(\d{2}):(\d{2}):(\d{2}\.\d+)", message)
|
1119
|
+
if duration_match:
|
1120
|
+
hours = int(duration_match.group(1))
|
1121
|
+
minutes = int(duration_match.group(2))
|
1122
|
+
seconds = float(duration_match.group(3))
|
1123
|
+
self._video_duration_seconds = hours * 3600 + minutes * 60 + seconds
|
1124
|
+
|
1125
|
+
# Parse FFmpeg progress information (time and speed)
|
1126
|
+
time_match = re.search(r"time=(\d{2}):(\d{2}):(\d{2})\.\d+", message)
|
1127
|
+
speed_match = re.search(r"speed=\s*([\d.]+)x", message)
|
1128
|
+
|
1129
|
+
if time_match and speed_match:
|
1130
|
+
hours = int(time_match.group(1))
|
1131
|
+
minutes = int(time_match.group(2))
|
1132
|
+
seconds = int(time_match.group(3))
|
1133
|
+
time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
1134
|
+
speed_str = speed_match.group(1)
|
1135
|
+
|
1136
|
+
# Calculate percentage if we have duration
|
1137
|
+
if self._video_duration_seconds and self._video_duration_seconds > 0:
|
1138
|
+
current_seconds = hours * 3600 + minutes * 60 + seconds
|
1139
|
+
percentage = min(
|
1140
|
+
100, int((current_seconds / self._video_duration_seconds) * 100)
|
1141
|
+
)
|
1142
|
+
self._set_status(f"{time_str}, {speed_str}x ({percentage}%)")
|
1143
|
+
self._set_progress(percentage) # Update progress bar
|
1144
|
+
else:
|
1145
|
+
self._set_status(f"{time_str}, {speed_str}x")
|
891
1146
|
|
892
1147
|
def _apply_status_style(self, status: str) -> None:
|
893
1148
|
color = STATUS_COLORS.get(status.lower())
|
894
1149
|
if color:
|
895
1150
|
self.status_label.configure(fg=color)
|
896
1151
|
else:
|
897
|
-
|
1152
|
+
# For extracting audio or FFmpeg progress messages, use processing color
|
1153
|
+
if "extracting audio" in status.lower() or re.search(
|
1154
|
+
r"\d{2}:\d{2}:\d{2}.*\d+\.?\d*x", status
|
1155
|
+
):
|
1156
|
+
self.status_label.configure(fg=STATUS_COLORS["processing"])
|
898
1157
|
|
899
1158
|
def _set_status(self, status: str) -> None:
|
900
1159
|
def apply() -> None:
|
@@ -902,22 +1161,35 @@ class TalksReducerGUI:
|
|
902
1161
|
self._status_state = status
|
903
1162
|
self.status_var.set(status)
|
904
1163
|
self._apply_status_style(status)
|
1164
|
+
self._set_progress_bar_style(status)
|
905
1165
|
lowered = status.lower()
|
906
|
-
|
907
|
-
|
1166
|
+
is_processing = lowered == "processing" or "extracting audio" in lowered
|
1167
|
+
|
1168
|
+
if is_processing:
|
908
1169
|
self._start_status_animation()
|
909
|
-
|
910
|
-
if
|
911
|
-
self.
|
1170
|
+
# Show stop button during processing
|
1171
|
+
if hasattr(self, 'status_frame'):
|
1172
|
+
self.status_frame.grid()
|
1173
|
+
self.stop_button.grid()
|
1174
|
+
self.drop_hint_button.grid_remove()
|
912
1175
|
|
913
1176
|
if lowered == "success":
|
914
|
-
if self.simple_mode_var.get():
|
915
|
-
self.
|
1177
|
+
if self.simple_mode_var.get() and hasattr(self, 'status_frame'):
|
1178
|
+
self.status_frame.grid()
|
1179
|
+
self.stop_button.grid_remove()
|
1180
|
+
self.drop_hint_button.grid_remove()
|
916
1181
|
self.open_button.grid()
|
1182
|
+
self.open_button.lift() # Ensure open_button is above drop_hint_button
|
1183
|
+
print("success status")
|
917
1184
|
else:
|
918
1185
|
self.open_button.grid_remove()
|
919
|
-
|
920
|
-
|
1186
|
+
print("not success status")
|
1187
|
+
if self.simple_mode_var.get() and not is_processing and hasattr(self, 'status_frame'):
|
1188
|
+
self.status_frame.grid_remove()
|
1189
|
+
self.stop_button.grid_remove()
|
1190
|
+
# Show drop hint when no other buttons are visible
|
1191
|
+
if hasattr(self, 'drop_hint_button'):
|
1192
|
+
self.drop_hint_button.grid()
|
921
1193
|
|
922
1194
|
self.root.after(0, apply)
|
923
1195
|
|
@@ -945,6 +1217,99 @@ class TalksReducerGUI:
|
|
945
1217
|
if self._status_state.lower() != "processing":
|
946
1218
|
self.status_var.set(self._status_state)
|
947
1219
|
|
1220
|
+
def _calculate_gradient_color(self, percentage: int, darken: float = 1.0) -> str:
|
1221
|
+
"""Calculate color gradient from red (0%) to green (100%).
|
1222
|
+
|
1223
|
+
Args:
|
1224
|
+
percentage: The position in the gradient (0-100)
|
1225
|
+
darken: Value between 0.0 (black) and 1.0 (original brightness)
|
1226
|
+
|
1227
|
+
Returns:
|
1228
|
+
Hex color code string
|
1229
|
+
"""
|
1230
|
+
# Clamp percentage between 0 and 100
|
1231
|
+
percentage = max(0, min(100, percentage))
|
1232
|
+
# Clamp darken between 0.0 and 1.0
|
1233
|
+
darken = max(0.0, min(1.0, darken))
|
1234
|
+
|
1235
|
+
if percentage <= 50:
|
1236
|
+
# Red to Yellow (0% to 50%)
|
1237
|
+
# Red: (248, 113, 113) -> Yellow: (250, 204, 21)
|
1238
|
+
ratio = percentage / 50.0
|
1239
|
+
r = int((248 + (250 - 248) * ratio) * darken)
|
1240
|
+
g = int((113 + (204 - 113) * ratio) * darken)
|
1241
|
+
b = int((113 + (21 - 113) * ratio) * darken)
|
1242
|
+
else:
|
1243
|
+
# Yellow to Green (50% to 100%)
|
1244
|
+
# Yellow: (250, 204, 21) -> Green: (34, 197, 94)
|
1245
|
+
ratio = (percentage - 50) / 50.0
|
1246
|
+
r = int((250 + (34 - 250) * ratio) * darken)
|
1247
|
+
g = int((204 + (197 - 204) * ratio) * darken)
|
1248
|
+
b = int((21 + (94 - 21) * ratio) * darken)
|
1249
|
+
|
1250
|
+
# Ensure values are within 0-255 range after darkening
|
1251
|
+
r = max(0, min(255, r))
|
1252
|
+
g = max(0, min(255, g))
|
1253
|
+
b = max(0, min(255, b))
|
1254
|
+
|
1255
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
1256
|
+
|
1257
|
+
def _set_progress(self, percentage: int) -> None:
|
1258
|
+
"""Update the progress bar value and color (thread-safe)."""
|
1259
|
+
|
1260
|
+
def updater() -> None:
|
1261
|
+
self.progress_var.set(percentage)
|
1262
|
+
# Update color based on percentage gradient
|
1263
|
+
color = self._calculate_gradient_color(percentage, 0.5)
|
1264
|
+
palette = (
|
1265
|
+
LIGHT_THEME if self._detect_system_theme() == "light" else DARK_THEME
|
1266
|
+
)
|
1267
|
+
if self.theme_var.get().lower() in {"light", "dark"}:
|
1268
|
+
palette = (
|
1269
|
+
LIGHT_THEME
|
1270
|
+
if self.theme_var.get().lower() == "light"
|
1271
|
+
else DARK_THEME
|
1272
|
+
)
|
1273
|
+
|
1274
|
+
self.style.configure(
|
1275
|
+
"Dynamic.Horizontal.TProgressbar",
|
1276
|
+
background=color,
|
1277
|
+
troughcolor=palette["surface"],
|
1278
|
+
borderwidth=0,
|
1279
|
+
thickness=20,
|
1280
|
+
)
|
1281
|
+
self.progress_bar.configure(style="Dynamic.Horizontal.TProgressbar")
|
1282
|
+
|
1283
|
+
# Show stop button when progress < 100
|
1284
|
+
if percentage < 100:
|
1285
|
+
if hasattr(self, 'status_frame'):
|
1286
|
+
self.status_frame.grid()
|
1287
|
+
self.stop_button.grid()
|
1288
|
+
self.drop_hint_button.grid_remove()
|
1289
|
+
self.root.after(0, updater)
|
1290
|
+
|
1291
|
+
def _set_progress_bar_style(self, status: str) -> None:
|
1292
|
+
"""Update the progress bar color based on status."""
|
1293
|
+
|
1294
|
+
def updater() -> None:
|
1295
|
+
# Map status to progress bar style
|
1296
|
+
status_lower = status.lower()
|
1297
|
+
if status_lower == "success":
|
1298
|
+
style = "Success.Horizontal.TProgressbar"
|
1299
|
+
elif status_lower == "error":
|
1300
|
+
style = "Error.Horizontal.TProgressbar"
|
1301
|
+
elif status_lower == "aborted":
|
1302
|
+
style = "Aborted.Horizontal.TProgressbar"
|
1303
|
+
elif status_lower == "idle":
|
1304
|
+
style = "Idle.Horizontal.TProgressbar"
|
1305
|
+
else:
|
1306
|
+
# For processing states, use dynamic gradient (will be set by _set_progress)
|
1307
|
+
return
|
1308
|
+
|
1309
|
+
self.progress_bar.configure(style=style)
|
1310
|
+
|
1311
|
+
self.root.after(0, updater)
|
1312
|
+
|
948
1313
|
def _notify(self, callback: Callable[[], None]) -> None:
|
949
1314
|
self.root.after(0, callback)
|
950
1315
|
|
@@ -996,7 +1361,21 @@ def main(argv: Optional[Sequence[str]] = None) -> bool:
|
|
996
1361
|
print()
|
997
1362
|
print("Run 'python3 -m talks_reducer --help' for all options.")
|
998
1363
|
print()
|
999
|
-
print("
|
1364
|
+
print("Troubleshooting tips:")
|
1365
|
+
if sys.platform == "darwin":
|
1366
|
+
print(
|
1367
|
+
" - On macOS, install Python from python.org or ensure "
|
1368
|
+
"Homebrew's python-tk package is present."
|
1369
|
+
)
|
1370
|
+
elif sys.platform.startswith("linux"):
|
1371
|
+
print(
|
1372
|
+
" - On Linux, install the Tk bindings for Python (for example, "
|
1373
|
+
"python3-tk)."
|
1374
|
+
)
|
1375
|
+
else:
|
1376
|
+
print(" - Ensure your Python installation includes Tk support.")
|
1377
|
+
print(" - You can always fall back to the CLI workflow below.")
|
1378
|
+
print()
|
1000
1379
|
print("The CLI interface works perfectly and is recommended.")
|
1001
1380
|
except UnicodeEncodeError:
|
1002
1381
|
# Fallback for extreme encoding issues
|
@@ -157,12 +157,15 @@ def speed_up_video(
|
|
157
157
|
ffmpeg_path=ffmpeg_path,
|
158
158
|
)
|
159
159
|
|
160
|
+
reporter.log("Extracting audio...")
|
161
|
+
process_callback = getattr(reporter, 'process_callback', None)
|
160
162
|
run_timed_ffmpeg_command(
|
161
163
|
extract_command,
|
162
164
|
reporter=reporter,
|
163
165
|
total=int(original_duration * frame_rate),
|
164
166
|
unit="frames",
|
165
167
|
desc="Extracting audio:",
|
168
|
+
process_callback=process_callback,
|
166
169
|
)
|
167
170
|
|
168
171
|
wav_sample_rate, audio_data = wavfile.read(os.fspath(audio_wav))
|
@@ -249,6 +252,7 @@ def speed_up_video(
|
|
249
252
|
total=updated_chunks[-1][3],
|
250
253
|
unit="frames",
|
251
254
|
desc="Generating final:",
|
255
|
+
process_callback=process_callback,
|
252
256
|
)
|
253
257
|
except subprocess.CalledProcessError as exc:
|
254
258
|
if fallback_command_str and use_cuda_encoder:
|
@@ -259,6 +263,7 @@ def speed_up_video(
|
|
259
263
|
total=updated_chunks[-1][3],
|
260
264
|
unit="frames",
|
261
265
|
desc="Generating final (fallback):",
|
266
|
+
process_callback=process_callback,
|
262
267
|
)
|
263
268
|
else:
|
264
269
|
raise
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: talks-reducer
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: CLI for speeding up long-form talks by removing silence
|
5
5
|
Author: Talks Reducer Maintainers
|
6
6
|
License-Expression: MIT
|
@@ -85,6 +85,8 @@ continues to work unchanged for local development.
|
|
85
85
|
- **Input drop zone** — drag files or folders from your desktop or add them via
|
86
86
|
the Explorer/Finder dialog; duplicates are ignored.
|
87
87
|
- **Small video** — toggles the `--small` preset used by the CLI.
|
88
|
+
- **Open after convert** — controls whether the exported file is revealed in
|
89
|
+
your system file manager as soon as each job finishes.
|
88
90
|
- **Advanced** — reveals optional controls for the output path, temp folder,
|
89
91
|
timing/audio knobs mirrored from the command line, and an appearance picker
|
90
92
|
that can force dark or light mode or follow your operating system.
|
@@ -94,6 +96,10 @@ a background thread. Once every queued job succeeds an **Open last output**
|
|
94
96
|
button appears so you can jump straight to the exported file in your system
|
95
97
|
file manager.
|
96
98
|
|
99
|
+
The GUI stores your last-used Simple mode, Small video, Open after convert, and
|
100
|
+
theme preferences in a cross-platform configuration file so they persist across
|
101
|
+
launches.
|
102
|
+
|
97
103
|
## Repository Structure
|
98
104
|
- `talks_reducer/` — Python package that exposes the CLI and reusable pipeline:
|
99
105
|
- `cli.py` parses arguments and dispatches to the pipeline.
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|