talks-reducer 0.5.5__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- talks_reducer/__about__.py +1 -1
- talks_reducer/cli.py +192 -15
- talks_reducer/discovery.py +42 -12
- talks_reducer/gui.py +637 -162
- talks_reducer/pipeline.py +1 -1
- talks_reducer/server.py +103 -22
- talks_reducer/server_tray.py +216 -17
- talks_reducer/service_client.py +258 -4
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/METADATA +13 -4
- talks_reducer-0.6.0.dist-info/RECORD +21 -0
- talks_reducer-0.5.5.dist-info/RECORD +0 -21
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/WHEEL +0 -0
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/top_level.txt +0 -0
talks_reducer/gui.py
CHANGED
@@ -10,9 +10,21 @@ import re
|
|
10
10
|
import subprocess
|
11
11
|
import sys
|
12
12
|
import threading
|
13
|
+
import urllib.error
|
14
|
+
import urllib.parse
|
15
|
+
import urllib.request
|
13
16
|
from importlib.metadata import version
|
14
17
|
from pathlib import Path
|
15
|
-
from typing import
|
18
|
+
from typing import (
|
19
|
+
TYPE_CHECKING,
|
20
|
+
Any,
|
21
|
+
Callable,
|
22
|
+
Iterable,
|
23
|
+
List,
|
24
|
+
Optional,
|
25
|
+
Sequence,
|
26
|
+
Tuple,
|
27
|
+
)
|
16
28
|
|
17
29
|
if TYPE_CHECKING:
|
18
30
|
import tkinter as tk
|
@@ -122,6 +134,7 @@ except ModuleNotFoundError: # pragma: no cover - runtime dependency
|
|
122
134
|
|
123
135
|
STATUS_COLORS = {
|
124
136
|
"idle": "#9ca3af",
|
137
|
+
"waiting": "#9ca3af",
|
125
138
|
"processing": "#af8e0e",
|
126
139
|
"success": "#178941",
|
127
140
|
"error": "#ad4f4f",
|
@@ -157,6 +170,25 @@ _TRAY_LOCK = threading.Lock()
|
|
157
170
|
_TRAY_PROCESS: Optional[subprocess.Popen[Any]] = None
|
158
171
|
|
159
172
|
|
173
|
+
def _default_remote_destination(input_file: Path, *, small: bool) -> Path:
|
174
|
+
"""Return the default remote output path for *input_file*.
|
175
|
+
|
176
|
+
Mirrors the naming scheme from the local pipeline so that remote jobs save
|
177
|
+
next to the source media with the expected suffix.
|
178
|
+
"""
|
179
|
+
|
180
|
+
name = input_file.name
|
181
|
+
dot_index = name.rfind(".")
|
182
|
+
suffix = "_speedup_small" if small else "_speedup"
|
183
|
+
|
184
|
+
if dot_index != -1:
|
185
|
+
new_name = name[:dot_index] + suffix + name[dot_index:]
|
186
|
+
else:
|
187
|
+
new_name = name + suffix
|
188
|
+
|
189
|
+
return input_file.with_name(new_name)
|
190
|
+
|
191
|
+
|
160
192
|
def _ensure_server_tray_running(extra_args: Optional[Sequence[str]] = None) -> None:
|
161
193
|
"""Start the server tray in a background process if one is not active."""
|
162
194
|
|
@@ -179,6 +211,31 @@ def _ensure_server_tray_running(extra_args: Optional[Sequence[str]] = None) -> N
|
|
179
211
|
)
|
180
212
|
|
181
213
|
|
214
|
+
def _parse_ratios_from_summary(summary: str) -> Tuple[Optional[float], Optional[float]]:
|
215
|
+
"""Extract time and size ratios from a Markdown *summary* string."""
|
216
|
+
|
217
|
+
time_ratio: Optional[float] = None
|
218
|
+
size_ratio: Optional[float] = None
|
219
|
+
|
220
|
+
for line in summary.splitlines():
|
221
|
+
if "**Duration:**" in line:
|
222
|
+
match = re.search(r"—\s*([0-9]+(?:\.[0-9]+)?)% of the original", line)
|
223
|
+
if match:
|
224
|
+
try:
|
225
|
+
time_ratio = float(match.group(1)) / 100
|
226
|
+
except ValueError:
|
227
|
+
time_ratio = None
|
228
|
+
elif "**Size:**" in line:
|
229
|
+
match = re.search(r"\*\*Size:\*\*\s*([0-9]+(?:\.[0-9]+)?)%", line)
|
230
|
+
if match:
|
231
|
+
try:
|
232
|
+
size_ratio = float(match.group(1)) / 100
|
233
|
+
except ValueError:
|
234
|
+
size_ratio = None
|
235
|
+
|
236
|
+
return time_ratio, size_ratio
|
237
|
+
|
238
|
+
|
182
239
|
class _GuiProgressHandle(ProgressHandle):
|
183
240
|
"""Simple progress handle that records totals but only logs milestones."""
|
184
241
|
|
@@ -243,6 +300,10 @@ class TalksReducerGUI:
|
|
243
300
|
"""Tkinter application mirroring the CLI options with form controls."""
|
244
301
|
|
245
302
|
PADDING = 10
|
303
|
+
AUDIO_PROCESSING_RATIO = 0.02
|
304
|
+
AUDIO_PROGRESS_STEPS = 20
|
305
|
+
MIN_AUDIO_INTERVAL_MS = 10
|
306
|
+
DEFAULT_AUDIO_INTERVAL_MS = 200
|
246
307
|
|
247
308
|
def _determine_config_path(self) -> Path:
|
248
309
|
if sys.platform == "win32":
|
@@ -281,6 +342,21 @@ class TalksReducerGUI:
|
|
281
342
|
self._settings[key] = value
|
282
343
|
return value
|
283
344
|
|
345
|
+
def _get_float_setting(self, key: str, default: float) -> float:
|
346
|
+
"""Return *key* as a float, coercing stored strings when necessary."""
|
347
|
+
|
348
|
+
raw_value = self._get_setting(key, default)
|
349
|
+
try:
|
350
|
+
number = float(raw_value)
|
351
|
+
except (TypeError, ValueError):
|
352
|
+
number = float(default)
|
353
|
+
|
354
|
+
if self._settings.get(key) != number:
|
355
|
+
self._settings[key] = number
|
356
|
+
self._save_settings()
|
357
|
+
|
358
|
+
return number
|
359
|
+
|
284
360
|
def _update_setting(self, key: str, value: object) -> None:
|
285
361
|
if self._settings.get(key) == value:
|
286
362
|
return
|
@@ -337,6 +413,10 @@ class TalksReducerGUI:
|
|
337
413
|
self._encode_target_duration_seconds: Optional[float] = None
|
338
414
|
self._encode_total_frames: Optional[int] = None
|
339
415
|
self._encode_current_frame: Optional[int] = None
|
416
|
+
self._source_duration_seconds: Optional[float] = None
|
417
|
+
self._audio_progress_job: Optional[str] = None
|
418
|
+
self._audio_progress_interval_ms: Optional[int] = None
|
419
|
+
self._audio_progress_steps_completed = 0
|
340
420
|
self.progress_var = tk.IntVar(value=0)
|
341
421
|
self._ffmpeg_process: Optional[subprocess.Popen] = None
|
342
422
|
self._stop_requested = False
|
@@ -353,6 +433,11 @@ class TalksReducerGUI:
|
|
353
433
|
self.open_after_convert_var = tk.BooleanVar(
|
354
434
|
value=self._get_setting("open_after_convert", True)
|
355
435
|
)
|
436
|
+
stored_mode = str(self._get_setting("processing_mode", "local"))
|
437
|
+
if stored_mode not in {"local", "remote"}:
|
438
|
+
stored_mode = "local"
|
439
|
+
self.processing_mode_var = tk.StringVar(value=stored_mode)
|
440
|
+
self.processing_mode_var.trace_add("write", self._on_processing_mode_change)
|
356
441
|
self.theme_var = tk.StringVar(value=self._get_setting("theme", "os"))
|
357
442
|
self.theme_var.trace_add("write", self._on_theme_change)
|
358
443
|
self.small_var.trace_add("write", self._on_small_video_change)
|
@@ -365,6 +450,10 @@ class TalksReducerGUI:
|
|
365
450
|
self.server_url_var.trace_add("write", self._on_server_url_change)
|
366
451
|
self._discovery_thread: Optional[threading.Thread] = None
|
367
452
|
|
453
|
+
self._basic_defaults: dict[str, float] = {}
|
454
|
+
self._basic_variables: dict[str, tk.DoubleVar] = {}
|
455
|
+
self._slider_updaters: dict[str, Callable[[str], None]] = {}
|
456
|
+
|
368
457
|
self._build_layout()
|
369
458
|
self._apply_simple_mode(initial=True)
|
370
459
|
self._apply_status_style(self._status_state)
|
@@ -372,6 +461,47 @@ class TalksReducerGUI:
|
|
372
461
|
self._save_settings()
|
373
462
|
self._hide_stop_button()
|
374
463
|
|
464
|
+
# Ping server on startup if in remote mode
|
465
|
+
if (
|
466
|
+
self.processing_mode_var.get() == "remote"
|
467
|
+
and self.server_url_var.get().strip()
|
468
|
+
and hasattr(self, "_ping_server")
|
469
|
+
):
|
470
|
+
server_url = self.server_url_var.get().strip()
|
471
|
+
host_label = self._format_server_host(server_url)
|
472
|
+
|
473
|
+
def ping_worker() -> None:
|
474
|
+
try:
|
475
|
+
if self._ping_server(server_url):
|
476
|
+
self._set_status("Idle", f"Server {host_label} is reachable")
|
477
|
+
self._notify(
|
478
|
+
lambda: self._append_log(f"Server {host_label} ready")
|
479
|
+
)
|
480
|
+
else:
|
481
|
+
self._set_status(
|
482
|
+
"Error", f"Server {host_label} is not reachable"
|
483
|
+
)
|
484
|
+
self._notify(
|
485
|
+
lambda: self._append_log(
|
486
|
+
f"Server {host_label} is not reachable"
|
487
|
+
)
|
488
|
+
)
|
489
|
+
ping_worker()
|
490
|
+
except Exception as exc:
|
491
|
+
self._set_status(
|
492
|
+
"Idle", f"Error pinging server {host_label}: {exc}"
|
493
|
+
)
|
494
|
+
self._notify(
|
495
|
+
lambda: self._append_log(
|
496
|
+
f"Error pinging server {host_label}: {exc}"
|
497
|
+
)
|
498
|
+
)
|
499
|
+
|
500
|
+
import threading
|
501
|
+
|
502
|
+
ping_thread = threading.Thread(target=ping_worker, daemon=True)
|
503
|
+
ping_thread.start()
|
504
|
+
|
375
505
|
if not self._dnd_available:
|
376
506
|
self._append_log(
|
377
507
|
"Drag and drop requires the tkinterdnd2 package. Install it to enable the drop zone."
|
@@ -420,16 +550,8 @@ class TalksReducerGUI:
|
|
420
550
|
input_frame.grid(row=0, column=0, sticky="nsew")
|
421
551
|
main.rowconfigure(0, weight=1)
|
422
552
|
main.columnconfigure(0, weight=1)
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
self.input_list = self.tk.Listbox(input_frame, height=5)
|
427
|
-
self.input_list.grid(row=0, column=0, columnspan=4, sticky="nsew", pady=(0, 12))
|
428
|
-
self.input_scrollbar = self.ttk.Scrollbar(
|
429
|
-
input_frame, orient=self.tk.VERTICAL, command=self.input_list.yview
|
430
|
-
)
|
431
|
-
self.input_scrollbar.grid(row=0, column=4, sticky="ns", pady=(0, 12))
|
432
|
-
self.input_list.configure(yscrollcommand=self.input_scrollbar.set)
|
553
|
+
input_frame.columnconfigure(0, weight=1)
|
554
|
+
input_frame.rowconfigure(0, weight=1)
|
433
555
|
|
434
556
|
self.drop_zone = self.tk.Label(
|
435
557
|
input_frame,
|
@@ -440,40 +562,19 @@ class TalksReducerGUI:
|
|
440
562
|
pady=self.PADDING,
|
441
563
|
highlightthickness=0,
|
442
564
|
)
|
443
|
-
self.drop_zone.grid(row=
|
444
|
-
input_frame.rowconfigure(1, weight=1)
|
565
|
+
self.drop_zone.grid(row=0, column=0, sticky="nsew")
|
445
566
|
self._configure_drop_targets(self.drop_zone)
|
446
|
-
self._configure_drop_targets(self.input_list)
|
447
567
|
self.drop_zone.configure(cursor="hand2", takefocus=1)
|
448
568
|
self.drop_zone.bind("<Button-1>", self._on_drop_zone_click)
|
449
569
|
self.drop_zone.bind("<Return>", self._on_drop_zone_click)
|
450
570
|
self.drop_zone.bind("<space>", self._on_drop_zone_click)
|
451
571
|
|
452
|
-
self.add_files_button = self.ttk.Button(
|
453
|
-
input_frame, text="Add files", command=self._add_files
|
454
|
-
)
|
455
|
-
self.add_files_button.grid(row=2, column=0, pady=8, sticky="w")
|
456
|
-
self.add_folder_button = self.ttk.Button(
|
457
|
-
input_frame, text="Add folder", command=self._add_directory
|
458
|
-
)
|
459
|
-
self.add_folder_button.grid(row=2, column=1, pady=8)
|
460
|
-
self.remove_selected_button = self.ttk.Button(
|
461
|
-
input_frame, text="Remove selected", command=self._remove_selected
|
462
|
-
)
|
463
|
-
self.remove_selected_button.grid(row=2, column=2, pady=8, sticky="w")
|
464
|
-
self.run_after_drop_check = self.ttk.Checkbutton(
|
465
|
-
input_frame,
|
466
|
-
text="Run after drop",
|
467
|
-
variable=self.run_after_drop_var,
|
468
|
-
)
|
469
|
-
self.run_after_drop_check.grid(row=2, column=3, pady=8, sticky="e")
|
470
|
-
|
471
572
|
# Options frame
|
472
|
-
|
473
|
-
|
474
|
-
|
573
|
+
self.options_frame = self.ttk.Frame(main, padding=self.PADDING)
|
574
|
+
self.options_frame.grid(row=2, column=0, pady=(0, 0), sticky="ew")
|
575
|
+
self.options_frame.columnconfigure(0, weight=1)
|
475
576
|
|
476
|
-
checkbox_frame = self.ttk.Frame(
|
577
|
+
checkbox_frame = self.ttk.Frame(self.options_frame)
|
477
578
|
checkbox_frame.grid(row=0, column=0, columnspan=2, sticky="w")
|
478
579
|
|
479
580
|
self.ttk.Checkbutton(
|
@@ -499,70 +600,119 @@ class TalksReducerGUI:
|
|
499
600
|
)
|
500
601
|
|
501
602
|
self.advanced_visible = self.tk.BooleanVar(value=False)
|
502
|
-
self.advanced_button = self.ttk.Button(
|
503
|
-
options,
|
504
|
-
text="Advanced",
|
505
|
-
command=self._toggle_advanced,
|
506
|
-
)
|
507
|
-
self.advanced_button.grid(row=1, column=1, sticky="e")
|
508
603
|
|
509
|
-
|
510
|
-
self.
|
511
|
-
self.
|
604
|
+
basic_label_container = self.ttk.Frame(self.options_frame)
|
605
|
+
basic_label = self.ttk.Label(basic_label_container, text="Basic options")
|
606
|
+
basic_label.pack(side=self.tk.LEFT)
|
512
607
|
|
513
|
-
self.
|
514
|
-
|
515
|
-
|
608
|
+
self.reset_basic_button = self.ttk.Button(
|
609
|
+
basic_label_container,
|
610
|
+
text="Reset to defaults",
|
611
|
+
command=self._reset_basic_defaults,
|
612
|
+
state=self.tk.DISABLED,
|
516
613
|
)
|
517
614
|
|
518
|
-
self.
|
519
|
-
|
520
|
-
self.advanced_frame, "Temp folder", self.temp_var, row=1, browse=True
|
615
|
+
self.basic_options_frame = self.ttk.Labelframe(
|
616
|
+
self.options_frame, padding=0, labelwidget=basic_label_container
|
521
617
|
)
|
522
|
-
|
523
|
-
|
524
|
-
self._add_entry(
|
525
|
-
self.advanced_frame,
|
526
|
-
"Silent threshold",
|
527
|
-
self.silent_threshold_var,
|
528
|
-
row=2,
|
618
|
+
self.basic_options_frame.grid(
|
619
|
+
row=2, column=0, columnspan=2, sticky="ew", pady=(12, 0)
|
529
620
|
)
|
621
|
+
self.basic_options_frame.columnconfigure(1, weight=1)
|
530
622
|
|
531
|
-
self.
|
532
|
-
self._add_entry(
|
533
|
-
self.advanced_frame, "Sounded speed", self.sounded_speed_var, row=3
|
534
|
-
)
|
623
|
+
self._reset_button_visible = False
|
535
624
|
|
536
|
-
self.silent_speed_var = self.tk.
|
537
|
-
|
538
|
-
|
625
|
+
self.silent_speed_var = self.tk.DoubleVar(
|
626
|
+
value=min(max(self._get_float_setting("silent_speed", 4.0), 1.0), 10.0)
|
627
|
+
)
|
628
|
+
self._add_slider(
|
629
|
+
self.basic_options_frame,
|
630
|
+
"Silent speed",
|
631
|
+
self.silent_speed_var,
|
632
|
+
row=0,
|
633
|
+
setting_key="silent_speed",
|
634
|
+
minimum=1.0,
|
635
|
+
maximum=10.0,
|
636
|
+
resolution=0.5,
|
637
|
+
display_format="{:.1f}×",
|
638
|
+
default_value=4.0,
|
539
639
|
)
|
540
640
|
|
541
|
-
self.
|
542
|
-
|
543
|
-
|
641
|
+
self.sounded_speed_var = self.tk.DoubleVar(
|
642
|
+
value=min(max(self._get_float_setting("sounded_speed", 1.0), 0.75), 2.0)
|
643
|
+
)
|
644
|
+
self._add_slider(
|
645
|
+
self.basic_options_frame,
|
646
|
+
"Sounded speed",
|
647
|
+
self.sounded_speed_var,
|
648
|
+
row=1,
|
649
|
+
setting_key="sounded_speed",
|
650
|
+
minimum=0.75,
|
651
|
+
maximum=2.0,
|
652
|
+
resolution=0.25,
|
653
|
+
display_format="{:.2f}×",
|
654
|
+
default_value=1.0,
|
544
655
|
)
|
545
656
|
|
546
|
-
self.
|
547
|
-
|
657
|
+
self.silent_threshold_var = self.tk.DoubleVar(
|
658
|
+
value=min(max(self._get_float_setting("silent_threshold", 0.05), 0.0), 1.0)
|
659
|
+
)
|
660
|
+
self._add_slider(
|
661
|
+
self.basic_options_frame,
|
662
|
+
"Silent threshold",
|
663
|
+
self.silent_threshold_var,
|
664
|
+
row=2,
|
665
|
+
setting_key="silent_threshold",
|
666
|
+
minimum=0.0,
|
667
|
+
maximum=1.0,
|
668
|
+
resolution=0.01,
|
669
|
+
display_format="{:.2f}",
|
670
|
+
default_value=0.05,
|
671
|
+
)
|
548
672
|
|
549
|
-
self.ttk.Label(self.
|
550
|
-
row=
|
673
|
+
self.ttk.Label(self.basic_options_frame, text="Server URL").grid(
|
674
|
+
row=3, column=0, sticky="w", pady=4
|
675
|
+
)
|
676
|
+
stored_server_url = str(
|
677
|
+
self._get_setting("server_url", "http://localhost:9005")
|
551
678
|
)
|
679
|
+
if not stored_server_url:
|
680
|
+
stored_server_url = "http://localhost:9005"
|
681
|
+
self._update_setting("server_url", stored_server_url)
|
682
|
+
self.server_url_var.set(stored_server_url)
|
552
683
|
self.server_url_entry = self.ttk.Entry(
|
553
|
-
self.
|
684
|
+
self.basic_options_frame, textvariable=self.server_url_var, width=30
|
554
685
|
)
|
555
|
-
self.server_url_entry.grid(row=
|
686
|
+
self.server_url_entry.grid(row=3, column=1, sticky="w", pady=4)
|
556
687
|
self.server_discover_button = self.ttk.Button(
|
557
|
-
self.
|
558
|
-
)
|
559
|
-
self.server_discover_button.grid(row=
|
560
|
-
|
561
|
-
self.ttk.Label(self.
|
562
|
-
row=
|
563
|
-
)
|
564
|
-
|
565
|
-
|
688
|
+
self.basic_options_frame, text="Discover", command=self._start_discovery
|
689
|
+
)
|
690
|
+
self.server_discover_button.grid(row=3, column=2, padx=(8, 0))
|
691
|
+
|
692
|
+
self.ttk.Label(self.basic_options_frame, text="Processing mode").grid(
|
693
|
+
row=4, column=0, sticky="w", pady=4
|
694
|
+
)
|
695
|
+
mode_choice = self.ttk.Frame(self.basic_options_frame)
|
696
|
+
mode_choice.grid(row=4, column=1, columnspan=2, sticky="w", pady=4)
|
697
|
+
self.ttk.Radiobutton(
|
698
|
+
mode_choice,
|
699
|
+
text="Local",
|
700
|
+
value="local",
|
701
|
+
variable=self.processing_mode_var,
|
702
|
+
).pack(side=self.tk.LEFT, padx=(0, 8))
|
703
|
+
self.remote_mode_button = self.ttk.Radiobutton(
|
704
|
+
mode_choice,
|
705
|
+
text="Remote",
|
706
|
+
value="remote",
|
707
|
+
variable=self.processing_mode_var,
|
708
|
+
)
|
709
|
+
self.remote_mode_button.pack(side=self.tk.LEFT, padx=(0, 8))
|
710
|
+
|
711
|
+
self.ttk.Label(self.basic_options_frame, text="Theme").grid(
|
712
|
+
row=5, column=0, sticky="w", pady=(8, 0)
|
713
|
+
)
|
714
|
+
theme_choice = self.ttk.Frame(self.basic_options_frame)
|
715
|
+
theme_choice.grid(row=5, column=1, columnspan=2, sticky="w", pady=(8, 0))
|
566
716
|
for value, label in ("os", "OS"), ("light", "Light"), ("dark", "Dark"):
|
567
717
|
self.ttk.Radiobutton(
|
568
718
|
theme_choice,
|
@@ -572,7 +722,47 @@ class TalksReducerGUI:
|
|
572
722
|
command=self._apply_theme,
|
573
723
|
).pack(side=self.tk.LEFT, padx=(0, 8))
|
574
724
|
|
725
|
+
self.advanced_button = self.ttk.Button(
|
726
|
+
self.options_frame,
|
727
|
+
text="Advanced",
|
728
|
+
command=self._toggle_advanced,
|
729
|
+
)
|
730
|
+
self.advanced_button.grid(
|
731
|
+
row=3, column=0, columnspan=2, sticky="w", pady=(12, 0)
|
732
|
+
)
|
733
|
+
|
734
|
+
self.advanced_frame = self.ttk.Frame(self.options_frame, padding=0)
|
735
|
+
self.advanced_frame.grid(row=4, column=0, columnspan=2, sticky="nsew")
|
736
|
+
self.advanced_frame.columnconfigure(1, weight=1)
|
737
|
+
|
738
|
+
self.output_var = self.tk.StringVar()
|
739
|
+
self._add_entry(
|
740
|
+
self.advanced_frame, "Output file", self.output_var, row=0, browse=True
|
741
|
+
)
|
742
|
+
|
743
|
+
self.temp_var = self.tk.StringVar(value=str(default_temp_folder()))
|
744
|
+
self._add_entry(
|
745
|
+
self.advanced_frame, "Temp folder", self.temp_var, row=1, browse=True
|
746
|
+
)
|
747
|
+
|
748
|
+
self.sample_rate_var = self.tk.StringVar(value="48000")
|
749
|
+
self._add_entry(self.advanced_frame, "Sample rate", self.sample_rate_var, row=2)
|
750
|
+
|
751
|
+
frame_margin_setting = self._get_setting("frame_margin", 2)
|
752
|
+
try:
|
753
|
+
frame_margin_default = int(frame_margin_setting)
|
754
|
+
except (TypeError, ValueError):
|
755
|
+
frame_margin_default = 2
|
756
|
+
self._update_setting("frame_margin", frame_margin_default)
|
757
|
+
|
758
|
+
self.frame_margin_var = self.tk.StringVar(value=str(frame_margin_default))
|
759
|
+
self._add_entry(
|
760
|
+
self.advanced_frame, "Frame margin", self.frame_margin_var, row=3
|
761
|
+
)
|
762
|
+
|
575
763
|
self._toggle_advanced(initial=True)
|
764
|
+
self._update_processing_mode_state()
|
765
|
+
self._update_basic_reset_state()
|
576
766
|
|
577
767
|
# Action buttons and log output
|
578
768
|
status_frame = self.ttk.Frame(main, padding=self.PADDING)
|
@@ -580,6 +770,7 @@ class TalksReducerGUI:
|
|
580
770
|
status_frame.columnconfigure(0, weight=0)
|
581
771
|
status_frame.columnconfigure(1, weight=1)
|
582
772
|
status_frame.columnconfigure(2, weight=0)
|
773
|
+
self.status_frame = status_frame
|
583
774
|
|
584
775
|
self.ttk.Label(status_frame, text="Status:").grid(row=0, column=0, sticky="w")
|
585
776
|
self.status_label = self.tk.Label(
|
@@ -664,6 +855,166 @@ class TalksReducerGUI:
|
|
664
855
|
)
|
665
856
|
button.grid(row=row, column=2, padx=(8, 0))
|
666
857
|
|
858
|
+
def _add_slider(
|
859
|
+
self,
|
860
|
+
parent, # type: tk.Misc
|
861
|
+
label: str,
|
862
|
+
variable, # type: tk.DoubleVar
|
863
|
+
*,
|
864
|
+
row: int,
|
865
|
+
setting_key: str,
|
866
|
+
minimum: float,
|
867
|
+
maximum: float,
|
868
|
+
resolution: float,
|
869
|
+
display_format: str,
|
870
|
+
default_value: float,
|
871
|
+
) -> None:
|
872
|
+
self.ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", pady=4)
|
873
|
+
|
874
|
+
value_label = self.ttk.Label(parent)
|
875
|
+
value_label.grid(row=row, column=2, sticky="e", pady=4)
|
876
|
+
|
877
|
+
def update(value: str) -> None:
|
878
|
+
numeric = float(value)
|
879
|
+
clamped = max(minimum, min(maximum, numeric))
|
880
|
+
steps = round((clamped - minimum) / resolution)
|
881
|
+
quantized = minimum + steps * resolution
|
882
|
+
if abs(variable.get() - quantized) > 1e-9:
|
883
|
+
variable.set(quantized)
|
884
|
+
value_label.configure(text=display_format.format(quantized))
|
885
|
+
self._update_setting(setting_key, float(f"{quantized:.6f}"))
|
886
|
+
self._update_basic_reset_state()
|
887
|
+
|
888
|
+
slider = self.tk.Scale(
|
889
|
+
parent,
|
890
|
+
variable=variable,
|
891
|
+
from_=minimum,
|
892
|
+
to=maximum,
|
893
|
+
orient=self.tk.HORIZONTAL,
|
894
|
+
resolution=resolution,
|
895
|
+
showvalue=False,
|
896
|
+
command=update,
|
897
|
+
length=240,
|
898
|
+
)
|
899
|
+
slider.grid(row=row, column=1, sticky="ew", pady=4, padx=(0, 8))
|
900
|
+
|
901
|
+
update(str(variable.get()))
|
902
|
+
|
903
|
+
self._slider_updaters[setting_key] = update
|
904
|
+
self._basic_defaults[setting_key] = default_value
|
905
|
+
self._basic_variables[setting_key] = variable
|
906
|
+
variable.trace_add("write", lambda *_: self._update_basic_reset_state())
|
907
|
+
|
908
|
+
def _update_basic_reset_state(self) -> None:
|
909
|
+
"""Enable or disable the reset control based on slider values."""
|
910
|
+
|
911
|
+
if not hasattr(self, "reset_basic_button"):
|
912
|
+
return
|
913
|
+
|
914
|
+
should_enable = False
|
915
|
+
for key, default_value in self._basic_defaults.items():
|
916
|
+
variable = self._basic_variables.get(key)
|
917
|
+
if variable is None:
|
918
|
+
continue
|
919
|
+
try:
|
920
|
+
current_value = float(variable.get())
|
921
|
+
except (TypeError, ValueError):
|
922
|
+
should_enable = True
|
923
|
+
break
|
924
|
+
if abs(current_value - default_value) > 1e-9:
|
925
|
+
should_enable = True
|
926
|
+
break
|
927
|
+
|
928
|
+
if should_enable:
|
929
|
+
if not getattr(self, "_reset_button_visible", False):
|
930
|
+
self.reset_basic_button.pack(side=self.tk.LEFT, padx=(8, 0))
|
931
|
+
self._reset_button_visible = True
|
932
|
+
self.reset_basic_button.configure(state=self.tk.NORMAL)
|
933
|
+
else:
|
934
|
+
if getattr(self, "_reset_button_visible", False):
|
935
|
+
self.reset_basic_button.pack_forget()
|
936
|
+
self._reset_button_visible = False
|
937
|
+
self.reset_basic_button.configure(state=self.tk.DISABLED)
|
938
|
+
|
939
|
+
def _reset_basic_defaults(self) -> None:
|
940
|
+
"""Restore the basic numeric controls to their default values."""
|
941
|
+
|
942
|
+
for key, default_value in self._basic_defaults.items():
|
943
|
+
variable = self._basic_variables.get(key)
|
944
|
+
if variable is None:
|
945
|
+
continue
|
946
|
+
|
947
|
+
try:
|
948
|
+
current_value = float(variable.get())
|
949
|
+
except (TypeError, ValueError):
|
950
|
+
current_value = default_value
|
951
|
+
|
952
|
+
if abs(current_value - default_value) <= 1e-9:
|
953
|
+
continue
|
954
|
+
|
955
|
+
variable.set(default_value)
|
956
|
+
updater = self._slider_updaters.get(key)
|
957
|
+
if updater is not None:
|
958
|
+
updater(str(default_value))
|
959
|
+
else:
|
960
|
+
self._update_setting(key, float(f"{default_value:.6f}"))
|
961
|
+
|
962
|
+
self._update_basic_reset_state()
|
963
|
+
|
964
|
+
def _update_processing_mode_state(self) -> None:
|
965
|
+
has_url = bool(self.server_url_var.get().strip())
|
966
|
+
if not has_url and self.processing_mode_var.get() == "remote":
|
967
|
+
self.processing_mode_var.set("local")
|
968
|
+
return
|
969
|
+
|
970
|
+
if hasattr(self, "remote_mode_button"):
|
971
|
+
state = self.tk.NORMAL if has_url else self.tk.DISABLED
|
972
|
+
self.remote_mode_button.configure(state=state)
|
973
|
+
|
974
|
+
def _normalize_server_url(self, server_url: str) -> str:
|
975
|
+
parsed = urllib.parse.urlsplit(server_url)
|
976
|
+
if not parsed.scheme:
|
977
|
+
parsed = urllib.parse.urlsplit(f"http://{server_url}")
|
978
|
+
|
979
|
+
netloc = parsed.netloc or parsed.path
|
980
|
+
if not netloc:
|
981
|
+
return server_url
|
982
|
+
|
983
|
+
path = parsed.path if parsed.netloc else ""
|
984
|
+
normalized_path = path or "/"
|
985
|
+
return urllib.parse.urlunsplit((parsed.scheme, netloc, normalized_path, "", ""))
|
986
|
+
|
987
|
+
def _format_server_host(self, server_url: str) -> str:
|
988
|
+
parsed = urllib.parse.urlsplit(server_url)
|
989
|
+
if not parsed.scheme:
|
990
|
+
parsed = urllib.parse.urlsplit(f"http://{server_url}")
|
991
|
+
|
992
|
+
host = parsed.netloc or parsed.path or server_url
|
993
|
+
if parsed.netloc and parsed.path and parsed.path not in {"", "/"}:
|
994
|
+
host = f"{parsed.netloc}{parsed.path}"
|
995
|
+
|
996
|
+
host = host.rstrip("/").split(":")[0]
|
997
|
+
return host or server_url
|
998
|
+
|
999
|
+
def _ping_server(self, server_url: str, *, timeout: float = 5.0) -> bool:
|
1000
|
+
normalized = self._normalize_server_url(server_url)
|
1001
|
+
request = urllib.request.Request(
|
1002
|
+
normalized,
|
1003
|
+
headers={"User-Agent": "talks-reducer-gui"},
|
1004
|
+
method="GET",
|
1005
|
+
)
|
1006
|
+
|
1007
|
+
try:
|
1008
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
1009
|
+
status = getattr(response, "status", None)
|
1010
|
+
if status is None:
|
1011
|
+
status = response.getcode()
|
1012
|
+
if status is None:
|
1013
|
+
return False
|
1014
|
+
return 200 <= int(status) < 500
|
1015
|
+
except (urllib.error.URLError, ValueError):
|
1016
|
+
return False
|
1017
|
+
|
667
1018
|
def _start_discovery(self) -> None:
|
668
1019
|
"""Search the local network for running Talks Reducer servers."""
|
669
1020
|
|
@@ -677,7 +1028,11 @@ class TalksReducerGUI:
|
|
677
1028
|
|
678
1029
|
def worker() -> None:
|
679
1030
|
try:
|
680
|
-
urls = discover_servers(
|
1031
|
+
urls = discover_servers(
|
1032
|
+
progress_callback=lambda current, total: self._notify(
|
1033
|
+
lambda c=current, t=total: self._on_discovery_progress(c, t)
|
1034
|
+
)
|
1035
|
+
)
|
681
1036
|
except Exception as exc: # pragma: no cover - network failure safeguard
|
682
1037
|
self._notify(lambda: self._on_discovery_failed(exc))
|
683
1038
|
return
|
@@ -692,6 +1047,14 @@ class TalksReducerGUI:
|
|
692
1047
|
self._append_log(message)
|
693
1048
|
self.messagebox.showerror("Discovery failed", message)
|
694
1049
|
|
1050
|
+
def _on_discovery_progress(self, current: int, total: int) -> None:
|
1051
|
+
if total > 0:
|
1052
|
+
bounded = max(0, min(current, total))
|
1053
|
+
label = f"{bounded} / {total}"
|
1054
|
+
else:
|
1055
|
+
label = "Discovering…"
|
1056
|
+
self.server_discover_button.configure(text=label)
|
1057
|
+
|
695
1058
|
def _on_discovery_complete(self, urls: List[str]) -> None:
|
696
1059
|
self.server_discover_button.configure(state=self.tk.NORMAL, text="Discover")
|
697
1060
|
if not urls:
|
@@ -774,38 +1137,17 @@ class TalksReducerGUI:
|
|
774
1137
|
|
775
1138
|
def _apply_simple_mode(self, *, initial: bool = False) -> None:
|
776
1139
|
simple = self.simple_mode_var.get()
|
777
|
-
widgets = [
|
778
|
-
self.input_list,
|
779
|
-
self.input_scrollbar,
|
780
|
-
self.add_files_button,
|
781
|
-
self.add_folder_button,
|
782
|
-
self.remove_selected_button,
|
783
|
-
self.run_after_drop_check,
|
784
|
-
]
|
785
|
-
|
786
1140
|
if simple:
|
787
|
-
|
788
|
-
widget.grid_remove()
|
1141
|
+
self.basic_options_frame.grid_remove()
|
789
1142
|
self.log_frame.grid_remove()
|
790
1143
|
self.stop_button.grid_remove()
|
791
1144
|
self.advanced_button.grid_remove()
|
792
1145
|
self.advanced_frame.grid_remove()
|
793
|
-
if hasattr(self, "status_frame"):
|
794
|
-
self.status_frame.grid_remove()
|
795
1146
|
self.run_after_drop_var.set(True)
|
796
1147
|
self._apply_window_size(simple=True)
|
797
|
-
if self.status_var.get().lower() == "success" and hasattr(
|
798
|
-
self, "status_frame"
|
799
|
-
):
|
800
|
-
self.status_frame.grid()
|
801
|
-
self.open_button.grid()
|
802
|
-
self.drop_hint_button.grid_remove()
|
803
1148
|
else:
|
804
|
-
|
805
|
-
widget.grid()
|
1149
|
+
self.basic_options_frame.grid()
|
806
1150
|
self.log_frame.grid()
|
807
|
-
if hasattr(self, "status_frame"):
|
808
|
-
self.status_frame.grid()
|
809
1151
|
self.advanced_button.grid()
|
810
1152
|
if self.advanced_visible.get():
|
811
1153
|
self.advanced_frame.grid()
|
@@ -850,9 +1192,18 @@ class TalksReducerGUI:
|
|
850
1192
|
"open_after_convert", bool(self.open_after_convert_var.get())
|
851
1193
|
)
|
852
1194
|
|
1195
|
+
def _on_processing_mode_change(self, *_: object) -> None:
|
1196
|
+
value = self.processing_mode_var.get()
|
1197
|
+
if value not in {"local", "remote"}:
|
1198
|
+
self.processing_mode_var.set("local")
|
1199
|
+
return
|
1200
|
+
self._update_setting("processing_mode", value)
|
1201
|
+
self._update_processing_mode_state()
|
1202
|
+
|
853
1203
|
def _on_server_url_change(self, *_: object) -> None:
|
854
1204
|
value = self.server_url_var.get().strip()
|
855
1205
|
self._update_setting("server_url", value)
|
1206
|
+
self._update_processing_mode_state()
|
856
1207
|
|
857
1208
|
def _apply_theme(self) -> None:
|
858
1209
|
preference = self.theme_var.get().lower()
|
@@ -972,14 +1323,6 @@ class TalksReducerGUI:
|
|
972
1323
|
fg=palette["foreground"],
|
973
1324
|
highlightthickness=0,
|
974
1325
|
)
|
975
|
-
self.input_list.configure(
|
976
|
-
bg=palette["surface"],
|
977
|
-
fg=palette["foreground"],
|
978
|
-
selectbackground=palette.get("selection_background", palette["accent"]),
|
979
|
-
selectforeground=palette.get("selection_foreground", palette["surface"]),
|
980
|
-
highlightbackground=palette["border"],
|
981
|
-
highlightcolor=palette["border"],
|
982
|
-
)
|
983
1326
|
self.log_text.configure(
|
984
1327
|
bg=palette["surface"],
|
985
1328
|
fg=palette["foreground"],
|
@@ -1041,7 +1384,6 @@ class TalksReducerGUI:
|
|
1041
1384
|
resolved = os.fspath(Path(path))
|
1042
1385
|
if resolved not in self.input_files:
|
1043
1386
|
self.input_files.append(resolved)
|
1044
|
-
self.input_list.insert(self.tk.END, resolved)
|
1045
1387
|
normalized.append(resolved)
|
1046
1388
|
|
1047
1389
|
if auto_run and normalized:
|
@@ -1075,21 +1417,13 @@ class TalksReducerGUI:
|
|
1075
1417
|
for path in paths:
|
1076
1418
|
if path and path not in self.input_files:
|
1077
1419
|
self.input_files.append(path)
|
1078
|
-
self.input_list.insert(self.tk.END, path)
|
1079
1420
|
added = True
|
1080
1421
|
if auto_run and added and self.run_after_drop_var.get():
|
1081
1422
|
self._start_run()
|
1082
1423
|
|
1083
|
-
def _remove_selected(self) -> None:
|
1084
|
-
selection = list(self.input_list.curselection())
|
1085
|
-
for index in reversed(selection):
|
1086
|
-
self.input_list.delete(index)
|
1087
|
-
del self.input_files[index]
|
1088
|
-
|
1089
1424
|
def _clear_input_files(self) -> None:
|
1090
|
-
"""Clear all input files
|
1425
|
+
"""Clear all queued input files."""
|
1091
1426
|
self.input_files.clear()
|
1092
|
-
self.input_list.delete(0, self.tk.END)
|
1093
1427
|
|
1094
1428
|
def _on_drop(self, event: object) -> None:
|
1095
1429
|
data = getattr(event, "data", "")
|
@@ -1099,7 +1433,6 @@ class TalksReducerGUI:
|
|
1099
1433
|
cleaned = [path.strip("{}") for path in paths]
|
1100
1434
|
# Clear existing files before adding dropped files
|
1101
1435
|
self.input_files.clear()
|
1102
|
-
self.input_list.delete(0, self.tk.END)
|
1103
1436
|
self._extend_inputs(cleaned, auto_run=True)
|
1104
1437
|
|
1105
1438
|
def _on_drop_zone_click(self, event: object) -> str | None:
|
@@ -1146,6 +1479,13 @@ class TalksReducerGUI:
|
|
1146
1479
|
self._stop_requested = False
|
1147
1480
|
open_after_convert = bool(self.open_after_convert_var.get())
|
1148
1481
|
server_url = self.server_url_var.get().strip()
|
1482
|
+
remote_mode = self.processing_mode_var.get() == "remote"
|
1483
|
+
if remote_mode and not server_url:
|
1484
|
+
self.messagebox.showerror(
|
1485
|
+
"Missing server URL", "Remote mode requires a server URL."
|
1486
|
+
)
|
1487
|
+
return
|
1488
|
+
remote_mode = remote_mode and bool(server_url)
|
1149
1489
|
|
1150
1490
|
def worker() -> None:
|
1151
1491
|
def set_process(proc: subprocess.Popen) -> None:
|
@@ -1162,7 +1502,7 @@ class TalksReducerGUI:
|
|
1162
1502
|
self._set_status("Idle")
|
1163
1503
|
return
|
1164
1504
|
|
1165
|
-
if
|
1505
|
+
if remote_mode:
|
1166
1506
|
success = self._process_files_via_server(
|
1167
1507
|
files,
|
1168
1508
|
args,
|
@@ -1225,7 +1565,7 @@ class TalksReducerGUI:
|
|
1225
1565
|
self._processing_thread.start()
|
1226
1566
|
|
1227
1567
|
# Show Stop button when processing starts
|
1228
|
-
if
|
1568
|
+
if remote_mode:
|
1229
1569
|
self.stop_button.grid_remove()
|
1230
1570
|
else:
|
1231
1571
|
self.stop_button.grid()
|
@@ -1272,18 +1612,14 @@ class TalksReducerGUI:
|
|
1272
1612
|
args["output_file"] = Path(self.output_var.get())
|
1273
1613
|
if self.temp_var.get():
|
1274
1614
|
args["temp_folder"] = Path(self.temp_var.get())
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
if self.silent_speed_var.get():
|
1284
|
-
args["silent_speed"] = self._parse_float(
|
1285
|
-
self.silent_speed_var.get(), "Silent speed"
|
1286
|
-
)
|
1615
|
+
silent_threshold = float(self.silent_threshold_var.get())
|
1616
|
+
args["silent_threshold"] = round(silent_threshold, 2)
|
1617
|
+
|
1618
|
+
sounded_speed = float(self.sounded_speed_var.get())
|
1619
|
+
args["sounded_speed"] = round(sounded_speed, 2)
|
1620
|
+
|
1621
|
+
silent_speed = float(self.silent_speed_var.get())
|
1622
|
+
args["silent_speed"] = round(silent_speed, 2)
|
1287
1623
|
if self.frame_margin_var.get():
|
1288
1624
|
args["frame_spreadage"] = int(
|
1289
1625
|
round(self._parse_float(self.frame_margin_var.get(), "Frame margin"))
|
@@ -1319,6 +1655,19 @@ class TalksReducerGUI:
|
|
1319
1655
|
self._notify(lambda: self._set_status("Error"))
|
1320
1656
|
return False
|
1321
1657
|
|
1658
|
+
host_label = self._format_server_host(server_url)
|
1659
|
+
self._notify(
|
1660
|
+
lambda: self._set_status("waiting", f"Waiting server {host_label}...")
|
1661
|
+
)
|
1662
|
+
if not self._ping_server(server_url):
|
1663
|
+
self._append_log(f"Server unreachable: {server_url}")
|
1664
|
+
self._notify(
|
1665
|
+
lambda: self._set_status("Error", f"Server {host_label} unreachable")
|
1666
|
+
)
|
1667
|
+
return False
|
1668
|
+
|
1669
|
+
self._notify(lambda: self._set_status("waiting", f"Server {host_label} ready"))
|
1670
|
+
|
1322
1671
|
output_override = args.get("output_file") if len(files) == 1 else None
|
1323
1672
|
ignored = [key for key in args if key not in {"output_file", "small"}]
|
1324
1673
|
if ignored:
|
@@ -1327,33 +1676,52 @@ class TalksReducerGUI:
|
|
1327
1676
|
f"Server mode ignores the following options: {ignored_options}"
|
1328
1677
|
)
|
1329
1678
|
|
1679
|
+
small_mode = bool(args.get("small", False))
|
1680
|
+
|
1330
1681
|
for index, file in enumerate(files, start=1):
|
1331
1682
|
basename = os.path.basename(file)
|
1332
1683
|
self._append_log(
|
1333
1684
|
f"Uploading {index}/{len(files)}: {basename} to {server_url}"
|
1334
1685
|
)
|
1686
|
+
input_path = Path(file)
|
1687
|
+
|
1688
|
+
if output_override is not None:
|
1689
|
+
output_path = Path(output_override)
|
1690
|
+
if output_path.is_dir():
|
1691
|
+
output_path = (
|
1692
|
+
output_path
|
1693
|
+
/ _default_remote_destination(input_path, small=small_mode).name
|
1694
|
+
)
|
1695
|
+
else:
|
1696
|
+
output_path = _default_remote_destination(input_path, small=small_mode)
|
1697
|
+
|
1335
1698
|
try:
|
1336
1699
|
destination, summary, log_text = service_module.send_video(
|
1337
|
-
input_path=
|
1338
|
-
output_path=
|
1700
|
+
input_path=input_path,
|
1701
|
+
output_path=output_path,
|
1339
1702
|
server_url=server_url,
|
1340
|
-
small=
|
1703
|
+
small=small_mode,
|
1704
|
+
stream_updates=True,
|
1705
|
+
log_callback=self._append_log,
|
1706
|
+
# progress_callback=self._handle_service_progress,
|
1341
1707
|
)
|
1342
1708
|
except Exception as exc: # pragma: no cover - network safeguard
|
1343
|
-
|
1709
|
+
error_detail = f"{exc.__class__.__name__}: {exc}"
|
1710
|
+
error_msg = f"Processing failed: {error_detail}"
|
1344
1711
|
self._append_log(error_msg)
|
1345
1712
|
self._notify(lambda: self._set_status("Error"))
|
1346
1713
|
self._notify(
|
1347
1714
|
lambda: self.messagebox.showerror(
|
1348
1715
|
"Server error",
|
1349
|
-
f"Failed to process {basename}: {
|
1716
|
+
f"Failed to process {basename}: {error_detail}",
|
1350
1717
|
)
|
1351
1718
|
)
|
1352
1719
|
return False
|
1353
1720
|
|
1354
1721
|
self._last_output = Path(destination)
|
1355
|
-
|
1356
|
-
self.
|
1722
|
+
time_ratio, size_ratio = _parse_ratios_from_summary(summary)
|
1723
|
+
self._last_time_ratio = time_ratio
|
1724
|
+
self._last_size_ratio = size_ratio
|
1357
1725
|
for line in summary.splitlines():
|
1358
1726
|
self._append_log(line)
|
1359
1727
|
if log_text.strip():
|
@@ -1420,12 +1788,23 @@ class TalksReducerGUI:
|
|
1420
1788
|
|
1421
1789
|
def _update_status_from_message(self, message: str) -> None:
|
1422
1790
|
normalized = message.strip().lower()
|
1791
|
+
metadata_match = re.search(
|
1792
|
+
r"source metadata [—-] duration:\s*([\d.]+)s",
|
1793
|
+
message,
|
1794
|
+
re.IGNORECASE,
|
1795
|
+
)
|
1796
|
+
if metadata_match:
|
1797
|
+
try:
|
1798
|
+
self._source_duration_seconds = float(metadata_match.group(1))
|
1799
|
+
except ValueError:
|
1800
|
+
self._source_duration_seconds = None
|
1423
1801
|
if "all jobs finished successfully" in normalized:
|
1424
1802
|
# Create status message with ratios if available
|
1425
1803
|
status_msg = "Success"
|
1426
1804
|
if self._last_time_ratio is not None and self._last_size_ratio is not None:
|
1427
1805
|
status_msg = f"Time: {self._last_time_ratio:.0%}, Size: {self._last_size_ratio:.0%}"
|
1428
1806
|
|
1807
|
+
self._reset_audio_progress_state(clear_source=True)
|
1429
1808
|
self._set_status("success", status_msg)
|
1430
1809
|
self._set_progress(100) # 100% on success
|
1431
1810
|
self._video_duration_seconds = None # Reset for next video
|
@@ -1433,21 +1812,36 @@ class TalksReducerGUI:
|
|
1433
1812
|
self._encode_total_frames = None
|
1434
1813
|
self._encode_current_frame = None
|
1435
1814
|
elif normalized.startswith("extracting audio"):
|
1815
|
+
self._reset_audio_progress_state(clear_source=False)
|
1436
1816
|
self._set_status("processing", "Extracting audio...")
|
1437
1817
|
self._set_progress(0) # 0% on start
|
1438
1818
|
self._video_duration_seconds = None # Reset for new processing
|
1439
1819
|
self._encode_target_duration_seconds = None
|
1440
1820
|
self._encode_total_frames = None
|
1441
1821
|
self._encode_current_frame = None
|
1442
|
-
|
1443
|
-
|
1444
|
-
|
1822
|
+
self._start_audio_progress()
|
1823
|
+
elif normalized.startswith("uploading"):
|
1824
|
+
self._set_status("processing", "Uploading...")
|
1825
|
+
elif normalized.startswith("starting processing"):
|
1826
|
+
self._reset_audio_progress_state(clear_source=True)
|
1445
1827
|
self._set_status("processing", "Processing")
|
1446
1828
|
self._set_progress(0) # 0% on start
|
1447
1829
|
self._video_duration_seconds = None # Reset for new processing
|
1448
1830
|
self._encode_target_duration_seconds = None
|
1449
1831
|
self._encode_total_frames = None
|
1450
1832
|
self._encode_current_frame = None
|
1833
|
+
elif normalized.startswith("processing"):
|
1834
|
+
is_new_job = bool(re.match(r"processing \d+/\d+:", normalized))
|
1835
|
+
should_reset = self._status_state.lower() != "processing" or is_new_job
|
1836
|
+
if should_reset:
|
1837
|
+
self._set_progress(0) # 0% on start
|
1838
|
+
self._video_duration_seconds = None # Reset for new processing
|
1839
|
+
self._encode_target_duration_seconds = None
|
1840
|
+
self._encode_total_frames = None
|
1841
|
+
self._encode_current_frame = None
|
1842
|
+
if is_new_job:
|
1843
|
+
self._reset_audio_progress_state(clear_source=True)
|
1844
|
+
self._set_status("processing", "Processing")
|
1451
1845
|
|
1452
1846
|
frame_total_match = re.search(
|
1453
1847
|
r"Final encode target frames(?: \(fallback\))?:\s*(\d+)", message
|
@@ -1473,12 +1867,12 @@ class TalksReducerGUI:
|
|
1473
1867
|
|
1474
1868
|
self._encode_current_frame = current_frame
|
1475
1869
|
if self._encode_total_frames and self._encode_total_frames > 0:
|
1476
|
-
|
1477
|
-
|
1478
|
-
|
1479
|
-
)
|
1870
|
+
self._complete_audio_phase()
|
1871
|
+
frame_ratio = min(current_frame / self._encode_total_frames, 1.0)
|
1872
|
+
percentage = min(100, 5 + int(frame_ratio * 95))
|
1480
1873
|
self._set_progress(percentage)
|
1481
1874
|
else:
|
1875
|
+
self._complete_audio_phase()
|
1482
1876
|
self._set_status("processing", f"{current_frame} frames encoded")
|
1483
1877
|
|
1484
1878
|
# Parse encode target duration reported by the pipeline
|
@@ -1537,11 +1931,95 @@ class TalksReducerGUI:
|
|
1537
1931
|
and total_seconds
|
1538
1932
|
and total_seconds > 0
|
1539
1933
|
):
|
1540
|
-
|
1934
|
+
self._complete_audio_phase()
|
1935
|
+
time_ratio = min(current_seconds / total_seconds, 1.0)
|
1936
|
+
percentage = min(100, 5 + int(time_ratio * 95))
|
1541
1937
|
self._set_progress(percentage)
|
1542
1938
|
|
1543
1939
|
self._set_status("processing", status_msg)
|
1544
1940
|
|
1941
|
+
def _compute_audio_progress_interval(self) -> int:
|
1942
|
+
duration = self._source_duration_seconds or self._video_duration_seconds
|
1943
|
+
if duration and duration > 0:
|
1944
|
+
audio_seconds = max(duration * self.AUDIO_PROCESSING_RATIO, 0.0)
|
1945
|
+
interval_seconds = audio_seconds / self.AUDIO_PROGRESS_STEPS
|
1946
|
+
interval_ms = int(round(interval_seconds * 1000))
|
1947
|
+
return max(self.MIN_AUDIO_INTERVAL_MS, interval_ms)
|
1948
|
+
return self.DEFAULT_AUDIO_INTERVAL_MS
|
1949
|
+
|
1950
|
+
def _start_audio_progress(self) -> None:
|
1951
|
+
interval_ms = self._compute_audio_progress_interval()
|
1952
|
+
|
1953
|
+
def _start() -> None:
|
1954
|
+
if self._audio_progress_job is not None:
|
1955
|
+
self.root.after_cancel(self._audio_progress_job)
|
1956
|
+
self._audio_progress_steps_completed = 0
|
1957
|
+
self._audio_progress_interval_ms = interval_ms
|
1958
|
+
self._audio_progress_job = self.root.after(
|
1959
|
+
interval_ms, self._advance_audio_progress
|
1960
|
+
)
|
1961
|
+
|
1962
|
+
self._notify(_start)
|
1963
|
+
|
1964
|
+
def _advance_audio_progress(self) -> None:
|
1965
|
+
self._audio_progress_job = None
|
1966
|
+
if self._audio_progress_steps_completed >= self.AUDIO_PROGRESS_STEPS:
|
1967
|
+
self._audio_progress_interval_ms = None
|
1968
|
+
return
|
1969
|
+
|
1970
|
+
self._audio_progress_steps_completed += 1
|
1971
|
+
audio_percentage = (
|
1972
|
+
self._audio_progress_steps_completed / self.AUDIO_PROGRESS_STEPS * 100
|
1973
|
+
)
|
1974
|
+
percentage = (audio_percentage / 100) * 5
|
1975
|
+
self._set_progress(percentage)
|
1976
|
+
self._set_status("processing", "Audio processing: %d%%" % (audio_percentage))
|
1977
|
+
|
1978
|
+
if self._audio_progress_steps_completed < self.AUDIO_PROGRESS_STEPS:
|
1979
|
+
interval_ms = (
|
1980
|
+
self._audio_progress_interval_ms or self.DEFAULT_AUDIO_INTERVAL_MS
|
1981
|
+
)
|
1982
|
+
self._audio_progress_job = self.root.after(
|
1983
|
+
interval_ms, self._advance_audio_progress
|
1984
|
+
)
|
1985
|
+
else:
|
1986
|
+
self._audio_progress_interval_ms = None
|
1987
|
+
|
1988
|
+
def _cancel_audio_progress(self) -> None:
|
1989
|
+
if self._audio_progress_job is None:
|
1990
|
+
self._audio_progress_interval_ms = None
|
1991
|
+
return
|
1992
|
+
|
1993
|
+
def _cancel() -> None:
|
1994
|
+
if self._audio_progress_job is not None:
|
1995
|
+
self.root.after_cancel(self._audio_progress_job)
|
1996
|
+
self._audio_progress_job = None
|
1997
|
+
self._audio_progress_interval_ms = None
|
1998
|
+
|
1999
|
+
self._notify(_cancel)
|
2000
|
+
|
2001
|
+
def _reset_audio_progress_state(self, *, clear_source: bool) -> None:
|
2002
|
+
if clear_source:
|
2003
|
+
self._source_duration_seconds = None
|
2004
|
+
self._audio_progress_steps_completed = 0
|
2005
|
+
self._audio_progress_interval_ms = None
|
2006
|
+
if self._audio_progress_job is not None:
|
2007
|
+
self._cancel_audio_progress()
|
2008
|
+
|
2009
|
+
def _complete_audio_phase(self) -> None:
|
2010
|
+
def _complete() -> None:
|
2011
|
+
if self._audio_progress_job is not None:
|
2012
|
+
self.root.after_cancel(self._audio_progress_job)
|
2013
|
+
self._audio_progress_job = None
|
2014
|
+
self._audio_progress_interval_ms = None
|
2015
|
+
if self._audio_progress_steps_completed < self.AUDIO_PROGRESS_STEPS:
|
2016
|
+
self._audio_progress_steps_completed = self.AUDIO_PROGRESS_STEPS
|
2017
|
+
current_value = self.progress_var.get()
|
2018
|
+
if current_value < self.AUDIO_PROGRESS_STEPS:
|
2019
|
+
self._set_progress(self.AUDIO_PROGRESS_STEPS)
|
2020
|
+
|
2021
|
+
self._notify(_complete)
|
2022
|
+
|
1545
2023
|
def _apply_status_style(self, status: str) -> None:
|
1546
2024
|
color = STATUS_COLORS.get(status.lower())
|
1547
2025
|
if color:
|
@@ -1580,6 +2058,8 @@ class TalksReducerGUI:
|
|
1580
2058
|
self.status_frame.grid()
|
1581
2059
|
self.stop_button.grid()
|
1582
2060
|
self.drop_hint_button.grid_remove()
|
2061
|
+
else:
|
2062
|
+
self._reset_audio_progress_state(clear_source=True)
|
1583
2063
|
|
1584
2064
|
if lowered == "success" or "time:" in lowered and "size:" in lowered:
|
1585
2065
|
if self.simple_mode_var.get() and hasattr(self, "status_frame"):
|
@@ -1592,12 +2072,7 @@ class TalksReducerGUI:
|
|
1592
2072
|
else:
|
1593
2073
|
self.open_button.grid_remove()
|
1594
2074
|
# print("not success status")
|
1595
|
-
if (
|
1596
|
-
self.simple_mode_var.get()
|
1597
|
-
and not is_processing
|
1598
|
-
and hasattr(self, "status_frame")
|
1599
|
-
):
|
1600
|
-
self.status_frame.grid_remove()
|
2075
|
+
if self.simple_mode_var.get() and not is_processing:
|
1601
2076
|
self.stop_button.grid_remove()
|
1602
2077
|
# Show drop hint when no other buttons are visible
|
1603
2078
|
if hasattr(self, "drop_hint_button"):
|