talks-reducer 0.6.3__py3-none-any.whl → 0.7.1__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 +9 -3
- talks_reducer/{gui.py → gui/__init__.py} +375 -1192
- talks_reducer/gui/__main__.py +8 -0
- talks_reducer/gui/discovery.py +126 -0
- talks_reducer/gui/layout.py +526 -0
- talks_reducer/gui/preferences.py +113 -0
- talks_reducer/gui/remote.py +356 -0
- talks_reducer/gui/theme.py +269 -0
- talks_reducer/models.py +1 -1
- talks_reducer/pipeline.py +142 -92
- talks_reducer/server.py +52 -4
- talks_reducer/service_client.py +56 -4
- {talks_reducer-0.6.3.dist-info → talks_reducer-0.7.1.dist-info}/METADATA +14 -5
- talks_reducer-0.7.1.dist-info/RECORD +29 -0
- talks_reducer-0.6.3.dist-info/RECORD +0 -23
- {talks_reducer-0.6.3.dist-info → talks_reducer-0.7.1.dist-info}/WHEEL +0 -0
- {talks_reducer-0.6.3.dist-info → talks_reducer-0.7.1.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.6.3.dist-info → talks_reducer-0.7.1.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.6.3.dist-info → talks_reducer-0.7.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,269 @@
|
|
1
|
+
"""Theme utilities shared by the Tkinter GUI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import subprocess
|
6
|
+
from typing import Any, Callable, Mapping, Sequence
|
7
|
+
|
8
|
+
STATUS_COLORS = {
|
9
|
+
"idle": "#9ca3af",
|
10
|
+
"waiting": "#9ca3af",
|
11
|
+
"processing": "#af8e0e",
|
12
|
+
"success": "#178941",
|
13
|
+
"error": "#ad4f4f",
|
14
|
+
"aborted": "#6d727a",
|
15
|
+
}
|
16
|
+
|
17
|
+
LIGHT_THEME = {
|
18
|
+
"background": "#f5f5f5",
|
19
|
+
"foreground": "#1f2933",
|
20
|
+
"accent": "#2563eb",
|
21
|
+
"surface": "#ffffff",
|
22
|
+
"border": "#cbd5e1",
|
23
|
+
"hover": "#efefef",
|
24
|
+
"hover_text": "#000000",
|
25
|
+
"selection_background": "#2563eb",
|
26
|
+
"selection_foreground": "#ffffff",
|
27
|
+
}
|
28
|
+
|
29
|
+
DARK_THEME = {
|
30
|
+
"background": "#1e1e28",
|
31
|
+
"foreground": "#f3f4f6",
|
32
|
+
"accent": "#60a5fa",
|
33
|
+
"surface": "#2b2b3c",
|
34
|
+
"border": "#4b5563",
|
35
|
+
"hover": "#333333",
|
36
|
+
"hover_text": "#ffffff",
|
37
|
+
"selection_background": "#333333",
|
38
|
+
"selection_foreground": "#f3f4f6",
|
39
|
+
}
|
40
|
+
|
41
|
+
|
42
|
+
RegistryReader = Callable[[str, str], int]
|
43
|
+
DefaultsRunner = Callable[[Sequence[str]], subprocess.CompletedProcess[str]]
|
44
|
+
|
45
|
+
|
46
|
+
def read_windows_theme_registry(key_path: str, value_name: str) -> int:
|
47
|
+
"""Read *value_name* from the registry key at *key_path*."""
|
48
|
+
|
49
|
+
import winreg # type: ignore
|
50
|
+
|
51
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key:
|
52
|
+
value, _ = winreg.QueryValueEx(key, value_name)
|
53
|
+
return int(value)
|
54
|
+
|
55
|
+
|
56
|
+
def run_defaults_command(args: Sequence[str]) -> subprocess.CompletedProcess[str]:
|
57
|
+
"""Execute the macOS ``defaults`` command used to detect theme."""
|
58
|
+
|
59
|
+
return subprocess.run(args, capture_output=True, text=True, check=False)
|
60
|
+
|
61
|
+
|
62
|
+
def detect_system_theme(
|
63
|
+
env: Mapping[str, str],
|
64
|
+
platform: str,
|
65
|
+
registry_reader: RegistryReader,
|
66
|
+
defaults_runner: DefaultsRunner,
|
67
|
+
) -> str:
|
68
|
+
"""Detect the system theme for the provided *platform* and environment."""
|
69
|
+
|
70
|
+
if platform.startswith("win"):
|
71
|
+
try:
|
72
|
+
value = registry_reader(
|
73
|
+
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
74
|
+
"AppsUseLightTheme",
|
75
|
+
)
|
76
|
+
return "light" if int(value) else "dark"
|
77
|
+
except OSError:
|
78
|
+
return "light"
|
79
|
+
|
80
|
+
if platform == "darwin":
|
81
|
+
try:
|
82
|
+
result = defaults_runner(["defaults", "read", "-g", "AppleInterfaceStyle"])
|
83
|
+
except Exception:
|
84
|
+
return "light"
|
85
|
+
if result.returncode == 0 and result.stdout.strip().lower() == "dark":
|
86
|
+
return "dark"
|
87
|
+
return "light"
|
88
|
+
|
89
|
+
theme = env.get("GTK_THEME", "").lower()
|
90
|
+
if "dark" in theme:
|
91
|
+
return "dark"
|
92
|
+
return "light"
|
93
|
+
|
94
|
+
|
95
|
+
def apply_theme(
|
96
|
+
style: Any,
|
97
|
+
palette: Mapping[str, str],
|
98
|
+
widgets: Mapping[str, Any],
|
99
|
+
) -> Mapping[str, str]:
|
100
|
+
"""Apply *palette* to *style* and update GUI *widgets*."""
|
101
|
+
|
102
|
+
root = widgets.get("root")
|
103
|
+
if root is not None:
|
104
|
+
root.configure(bg=palette["background"])
|
105
|
+
|
106
|
+
style.theme_use("clam")
|
107
|
+
style.configure(
|
108
|
+
".", background=palette["background"], foreground=palette["foreground"]
|
109
|
+
)
|
110
|
+
style.configure("TFrame", background=palette["background"])
|
111
|
+
style.configure(
|
112
|
+
"TLabelframe",
|
113
|
+
background=palette["background"],
|
114
|
+
foreground=palette["foreground"],
|
115
|
+
borderwidth=0,
|
116
|
+
relief="flat",
|
117
|
+
)
|
118
|
+
style.configure(
|
119
|
+
"TLabelframe.Label",
|
120
|
+
background=palette["background"],
|
121
|
+
foreground=palette["foreground"],
|
122
|
+
)
|
123
|
+
style.configure(
|
124
|
+
"TLabel", background=palette["background"], foreground=palette["foreground"]
|
125
|
+
)
|
126
|
+
style.configure(
|
127
|
+
"TCheckbutton",
|
128
|
+
background=palette["background"],
|
129
|
+
foreground=palette["foreground"],
|
130
|
+
)
|
131
|
+
style.map(
|
132
|
+
"TCheckbutton",
|
133
|
+
background=[("active", palette.get("hover", palette["background"]))],
|
134
|
+
)
|
135
|
+
style.configure(
|
136
|
+
"TRadiobutton",
|
137
|
+
background=palette["background"],
|
138
|
+
foreground=palette["foreground"],
|
139
|
+
)
|
140
|
+
style.map(
|
141
|
+
"TRadiobutton",
|
142
|
+
background=[("active", palette.get("hover", palette["background"]))],
|
143
|
+
)
|
144
|
+
style.configure(
|
145
|
+
"Link.TButton",
|
146
|
+
background=palette["background"],
|
147
|
+
foreground=palette["accent"],
|
148
|
+
borderwidth=0,
|
149
|
+
relief="flat",
|
150
|
+
highlightthickness=0,
|
151
|
+
padding=2,
|
152
|
+
font=("TkDefaultFont", 8, "underline"),
|
153
|
+
)
|
154
|
+
style.map(
|
155
|
+
"Link.TButton",
|
156
|
+
background=[
|
157
|
+
("active", palette.get("hover", palette["background"])),
|
158
|
+
("disabled", palette["background"]),
|
159
|
+
],
|
160
|
+
foreground=[
|
161
|
+
("active", palette.get("accent", palette["foreground"])),
|
162
|
+
("disabled", palette["foreground"]),
|
163
|
+
],
|
164
|
+
)
|
165
|
+
style.configure(
|
166
|
+
"TButton",
|
167
|
+
background=palette["surface"],
|
168
|
+
foreground=palette["foreground"],
|
169
|
+
padding=4,
|
170
|
+
font=("TkDefaultFont", 8),
|
171
|
+
)
|
172
|
+
style.map(
|
173
|
+
"TButton",
|
174
|
+
background=[
|
175
|
+
("active", palette.get("hover", palette["accent"])),
|
176
|
+
("disabled", palette["surface"]),
|
177
|
+
],
|
178
|
+
foreground=[
|
179
|
+
("active", palette.get("hover_text", "#000000")),
|
180
|
+
("disabled", palette["foreground"]),
|
181
|
+
],
|
182
|
+
)
|
183
|
+
style.configure(
|
184
|
+
"TEntry",
|
185
|
+
fieldbackground=palette["surface"],
|
186
|
+
foreground=palette["foreground"],
|
187
|
+
)
|
188
|
+
style.configure(
|
189
|
+
"TCombobox",
|
190
|
+
fieldbackground=palette["surface"],
|
191
|
+
foreground=palette["foreground"],
|
192
|
+
)
|
193
|
+
|
194
|
+
style.configure(
|
195
|
+
"Idle.Horizontal.TProgressbar",
|
196
|
+
background=STATUS_COLORS["idle"],
|
197
|
+
troughcolor=palette["surface"],
|
198
|
+
borderwidth=0,
|
199
|
+
thickness=20,
|
200
|
+
)
|
201
|
+
style.configure(
|
202
|
+
"Processing.Horizontal.TProgressbar",
|
203
|
+
background=STATUS_COLORS["processing"],
|
204
|
+
troughcolor=palette["surface"],
|
205
|
+
borderwidth=0,
|
206
|
+
thickness=20,
|
207
|
+
)
|
208
|
+
style.configure(
|
209
|
+
"Success.Horizontal.TProgressbar",
|
210
|
+
background=STATUS_COLORS["success"],
|
211
|
+
troughcolor=palette["surface"],
|
212
|
+
borderwidth=0,
|
213
|
+
thickness=20,
|
214
|
+
)
|
215
|
+
style.configure(
|
216
|
+
"Error.Horizontal.TProgressbar",
|
217
|
+
background=STATUS_COLORS["error"],
|
218
|
+
troughcolor=palette["surface"],
|
219
|
+
borderwidth=0,
|
220
|
+
thickness=20,
|
221
|
+
)
|
222
|
+
style.configure(
|
223
|
+
"Aborted.Horizontal.TProgressbar",
|
224
|
+
background=STATUS_COLORS["aborted"],
|
225
|
+
troughcolor=palette["surface"],
|
226
|
+
borderwidth=0,
|
227
|
+
thickness=20,
|
228
|
+
)
|
229
|
+
|
230
|
+
drop_zone = widgets.get("drop_zone")
|
231
|
+
if drop_zone is not None:
|
232
|
+
drop_zone.configure(
|
233
|
+
bg=palette["surface"],
|
234
|
+
fg=palette["foreground"],
|
235
|
+
highlightthickness=0,
|
236
|
+
)
|
237
|
+
|
238
|
+
tk_module = widgets.get("tk")
|
239
|
+
slider_relief = getattr(tk_module, "FLAT", "flat") if tk_module else "flat"
|
240
|
+
sliders = widgets.get("sliders") or []
|
241
|
+
for slider in sliders:
|
242
|
+
slider.configure(
|
243
|
+
background=palette["border"],
|
244
|
+
troughcolor=palette["surface"],
|
245
|
+
activebackground=palette["border"],
|
246
|
+
sliderrelief=slider_relief,
|
247
|
+
bd=0,
|
248
|
+
)
|
249
|
+
|
250
|
+
log_text = widgets.get("log_text")
|
251
|
+
if log_text is not None:
|
252
|
+
log_text.configure(
|
253
|
+
bg=palette["surface"],
|
254
|
+
fg=palette["foreground"],
|
255
|
+
insertbackground=palette["foreground"],
|
256
|
+
highlightbackground=palette["border"],
|
257
|
+
highlightcolor=palette["border"],
|
258
|
+
)
|
259
|
+
|
260
|
+
status_label = widgets.get("status_label")
|
261
|
+
if status_label is not None:
|
262
|
+
status_label.configure(bg=palette["background"])
|
263
|
+
|
264
|
+
apply_status_style = widgets.get("apply_status_style")
|
265
|
+
status_state = widgets.get("status_state")
|
266
|
+
if callable(apply_status_style) and status_state is not None:
|
267
|
+
apply_status_style(status_state)
|
268
|
+
|
269
|
+
return palette
|
talks_reducer/models.py
CHANGED
talks_reducer/pipeline.py
CHANGED
@@ -23,103 +23,39 @@ from .ffmpeg import (
|
|
23
23
|
)
|
24
24
|
from .models import ProcessingOptions, ProcessingResult
|
25
25
|
from .progress import NullProgressReporter, ProgressReporter
|
26
|
+
from talks_reducer.version_utils import resolve_version
|
26
27
|
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
suffix_parts = []
|
31
|
-
|
32
|
-
if small:
|
33
|
-
suffix_parts.append("_small")
|
34
|
-
|
35
|
-
if not suffix_parts:
|
36
|
-
suffix_parts.append("") # Default case
|
37
|
-
|
38
|
-
suffix = "_speedup" + "".join(suffix_parts)
|
39
|
-
new_name = (
|
40
|
-
filename.name[:dot_index] + suffix + filename.name[dot_index:]
|
41
|
-
if dot_index != -1
|
42
|
-
else filename.name + suffix
|
43
|
-
)
|
44
|
-
return filename.with_name(new_name)
|
45
|
-
|
46
|
-
|
47
|
-
def _create_path(path: Path) -> None:
|
48
|
-
try:
|
49
|
-
path.mkdir(parents=True, exist_ok=True)
|
50
|
-
except OSError as exc: # pragma: no cover - defensive logging
|
51
|
-
raise AssertionError(
|
52
|
-
"Creation of the directory failed. (The TEMP folder may already exist. Delete or rename it, and try again.)"
|
53
|
-
) from exc
|
54
|
-
|
55
|
-
|
56
|
-
def _delete_path(path: Path) -> None:
|
57
|
-
import time
|
58
|
-
from shutil import rmtree
|
59
|
-
|
60
|
-
try:
|
61
|
-
rmtree(path, ignore_errors=False)
|
62
|
-
for i in range(5):
|
63
|
-
if not path.exists():
|
64
|
-
return
|
65
|
-
time.sleep(0.01 * i)
|
66
|
-
except OSError as exc: # pragma: no cover - defensive logging
|
67
|
-
print(f"Deletion of the directory {path} failed")
|
68
|
-
print(exc)
|
69
|
-
|
70
|
-
|
71
|
-
def _extract_video_metadata(input_file: Path, frame_rate: float) -> Dict[str, float]:
|
72
|
-
from .ffmpeg import get_ffprobe_path
|
73
|
-
|
74
|
-
ffprobe_path = get_ffprobe_path()
|
75
|
-
command = [
|
76
|
-
ffprobe_path,
|
77
|
-
"-i",
|
78
|
-
os.fspath(input_file),
|
79
|
-
"-hide_banner",
|
80
|
-
"-loglevel",
|
81
|
-
"error",
|
82
|
-
"-select_streams",
|
83
|
-
"v",
|
84
|
-
"-show_entries",
|
85
|
-
"format=duration:stream=avg_frame_rate,nb_frames",
|
86
|
-
]
|
87
|
-
process = subprocess.Popen(
|
88
|
-
command,
|
89
|
-
stdout=subprocess.PIPE,
|
90
|
-
stderr=subprocess.PIPE,
|
91
|
-
bufsize=1,
|
92
|
-
universal_newlines=True,
|
93
|
-
)
|
94
|
-
stdout, _ = process.communicate()
|
29
|
+
class ProcessingAborted(RuntimeError):
|
30
|
+
"""Raised when processing is cancelled by the caller."""
|
95
31
|
|
96
|
-
match_frame_rate = re.search(r"frame_rate=(\d*)/(\d*)", str(stdout))
|
97
|
-
if match_frame_rate is not None:
|
98
|
-
frame_rate = float(match_frame_rate.group(1)) / float(match_frame_rate.group(2))
|
99
32
|
|
100
|
-
|
101
|
-
|
33
|
+
def _stop_requested(reporter: ProgressReporter | None) -> bool:
|
34
|
+
"""Return ``True`` when *reporter* indicates that processing should stop."""
|
102
35
|
|
103
|
-
|
104
|
-
|
36
|
+
if reporter is None:
|
37
|
+
return False
|
105
38
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
39
|
+
flag = getattr(reporter, "stop_requested", None)
|
40
|
+
if callable(flag):
|
41
|
+
try:
|
42
|
+
flag = flag()
|
43
|
+
except Exception: # pragma: no cover - defensive
|
44
|
+
flag = False
|
45
|
+
return bool(flag)
|
111
46
|
|
112
47
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
116
|
-
|
48
|
+
def _raise_if_stopped(
|
49
|
+
reporter: ProgressReporter | None, *, temp_path: Path | None = None
|
50
|
+
) -> None:
|
51
|
+
"""Abort processing when the user has requested a stop."""
|
117
52
|
|
53
|
+
if not _stop_requested(reporter):
|
54
|
+
return
|
118
55
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
return output_audio_data
|
56
|
+
if temp_path is not None and temp_path.exists():
|
57
|
+
_delete_path(temp_path)
|
58
|
+
raise ProcessingAborted("Processing aborted by user request.")
|
123
59
|
|
124
60
|
|
125
61
|
def speed_up_video(
|
@@ -152,10 +88,14 @@ def speed_up_video(
|
|
152
88
|
original_duration = metadata["duration"]
|
153
89
|
frame_count = metadata.get("frame_count", 0)
|
154
90
|
|
91
|
+
app_version = resolve_version()
|
92
|
+
if app_version and app_version != "unknown":
|
93
|
+
reporter.log(f"talks-reducer v{app_version}")
|
94
|
+
|
155
95
|
reporter.log(
|
156
96
|
(
|
157
|
-
"Source metadata
|
158
|
-
"
|
97
|
+
"Source metadata: duration: {duration:.2f}s, frame rate: {fps:.3f} fps,"
|
98
|
+
" frames: {frames}"
|
159
99
|
).format(
|
160
100
|
duration=original_duration,
|
161
101
|
fps=frame_rate,
|
@@ -186,6 +126,7 @@ def speed_up_video(
|
|
186
126
|
ffmpeg_path=ffmpeg_path,
|
187
127
|
)
|
188
128
|
|
129
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
189
130
|
reporter.log("Extracting audio...")
|
190
131
|
process_callback = getattr(reporter, "process_callback", None)
|
191
132
|
estimated_total_frames = frame_count
|
@@ -211,12 +152,13 @@ def speed_up_video(
|
|
211
152
|
audio_sample_count = audio_data.shape[0]
|
212
153
|
max_audio_volume = audio_utils.get_max_volume(audio_data)
|
213
154
|
|
214
|
-
reporter.log("
|
215
|
-
reporter.log(f"- Max Audio Volume: {max_audio_volume}")
|
155
|
+
reporter.log(f"Max Audio Volume: {max_audio_volume}")
|
216
156
|
|
217
157
|
samples_per_frame = wav_sample_rate / frame_rate
|
218
158
|
audio_frame_count = int(math.ceil(audio_sample_count / samples_per_frame))
|
219
159
|
|
160
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
161
|
+
|
220
162
|
has_loud_audio = chunk_utils.detect_loud_frames(
|
221
163
|
audio_data,
|
222
164
|
audio_frame_count,
|
@@ -227,7 +169,9 @@ def speed_up_video(
|
|
227
169
|
|
228
170
|
chunks, _ = chunk_utils.build_chunks(has_loud_audio, options.frame_spreadage)
|
229
171
|
|
230
|
-
reporter.log(f"
|
172
|
+
reporter.log(f"Processing {len(chunks)} chunks...")
|
173
|
+
|
174
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
231
175
|
|
232
176
|
new_speeds = [options.silent_speed, options.sounded_speed]
|
233
177
|
output_audio_data, updated_chunks = audio_utils.process_audio_chunks(
|
@@ -248,6 +192,8 @@ def speed_up_video(
|
|
248
192
|
_prepare_output_audio(output_audio_data),
|
249
193
|
)
|
250
194
|
|
195
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
196
|
+
|
251
197
|
expression = chunk_utils.get_tree_expression(updated_chunks)
|
252
198
|
filter_graph_path = temp_path / "filterGraph.txt"
|
253
199
|
with open(filter_graph_path, "w", encoding="utf-8") as filter_graph_file:
|
@@ -285,6 +231,8 @@ def speed_up_video(
|
|
285
231
|
_delete_path(temp_path)
|
286
232
|
raise FileNotFoundError("Filter graph file was not generated")
|
287
233
|
|
234
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
235
|
+
|
288
236
|
try:
|
289
237
|
final_total_frames = updated_chunks[-1][3] if updated_chunks else 0
|
290
238
|
if final_total_frames > 0:
|
@@ -315,6 +263,8 @@ def speed_up_video(
|
|
315
263
|
)
|
316
264
|
except subprocess.CalledProcessError as exc:
|
317
265
|
if fallback_command_str and use_cuda_encoder:
|
266
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
267
|
+
|
318
268
|
reporter.log("CUDA encoding failed, retrying with CPU encoder...")
|
319
269
|
if final_total_frames > 0:
|
320
270
|
reporter.log(
|
@@ -364,3 +314,103 @@ def speed_up_video(
|
|
364
314
|
time_ratio=time_ratio,
|
365
315
|
size_ratio=size_ratio,
|
366
316
|
)
|
317
|
+
|
318
|
+
|
319
|
+
def _input_to_output_filename(filename: Path, small: bool = False) -> Path:
|
320
|
+
dot_index = filename.name.rfind(".")
|
321
|
+
suffix_parts = []
|
322
|
+
|
323
|
+
if small:
|
324
|
+
suffix_parts.append("_small")
|
325
|
+
|
326
|
+
if not suffix_parts:
|
327
|
+
suffix_parts.append("") # Default case
|
328
|
+
|
329
|
+
suffix = "_speedup" + "".join(suffix_parts)
|
330
|
+
new_name = (
|
331
|
+
filename.name[:dot_index] + suffix + filename.name[dot_index:]
|
332
|
+
if dot_index != -1
|
333
|
+
else filename.name + suffix
|
334
|
+
)
|
335
|
+
return filename.with_name(new_name)
|
336
|
+
|
337
|
+
|
338
|
+
def _create_path(path: Path) -> None:
|
339
|
+
try:
|
340
|
+
path.mkdir(parents=True, exist_ok=True)
|
341
|
+
except OSError as exc: # pragma: no cover - defensive logging
|
342
|
+
raise AssertionError(
|
343
|
+
"Creation of the directory failed. (The TEMP folder may already exist. Delete or rename it, and try again.)"
|
344
|
+
) from exc
|
345
|
+
|
346
|
+
|
347
|
+
def _delete_path(path: Path) -> None:
|
348
|
+
import time
|
349
|
+
from shutil import rmtree
|
350
|
+
|
351
|
+
if not path.exists():
|
352
|
+
return
|
353
|
+
|
354
|
+
try:
|
355
|
+
rmtree(path, ignore_errors=False)
|
356
|
+
for i in range(5):
|
357
|
+
if not path.exists():
|
358
|
+
return
|
359
|
+
time.sleep(0.01 * i)
|
360
|
+
except OSError as exc: # pragma: no cover - defensive logging
|
361
|
+
print(f"Deletion of the directory {path} failed")
|
362
|
+
print(exc)
|
363
|
+
|
364
|
+
|
365
|
+
def _extract_video_metadata(input_file: Path, frame_rate: float) -> Dict[str, float]:
|
366
|
+
from .ffmpeg import get_ffprobe_path
|
367
|
+
|
368
|
+
ffprobe_path = get_ffprobe_path()
|
369
|
+
command = [
|
370
|
+
ffprobe_path,
|
371
|
+
"-i",
|
372
|
+
os.fspath(input_file),
|
373
|
+
"-hide_banner",
|
374
|
+
"-loglevel",
|
375
|
+
"error",
|
376
|
+
"-select_streams",
|
377
|
+
"v",
|
378
|
+
"-show_entries",
|
379
|
+
"format=duration:stream=avg_frame_rate,nb_frames",
|
380
|
+
]
|
381
|
+
process = subprocess.Popen(
|
382
|
+
command,
|
383
|
+
stdout=subprocess.PIPE,
|
384
|
+
stderr=subprocess.PIPE,
|
385
|
+
bufsize=1,
|
386
|
+
universal_newlines=True,
|
387
|
+
)
|
388
|
+
stdout, _ = process.communicate()
|
389
|
+
|
390
|
+
match_frame_rate = re.search(r"frame_rate=(\d*)/(\d*)", str(stdout))
|
391
|
+
if match_frame_rate is not None:
|
392
|
+
frame_rate = float(match_frame_rate.group(1)) / float(match_frame_rate.group(2))
|
393
|
+
|
394
|
+
match_duration = re.search(r"duration=([\d.]*)", str(stdout))
|
395
|
+
original_duration = float(match_duration.group(1)) if match_duration else 0.0
|
396
|
+
|
397
|
+
match_frames = re.search(r"nb_frames=(\d+)", str(stdout))
|
398
|
+
frame_count = int(match_frames.group(1)) if match_frames else 0
|
399
|
+
|
400
|
+
return {
|
401
|
+
"frame_rate": frame_rate,
|
402
|
+
"duration": original_duration,
|
403
|
+
"frame_count": frame_count,
|
404
|
+
}
|
405
|
+
|
406
|
+
|
407
|
+
def _ensure_two_dimensional(audio_data: np.ndarray) -> np.ndarray:
|
408
|
+
if audio_data.ndim == 1:
|
409
|
+
return audio_data[:, np.newaxis]
|
410
|
+
return audio_data
|
411
|
+
|
412
|
+
|
413
|
+
def _prepare_output_audio(output_audio_data: np.ndarray) -> np.ndarray:
|
414
|
+
if output_audio_data.ndim == 2 and output_audio_data.shape[1] == 1:
|
415
|
+
return output_audio_data[:, 0]
|
416
|
+
return output_audio_data
|
talks_reducer/server.py
CHANGED
@@ -11,7 +11,7 @@ from contextlib import AbstractContextManager, suppress
|
|
11
11
|
from pathlib import Path
|
12
12
|
from queue import SimpleQueue
|
13
13
|
from threading import Thread
|
14
|
-
from typing import Callable, Iterator, Optional, Sequence
|
14
|
+
from typing import Callable, Iterator, Optional, Sequence, cast
|
15
15
|
|
16
16
|
import gradio as gr
|
17
17
|
|
@@ -248,6 +248,9 @@ def _format_summary(result: ProcessingResult) -> str:
|
|
248
248
|
def process_video(
|
249
249
|
file_path: Optional[str],
|
250
250
|
small_video: bool,
|
251
|
+
silent_threshold: Optional[float] = None,
|
252
|
+
sounded_speed: Optional[float] = None,
|
253
|
+
silent_speed: Optional[float] = None,
|
251
254
|
progress: Optional[gr.Progress] = gr.Progress(track_tqdm=False),
|
252
255
|
) -> Iterator[tuple[Optional[str], str, str, Optional[str]]]:
|
253
256
|
"""Run the Talks Reducer pipeline for a single uploaded file."""
|
@@ -267,7 +270,7 @@ def process_video(
|
|
267
270
|
if progress is not None:
|
268
271
|
|
269
272
|
def _callback(current: int, total: int, desc: str) -> None:
|
270
|
-
progress(current, total
|
273
|
+
events.put(("progress", (current, total, desc)))
|
271
274
|
|
272
275
|
progress_callback = _callback
|
273
276
|
|
@@ -281,11 +284,20 @@ def process_video(
|
|
281
284
|
log_callback=_log_callback,
|
282
285
|
)
|
283
286
|
|
287
|
+
option_kwargs: dict[str, float] = {}
|
288
|
+
if silent_threshold is not None:
|
289
|
+
option_kwargs["silent_threshold"] = float(silent_threshold)
|
290
|
+
if sounded_speed is not None:
|
291
|
+
option_kwargs["sounded_speed"] = float(sounded_speed)
|
292
|
+
if silent_speed is not None:
|
293
|
+
option_kwargs["silent_speed"] = float(silent_speed)
|
294
|
+
|
284
295
|
options = ProcessingOptions(
|
285
296
|
input_file=input_path,
|
286
297
|
output_file=output_file,
|
287
298
|
temp_folder=temp_folder,
|
288
299
|
small=small_video,
|
300
|
+
**option_kwargs,
|
289
301
|
)
|
290
302
|
|
291
303
|
def _worker() -> None:
|
@@ -323,6 +335,11 @@ def process_video(
|
|
323
335
|
gr.update(),
|
324
336
|
gr.update(),
|
325
337
|
)
|
338
|
+
elif kind == "progress":
|
339
|
+
if progress is not None:
|
340
|
+
current, total, desc = cast(tuple[int, int, str], payload)
|
341
|
+
percent = current / total if total > 0 else 0
|
342
|
+
progress(percent, total=total, desc=desc)
|
326
343
|
elif kind == "result":
|
327
344
|
final_result = payload # type: ignore[assignment]
|
328
345
|
elif kind == "error":
|
@@ -371,14 +388,39 @@ def build_interface() -> gr.Blocks:
|
|
371
388
|
""".strip()
|
372
389
|
)
|
373
390
|
|
374
|
-
with gr.
|
391
|
+
with gr.Column():
|
375
392
|
file_input = gr.File(
|
376
393
|
label="Video file",
|
377
394
|
file_types=["video"],
|
378
395
|
type="filepath",
|
379
396
|
)
|
397
|
+
|
398
|
+
with gr.Row():
|
380
399
|
small_checkbox = gr.Checkbox(label="Small video", value=True)
|
381
400
|
|
401
|
+
with gr.Column():
|
402
|
+
silent_speed_input = gr.Slider(
|
403
|
+
minimum=1.0,
|
404
|
+
maximum=10.0,
|
405
|
+
value=4.0,
|
406
|
+
step=0.1,
|
407
|
+
label="Silent speed",
|
408
|
+
)
|
409
|
+
sounded_speed_input = gr.Slider(
|
410
|
+
minimum=0.5,
|
411
|
+
maximum=3.0,
|
412
|
+
value=1.0,
|
413
|
+
step=0.05,
|
414
|
+
label="Sounded speed",
|
415
|
+
)
|
416
|
+
silent_threshold_input = gr.Slider(
|
417
|
+
minimum=0.0,
|
418
|
+
maximum=1.0,
|
419
|
+
value=0.05,
|
420
|
+
step=0.01,
|
421
|
+
label="Silent threshold",
|
422
|
+
)
|
423
|
+
|
382
424
|
video_output = gr.Video(label="Processed video")
|
383
425
|
summary_output = gr.Markdown()
|
384
426
|
download_output = gr.File(label="Download processed file", interactive=False)
|
@@ -386,7 +428,13 @@ def build_interface() -> gr.Blocks:
|
|
386
428
|
|
387
429
|
file_input.upload(
|
388
430
|
process_video,
|
389
|
-
inputs=[
|
431
|
+
inputs=[
|
432
|
+
file_input,
|
433
|
+
small_checkbox,
|
434
|
+
silent_threshold_input,
|
435
|
+
sounded_speed_input,
|
436
|
+
silent_speed_input,
|
437
|
+
],
|
390
438
|
outputs=[video_output, log_output, summary_output, download_output],
|
391
439
|
queue=True,
|
392
440
|
api_name="process_video",
|