talks-reducer 0.2.23__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.23/talks_reducer.egg-info → talks_reducer-0.3.0}/PKG-INFO +7 -1
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/README.md +6 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/pyproject.toml +1 -1
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/cli.py +14 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/ffmpeg.py +13 -1
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/gui.py +458 -71
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/pipeline.py +5 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0/talks_reducer.egg-info}/PKG-INFO +7 -1
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/LICENSE +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/setup.cfg +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/__init__.py +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/__main__.py +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/audio.py +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/chunks.py +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/models.py +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer/progress.py +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer.egg-info/SOURCES.txt +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer.egg-info/dependency_links.txt +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer.egg-info/entry_points.txt +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer.egg-info/requires.txt +0 -0
- {talks_reducer-0.2.23 → talks_reducer-0.3.0}/talks_reducer.egg-info/top_level.txt +0 -0
- {talks_reducer-0.2.23 → 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.
|
@@ -6,6 +6,7 @@ import argparse
|
|
6
6
|
import os
|
7
7
|
import sys
|
8
8
|
import time
|
9
|
+
from importlib.metadata import version
|
9
10
|
from pathlib import Path
|
10
11
|
from typing import Dict, List, Optional, Sequence
|
11
12
|
|
@@ -22,6 +23,19 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
22
23
|
parser = argparse.ArgumentParser(
|
23
24
|
description="Modifies a video file to play at different speeds when there is sound vs. silence.",
|
24
25
|
)
|
26
|
+
|
27
|
+
# Add version argument
|
28
|
+
try:
|
29
|
+
pkg_version = version("talks-reducer")
|
30
|
+
except Exception:
|
31
|
+
pkg_version = "unknown"
|
32
|
+
|
33
|
+
parser.add_argument(
|
34
|
+
"--version",
|
35
|
+
action="version",
|
36
|
+
version=f"talks-reducer {pkg_version}",
|
37
|
+
)
|
38
|
+
|
25
39
|
parser.add_argument(
|
26
40
|
"input_file",
|
27
41
|
type=str,
|
@@ -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,9 +4,11 @@ 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
|
11
|
+
from importlib.metadata import version
|
10
12
|
from pathlib import Path
|
11
13
|
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Sequence
|
12
14
|
|
@@ -116,9 +118,10 @@ except ModuleNotFoundError: # pragma: no cover - runtime dependency
|
|
116
118
|
|
117
119
|
STATUS_COLORS = {
|
118
120
|
"idle": "#9ca3af",
|
119
|
-
"processing": "#
|
120
|
-
"success": "#
|
121
|
-
"error": "#
|
121
|
+
"processing": "#af8e0e",
|
122
|
+
"success": "#178941",
|
123
|
+
"error": "#ad4f4f",
|
124
|
+
"aborted": "#6d727a",
|
122
125
|
}
|
123
126
|
|
124
127
|
LIGHT_THEME = {
|
@@ -127,7 +130,8 @@ LIGHT_THEME = {
|
|
127
130
|
"accent": "#2563eb",
|
128
131
|
"surface": "#ffffff",
|
129
132
|
"border": "#cbd5e1",
|
130
|
-
"hover": "#
|
133
|
+
"hover": "#efefef",
|
134
|
+
"hover_text": "#000000",
|
131
135
|
"selection_background": "#2563eb",
|
132
136
|
"selection_foreground": "#ffffff",
|
133
137
|
}
|
@@ -139,6 +143,7 @@ DARK_THEME = {
|
|
139
143
|
"surface": "#2b2b3c",
|
140
144
|
"border": "#4b5563",
|
141
145
|
"hover": "#333333",
|
146
|
+
"hover_text": "#ffffff",
|
142
147
|
"selection_background": "#333333",
|
143
148
|
"selection_foreground": "#f3f4f6",
|
144
149
|
}
|
@@ -185,8 +190,13 @@ class _GuiProgressHandle(ProgressHandle):
|
|
185
190
|
class _TkProgressReporter(SignalProgressReporter):
|
186
191
|
"""Progress reporter that forwards updates to the GUI thread."""
|
187
192
|
|
188
|
-
def __init__(
|
193
|
+
def __init__(
|
194
|
+
self,
|
195
|
+
log_callback: Callable[[str], None],
|
196
|
+
process_callback: Optional[Callable] = None,
|
197
|
+
) -> None:
|
189
198
|
self._log_callback = log_callback
|
199
|
+
self.process_callback = process_callback
|
190
200
|
|
191
201
|
def log(self, message: str) -> None:
|
192
202
|
self._log_callback(message)
|
@@ -201,7 +211,55 @@ class _TkProgressReporter(SignalProgressReporter):
|
|
201
211
|
class TalksReducerGUI:
|
202
212
|
"""Tkinter application mirroring the CLI options with form controls."""
|
203
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
|
+
|
204
259
|
def __init__(self) -> None:
|
260
|
+
self._config_path = self._determine_config_path()
|
261
|
+
self._settings = self._load_settings()
|
262
|
+
|
205
263
|
# Import tkinter here to avoid loading it at module import time
|
206
264
|
import tkinter as tk
|
207
265
|
from tkinter import filedialog, messagebox, ttk
|
@@ -216,11 +274,18 @@ class TalksReducerGUI:
|
|
216
274
|
self.root = TkinterDnD.Tk() # type: ignore[call-arg]
|
217
275
|
else:
|
218
276
|
self.root = tk.Tk()
|
219
|
-
|
277
|
+
|
278
|
+
# Set window title with version
|
279
|
+
try:
|
280
|
+
app_version = version("talks-reducer")
|
281
|
+
self.root.title(f"Talks Reducer v{app_version}")
|
282
|
+
except Exception:
|
283
|
+
self.root.title("Talks Reducer")
|
284
|
+
|
220
285
|
self._apply_window_icon()
|
221
286
|
|
222
287
|
self._full_size = (760, 680)
|
223
|
-
self._simple_size = (
|
288
|
+
self._simple_size = (255, 330)
|
224
289
|
self.root.geometry(f"{self._full_size[0]}x{self._full_size[1]}")
|
225
290
|
self.style = self.ttk.Style(self.root)
|
226
291
|
|
@@ -230,21 +295,36 @@ class TalksReducerGUI:
|
|
230
295
|
self.status_var = tk.StringVar(value=self._status_state)
|
231
296
|
self._status_animation_job: Optional[str] = None
|
232
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
|
233
302
|
|
234
303
|
self.input_files: List[str] = []
|
235
304
|
|
236
305
|
self._dnd_available = TkinterDnD is not None and DND_FILES is not None
|
237
306
|
|
238
|
-
self.simple_mode_var = tk.BooleanVar(
|
307
|
+
self.simple_mode_var = tk.BooleanVar(
|
308
|
+
value=self._get_setting("simple_mode", True)
|
309
|
+
)
|
239
310
|
self.run_after_drop_var = tk.BooleanVar(value=True)
|
240
|
-
self.small_var = tk.BooleanVar(value=True)
|
241
|
-
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"))
|
242
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
|
+
)
|
243
321
|
|
244
322
|
self._build_layout()
|
245
323
|
self._apply_simple_mode(initial=True)
|
246
324
|
self._apply_status_style(self._status_state)
|
247
325
|
self._apply_theme()
|
326
|
+
self._save_settings()
|
327
|
+
self._hide_stop_button()
|
248
328
|
|
249
329
|
if not self._dnd_available:
|
250
330
|
self._append_log(
|
@@ -281,16 +361,17 @@ class TalksReducerGUI:
|
|
281
361
|
continue
|
282
362
|
|
283
363
|
def _build_layout(self) -> None:
|
284
|
-
main = self.ttk.Frame(self.root, padding=
|
364
|
+
main = self.ttk.Frame(self.root, padding=self.PADDING)
|
285
365
|
main.grid(row=0, column=0, sticky="nsew")
|
286
366
|
self.root.columnconfigure(0, weight=1)
|
287
367
|
self.root.rowconfigure(0, weight=1)
|
288
368
|
|
289
369
|
# Input selection frame
|
290
|
-
input_frame = self.ttk.
|
370
|
+
input_frame = self.ttk.Frame(main, padding=self.PADDING)
|
291
371
|
input_frame.grid(row=0, column=0, sticky="nsew")
|
292
372
|
main.rowconfigure(0, weight=1)
|
293
|
-
|
373
|
+
main.columnconfigure(0, weight=1)
|
374
|
+
for column in range(5):
|
294
375
|
input_frame.columnconfigure(column, weight=1)
|
295
376
|
|
296
377
|
self.input_list = self.tk.Listbox(input_frame, height=5)
|
@@ -304,13 +385,13 @@ class TalksReducerGUI:
|
|
304
385
|
self.drop_zone = self.tk.Label(
|
305
386
|
input_frame,
|
306
387
|
text="Drop files or folders here",
|
307
|
-
relief=self.tk.
|
308
|
-
borderwidth=
|
309
|
-
padx=
|
310
|
-
pady=
|
311
|
-
highlightthickness=
|
388
|
+
relief=self.tk.FLAT,
|
389
|
+
borderwidth=0,
|
390
|
+
padx=self.PADDING,
|
391
|
+
pady=self.PADDING,
|
392
|
+
highlightthickness=0,
|
312
393
|
)
|
313
|
-
self.drop_zone.grid(row=1, column=0, columnspan=
|
394
|
+
self.drop_zone.grid(row=1, column=0, columnspan=5, sticky="nsew")
|
314
395
|
input_frame.rowconfigure(1, weight=1)
|
315
396
|
self._configure_drop_targets(self.drop_zone)
|
316
397
|
self._configure_drop_targets(self.input_list)
|
@@ -335,8 +416,8 @@ class TalksReducerGUI:
|
|
335
416
|
self.run_after_drop_check.grid(row=2, column=3, pady=8, sticky="e")
|
336
417
|
|
337
418
|
# Options frame
|
338
|
-
options = self.ttk.
|
339
|
-
options.grid(row=
|
419
|
+
options = self.ttk.Frame(main, padding=self.PADDING)
|
420
|
+
options.grid(row=2, column=0, pady=(16, 0), sticky="ew")
|
340
421
|
options.columnconfigure(0, weight=1)
|
341
422
|
|
342
423
|
self.simple_mode_check = self.ttk.Checkbutton(
|
@@ -351,6 +432,12 @@ class TalksReducerGUI:
|
|
351
432
|
row=1, column=0, sticky="w", pady=(8, 0)
|
352
433
|
)
|
353
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
|
+
|
354
441
|
self.advanced_visible = self.tk.BooleanVar(value=False)
|
355
442
|
self.advanced_button = self.ttk.Button(
|
356
443
|
options,
|
@@ -359,8 +446,8 @@ class TalksReducerGUI:
|
|
359
446
|
)
|
360
447
|
self.advanced_button.grid(row=0, column=1, sticky="e")
|
361
448
|
|
362
|
-
self.advanced_frame = self.ttk.Frame(options, padding=
|
363
|
-
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")
|
364
451
|
self.advanced_frame.columnconfigure(1, weight=1)
|
365
452
|
|
366
453
|
self.output_var = self.tk.StringVar()
|
@@ -416,33 +503,54 @@ class TalksReducerGUI:
|
|
416
503
|
self._toggle_advanced(initial=True)
|
417
504
|
|
418
505
|
# Action buttons and log output
|
419
|
-
|
420
|
-
|
421
|
-
|
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))
|
422
526
|
|
423
|
-
self.
|
424
|
-
|
527
|
+
self.stop_button = self.ttk.Button(
|
528
|
+
status_frame, text="Stop", command=self._stop_processing
|
425
529
|
)
|
426
|
-
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
|
427
532
|
|
428
533
|
self.open_button = self.ttk.Button(
|
429
|
-
|
430
|
-
text="Open last
|
534
|
+
status_frame,
|
535
|
+
text="Open last",
|
431
536
|
command=self._open_last_output,
|
432
537
|
state=self.tk.DISABLED,
|
433
538
|
)
|
434
|
-
self.open_button.grid(row=
|
539
|
+
self.open_button.grid(row=2, column=0, columnspan=3, sticky="ew", pady=self.PADDING)
|
435
540
|
self.open_button.grid_remove()
|
436
541
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
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)
|
443
551
|
|
444
|
-
self.log_frame = self.ttk.
|
445
|
-
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")
|
446
554
|
main.rowconfigure(4, weight=1)
|
447
555
|
self.log_frame.columnconfigure(0, weight=1)
|
448
556
|
self.log_frame.rowconfigure(0, weight=1)
|
@@ -478,6 +586,7 @@ class TalksReducerGUI:
|
|
478
586
|
button.grid(row=row, column=2, padx=(8, 0))
|
479
587
|
|
480
588
|
def _toggle_simple_mode(self) -> None:
|
589
|
+
self._update_setting("simple_mode", self.simple_mode_var.get())
|
481
590
|
self._apply_simple_mode()
|
482
591
|
|
483
592
|
def _apply_simple_mode(self, *, initial: bool = False) -> None:
|
@@ -495,21 +604,23 @@ class TalksReducerGUI:
|
|
495
604
|
for widget in widgets:
|
496
605
|
widget.grid_remove()
|
497
606
|
self.log_frame.grid_remove()
|
498
|
-
self.
|
607
|
+
self.stop_button.grid_remove()
|
499
608
|
self.advanced_button.grid_remove()
|
500
609
|
self.advanced_frame.grid_remove()
|
501
|
-
self
|
610
|
+
if hasattr(self, 'status_frame'):
|
611
|
+
self.status_frame.grid_remove()
|
502
612
|
self.run_after_drop_var.set(True)
|
503
613
|
self._apply_window_size(simple=True)
|
504
|
-
if self.status_var.get().lower() == "success":
|
505
|
-
self.
|
614
|
+
if self.status_var.get().lower() == "success" and hasattr(self, 'status_frame'):
|
615
|
+
self.status_frame.grid()
|
506
616
|
self.open_button.grid()
|
617
|
+
self.drop_hint_button.grid_remove()
|
507
618
|
else:
|
508
619
|
for widget in widgets:
|
509
620
|
widget.grid()
|
510
621
|
self.log_frame.grid()
|
511
|
-
self
|
512
|
-
|
622
|
+
if hasattr(self, 'status_frame'):
|
623
|
+
self.status_frame.grid()
|
513
624
|
self.advanced_button.grid()
|
514
625
|
if self.advanced_visible.get():
|
515
626
|
self.advanced_frame.grid()
|
@@ -543,8 +654,17 @@ class TalksReducerGUI:
|
|
543
654
|
self.advanced_button.configure(text="Advanced")
|
544
655
|
|
545
656
|
def _on_theme_change(self, *_: object) -> None:
|
657
|
+
self._update_setting("theme", self.theme_var.get())
|
546
658
|
self._apply_theme()
|
547
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
|
+
|
548
668
|
def _apply_theme(self) -> None:
|
549
669
|
preference = self.theme_var.get().lower()
|
550
670
|
if preference not in {"light", "dark"}:
|
@@ -564,6 +684,8 @@ class TalksReducerGUI:
|
|
564
684
|
"TLabelframe",
|
565
685
|
background=palette["background"],
|
566
686
|
foreground=palette["foreground"],
|
687
|
+
borderwidth=0,
|
688
|
+
relief="flat",
|
567
689
|
)
|
568
690
|
self.style.configure(
|
569
691
|
"TLabelframe.Label",
|
@@ -578,11 +700,19 @@ class TalksReducerGUI:
|
|
578
700
|
background=palette["background"],
|
579
701
|
foreground=palette["foreground"],
|
580
702
|
)
|
703
|
+
self.style.map(
|
704
|
+
"TCheckbutton",
|
705
|
+
background=[("active", palette.get("hover", palette["background"]))],
|
706
|
+
)
|
581
707
|
self.style.configure(
|
582
708
|
"TRadiobutton",
|
583
709
|
background=palette["background"],
|
584
710
|
foreground=palette["foreground"],
|
585
711
|
)
|
712
|
+
self.style.map(
|
713
|
+
"TRadiobutton",
|
714
|
+
background=[("active", palette.get("hover", palette["background"]))],
|
715
|
+
)
|
586
716
|
self.style.configure(
|
587
717
|
"TButton",
|
588
718
|
background=palette["surface"],
|
@@ -596,7 +726,7 @@ class TalksReducerGUI:
|
|
596
726
|
("disabled", palette["surface"]),
|
597
727
|
],
|
598
728
|
foreground=[
|
599
|
-
("active", palette
|
729
|
+
("active", palette.get("hover_text", "#000000")),
|
600
730
|
("disabled", palette["foreground"]),
|
601
731
|
],
|
602
732
|
)
|
@@ -611,11 +741,47 @@ class TalksReducerGUI:
|
|
611
741
|
foreground=palette["foreground"],
|
612
742
|
)
|
613
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
|
+
|
614
781
|
self.drop_zone.configure(
|
615
782
|
bg=palette["surface"],
|
616
783
|
fg=palette["foreground"],
|
617
|
-
|
618
|
-
highlightcolor=palette["border"],
|
784
|
+
highlightthickness=0,
|
619
785
|
)
|
620
786
|
self.input_list.configure(
|
621
787
|
bg=palette["surface"],
|
@@ -706,12 +872,20 @@ class TalksReducerGUI:
|
|
706
872
|
self.input_list.delete(index)
|
707
873
|
del self.input_files[index]
|
708
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
|
+
|
709
880
|
def _on_drop(self, event: object) -> None:
|
710
881
|
data = getattr(event, "data", "")
|
711
882
|
if not data:
|
712
883
|
return
|
713
884
|
paths = self.root.tk.splitlist(data)
|
714
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)
|
715
889
|
self._extend_inputs(cleaned, auto_run=True)
|
716
890
|
|
717
891
|
def _browse_path(
|
@@ -745,10 +919,16 @@ class TalksReducerGUI:
|
|
745
919
|
return
|
746
920
|
|
747
921
|
self._append_log("Starting processing…")
|
748
|
-
self.
|
922
|
+
self._stop_requested = False
|
923
|
+
open_after_convert = bool(self.open_after_convert_var.get())
|
749
924
|
|
750
925
|
def worker() -> None:
|
751
|
-
|
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
|
+
)
|
752
932
|
try:
|
753
933
|
files = gather_input_files(self.input_files)
|
754
934
|
if not files:
|
@@ -768,30 +948,75 @@ class TalksReducerGUI:
|
|
768
948
|
result = speed_up_video(options, reporter=reporter)
|
769
949
|
self._last_output = result.output_file
|
770
950
|
self._append_log(f"Completed: {result.output_file}")
|
771
|
-
|
772
|
-
|
773
|
-
|
951
|
+
if open_after_convert:
|
952
|
+
self._notify(
|
953
|
+
lambda path=result.output_file: self._open_in_file_manager(
|
954
|
+
path
|
955
|
+
)
|
956
|
+
)
|
774
957
|
|
775
958
|
self._append_log("All jobs finished successfully.")
|
776
959
|
self._notify(lambda: self.open_button.configure(state=self.tk.NORMAL))
|
960
|
+
self._notify(self._clear_input_files)
|
777
961
|
except FFmpegNotFoundError as exc:
|
778
962
|
self._notify(
|
779
963
|
lambda: self.messagebox.showerror("FFmpeg not found", str(exc))
|
780
964
|
)
|
781
965
|
self._set_status("Error")
|
782
966
|
except Exception as exc: # pragma: no cover - GUI level safeguard
|
783
|
-
|
784
|
-
|
785
|
-
|
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
|
+
)
|
786
976
|
)
|
787
|
-
|
788
|
-
self._set_status("Error")
|
977
|
+
self._set_status("Error")
|
789
978
|
finally:
|
790
|
-
self._notify(
|
979
|
+
self._notify(self._hide_stop_button)
|
791
980
|
|
792
981
|
self._processing_thread = threading.Thread(target=worker, daemon=True)
|
793
982
|
self._processing_thread.start()
|
794
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
|
+
|
795
1020
|
def _collect_arguments(self) -> dict[str, object]:
|
796
1021
|
args: dict[str, object] = {}
|
797
1022
|
|
@@ -876,17 +1101,59 @@ class TalksReducerGUI:
|
|
876
1101
|
normalized = message.strip().lower()
|
877
1102
|
if "all jobs finished successfully" in normalized:
|
878
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
|
879
1110
|
elif normalized.startswith("starting processing") or normalized.startswith(
|
880
1111
|
"processing"
|
881
1112
|
):
|
882
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")
|
883
1146
|
|
884
1147
|
def _apply_status_style(self, status: str) -> None:
|
885
1148
|
color = STATUS_COLORS.get(status.lower())
|
886
1149
|
if color:
|
887
1150
|
self.status_label.configure(fg=color)
|
888
1151
|
else:
|
889
|
-
|
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"])
|
890
1157
|
|
891
1158
|
def _set_status(self, status: str) -> None:
|
892
1159
|
def apply() -> None:
|
@@ -894,22 +1161,35 @@ class TalksReducerGUI:
|
|
894
1161
|
self._status_state = status
|
895
1162
|
self.status_var.set(status)
|
896
1163
|
self._apply_status_style(status)
|
1164
|
+
self._set_progress_bar_style(status)
|
897
1165
|
lowered = status.lower()
|
898
|
-
|
899
|
-
|
1166
|
+
is_processing = lowered == "processing" or "extracting audio" in lowered
|
1167
|
+
|
1168
|
+
if is_processing:
|
900
1169
|
self._start_status_animation()
|
901
|
-
|
902
|
-
if
|
903
|
-
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()
|
904
1175
|
|
905
1176
|
if lowered == "success":
|
906
|
-
if self.simple_mode_var.get():
|
907
|
-
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()
|
908
1181
|
self.open_button.grid()
|
1182
|
+
self.open_button.lift() # Ensure open_button is above drop_hint_button
|
1183
|
+
print("success status")
|
909
1184
|
else:
|
910
1185
|
self.open_button.grid_remove()
|
911
|
-
|
912
|
-
|
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()
|
913
1193
|
|
914
1194
|
self.root.after(0, apply)
|
915
1195
|
|
@@ -937,6 +1217,99 @@ class TalksReducerGUI:
|
|
937
1217
|
if self._status_state.lower() != "processing":
|
938
1218
|
self.status_var.set(self._status_state)
|
939
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
|
+
|
940
1313
|
def _notify(self, callback: Callable[[], None]) -> None:
|
941
1314
|
self.root.after(0, callback)
|
942
1315
|
|
@@ -988,7 +1361,21 @@ def main(argv: Optional[Sequence[str]] = None) -> bool:
|
|
988
1361
|
print()
|
989
1362
|
print("Run 'python3 -m talks_reducer --help' for all options.")
|
990
1363
|
print()
|
991
|
-
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()
|
992
1379
|
print("The CLI interface works perfectly and is recommended.")
|
993
1380
|
except UnicodeEncodeError:
|
994
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
|