talks-reducer 0.6.3__py3-none-any.whl → 0.7.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.
@@ -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
@@ -29,7 +29,7 @@ def default_temp_folder() -> Path:
29
29
  else:
30
30
  base = Path(tempfile.gettempdir()) / "talks-reducer"
31
31
 
32
- return base / "temp"
32
+ return base / "talks-reducer-temp"
33
33
 
34
34
 
35
35
  @dataclass(frozen=True)
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
- def _input_to_output_filename(filename: Path, small: bool = False) -> Path:
29
- dot_index = filename.name.rfind(".")
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
- match_duration = re.search(r"duration=([\d.]*)", str(stdout))
101
- original_duration = float(match_duration.group(1)) if match_duration else 0.0
33
+ def _stop_requested(reporter: ProgressReporter | None) -> bool:
34
+ """Return ``True`` when *reporter* indicates that processing should stop."""
102
35
 
103
- match_frames = re.search(r"nb_frames=(\d+)", str(stdout))
104
- frame_count = int(match_frames.group(1)) if match_frames else 0
36
+ if reporter is None:
37
+ return False
105
38
 
106
- return {
107
- "frame_rate": frame_rate,
108
- "duration": original_duration,
109
- "frame_count": frame_count,
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 _ensure_two_dimensional(audio_data: np.ndarray) -> np.ndarray:
114
- if audio_data.ndim == 1:
115
- return audio_data[:, np.newaxis]
116
- return audio_data
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
- def _prepare_output_audio(output_audio_data: np.ndarray) -> np.ndarray:
120
- if output_audio_data.ndim == 2 and output_audio_data.shape[1] == 1:
121
- return output_audio_data[:, 0]
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 duration: {duration:.2f}s, frame rate: {fps:.3f} fps,"
158
- " reported frames: {frames}"
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("\nInformation:")
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"Generated {len(chunks)} chunks")
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=total, desc=desc)
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.Row():
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=[file_input, small_checkbox],
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",