talks-reducer 0.5.3__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {talks_reducer-0.5.3/talks_reducer.egg-info → talks_reducer-0.6.0}/PKG-INFO +28 -1
  2. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/README.md +27 -0
  3. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/__about__.py +1 -1
  4. talks_reducer-0.6.0/talks_reducer/cli.py +515 -0
  5. talks_reducer-0.6.0/talks_reducer/discovery.py +149 -0
  6. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/gui.py +846 -143
  7. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/pipeline.py +1 -1
  8. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/server.py +104 -23
  9. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/server_tray.py +215 -16
  10. talks_reducer-0.6.0/talks_reducer/service_client.py +356 -0
  11. {talks_reducer-0.5.3 → talks_reducer-0.6.0/talks_reducer.egg-info}/PKG-INFO +28 -1
  12. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer.egg-info/SOURCES.txt +3 -0
  13. talks_reducer-0.6.0/tests/test_cli.py +261 -0
  14. talks_reducer-0.6.0/tests/test_discovery.py +119 -0
  15. talks_reducer-0.6.0/tests/test_gui_summary_parsing.py +50 -0
  16. talks_reducer-0.6.0/tests/test_server.py +181 -0
  17. talks_reducer-0.6.0/tests/test_service_client.py +238 -0
  18. talks_reducer-0.5.3/talks_reducer/cli.py +0 -252
  19. talks_reducer-0.5.3/talks_reducer/service_client.py +0 -102
  20. talks_reducer-0.5.3/tests/test_cli.py +0 -105
  21. talks_reducer-0.5.3/tests/test_server.py +0 -68
  22. talks_reducer-0.5.3/tests/test_service_client.py +0 -112
  23. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/LICENSE +0 -0
  24. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/pyproject.toml +0 -0
  25. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/setup.cfg +0 -0
  26. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/__init__.py +0 -0
  27. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/__main__.py +0 -0
  28. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/audio.py +0 -0
  29. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/chunks.py +0 -0
  30. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/ffmpeg.py +0 -0
  31. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/models.py +0 -0
  32. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer/progress.py +0 -0
  33. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer.egg-info/dependency_links.txt +0 -0
  34. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer.egg-info/entry_points.txt +0 -0
  35. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer.egg-info/requires.txt +0 -0
  36. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/talks_reducer.egg-info/top_level.txt +0 -0
  37. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/tests/test_audio.py +0 -0
  38. {talks_reducer-0.5.3 → talks_reducer-0.6.0}/tests/test_pipeline_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: talks-reducer
3
- Version: 0.5.3
3
+ Version: 0.6.0
4
4
  Summary: CLI for speeding up long-form talks by removing silence
5
5
  Author: Talks Reducer Maintainers
6
6
  License-Expression: MIT
@@ -70,6 +70,19 @@ Example CLI usage:
70
70
  talks-reducer --small input.mp4
71
71
  ```
72
72
 
73
+ Need to offload work to a remote Talks Reducer server? Pass `--url` with the
74
+ server address and the CLI will upload the input, wait for processing to finish,
75
+ and download the rendered video. You can also provide `--host` to expand to the
76
+ default Talks Reducer port (`http://<host>:9005`):
77
+
78
+ ```sh
79
+ talks-reducer --url http://localhost:9005 demo.mp4
80
+ talks-reducer --host 192.168.1.42 demo.mp4
81
+ ```
82
+
83
+ Want to see progress as the remote server works? Add `--server-stream` so the
84
+ CLI prints live progress bars and log lines while you wait for the download.
85
+
73
86
  ### Speech detection
74
87
 
75
88
  Talks Reducer now relies on its built-in volume thresholding to detect speech. Adjust `--silent_threshold` if you need to fine-tune when segments count as silence. Dropping the optional Silero VAD integration keeps the install lightweight and avoids pulling in PyTorch.
@@ -91,6 +104,10 @@ Want the server to live in your system tray instead of a terminal window? Use:
91
104
  talks-reducer server-tray
92
105
  ```
93
106
 
107
+ Bundled Windows builds include the same behaviour: run
108
+ `talks-reducer.exe --server` to launch the tray-managed server directly from the
109
+ desktop shortcut without opening the GUI first.
110
+
94
111
  Pass `--debug` to print verbose logs about the tray icon lifecycle, and
95
112
  `--tray-mode pystray-detached` to try pystray's alternate detached runner. If
96
113
  the icon backend refuses to appear, fall back to `--tray-mode headless` to keep
@@ -111,6 +128,16 @@ This opens a local web page featuring a drag-and-drop upload zone, a **Small vid
111
128
  progress indicator, and automatic previews of the processed output. Once the job completes you can inspect the resulting compression
112
129
  ratio and download the rendered video directly from the page.
113
130
 
131
+ The desktop GUI mirrors this behaviour. Open **Advanced** settings to provide a
132
+ server URL and click **Discover** to scan your local network for Talks Reducer
133
+ instances listening on port `9005`. The button now updates with the discovery
134
+ progress, showing the scanned/total host count as `scanned / total`. A new
135
+ **Processing mode** toggle lets you decide whether work stays local or uploads
136
+ to the configured server—the **Remote** option becomes available as soon as a
137
+ URL is supplied. Leave the toggle on **Local** to keep rendering on this
138
+ machine even if a server is saved; switch to **Remote** to hand jobs off while
139
+ the GUI downloads the finished files automatically.
140
+
114
141
  ### Uploading and retrieving a processed video
115
142
 
116
143
  1. Open the printed `http://localhost:<port>` address (the default port is `9005`).
@@ -42,6 +42,19 @@ Example CLI usage:
42
42
  talks-reducer --small input.mp4
43
43
  ```
44
44
 
45
+ Need to offload work to a remote Talks Reducer server? Pass `--url` with the
46
+ server address and the CLI will upload the input, wait for processing to finish,
47
+ and download the rendered video. You can also provide `--host` to expand to the
48
+ default Talks Reducer port (`http://<host>:9005`):
49
+
50
+ ```sh
51
+ talks-reducer --url http://localhost:9005 demo.mp4
52
+ talks-reducer --host 192.168.1.42 demo.mp4
53
+ ```
54
+
55
+ Want to see progress as the remote server works? Add `--server-stream` so the
56
+ CLI prints live progress bars and log lines while you wait for the download.
57
+
45
58
  ### Speech detection
46
59
 
47
60
  Talks Reducer now relies on its built-in volume thresholding to detect speech. Adjust `--silent_threshold` if you need to fine-tune when segments count as silence. Dropping the optional Silero VAD integration keeps the install lightweight and avoids pulling in PyTorch.
@@ -63,6 +76,10 @@ Want the server to live in your system tray instead of a terminal window? Use:
63
76
  talks-reducer server-tray
64
77
  ```
65
78
 
79
+ Bundled Windows builds include the same behaviour: run
80
+ `talks-reducer.exe --server` to launch the tray-managed server directly from the
81
+ desktop shortcut without opening the GUI first.
82
+
66
83
  Pass `--debug` to print verbose logs about the tray icon lifecycle, and
67
84
  `--tray-mode pystray-detached` to try pystray's alternate detached runner. If
68
85
  the icon backend refuses to appear, fall back to `--tray-mode headless` to keep
@@ -83,6 +100,16 @@ This opens a local web page featuring a drag-and-drop upload zone, a **Small vid
83
100
  progress indicator, and automatic previews of the processed output. Once the job completes you can inspect the resulting compression
84
101
  ratio and download the rendered video directly from the page.
85
102
 
103
+ The desktop GUI mirrors this behaviour. Open **Advanced** settings to provide a
104
+ server URL and click **Discover** to scan your local network for Talks Reducer
105
+ instances listening on port `9005`. The button now updates with the discovery
106
+ progress, showing the scanned/total host count as `scanned / total`. A new
107
+ **Processing mode** toggle lets you decide whether work stays local or uploads
108
+ to the configured server—the **Remote** option becomes available as soon as a
109
+ URL is supplied. Leave the toggle on **Local** to keep rendering on this
110
+ machine even if a server is saved; switch to **Remote** to hand jobs off while
111
+ the GUI downloads the finished files automatically.
112
+
86
113
  ### Uploading and retrieving a processed video
87
114
 
88
115
  1. Open the printed `http://localhost:<port>` address (the default port is `9005`).
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.5.3"
5
+ __version__ = "0.6.0"
@@ -0,0 +1,515 @@
1
+ """Command line interface for the talks reducer package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ from importlib import import_module
12
+ from importlib.metadata import PackageNotFoundError, version
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Sequence, Tuple
15
+
16
+ from . import audio
17
+
18
+ try:
19
+ from .__about__ import __version__ as _about_version
20
+ except Exception: # pragma: no cover - fallback if metadata file missing
21
+ _about_version = ""
22
+ from .ffmpeg import FFmpegNotFoundError
23
+ from .models import ProcessingOptions, default_temp_folder
24
+ from .pipeline import speed_up_video
25
+ from .progress import TqdmProgressReporter
26
+
27
+
28
+ def _build_parser() -> argparse.ArgumentParser:
29
+ """Create the argument parser used by the command line interface."""
30
+
31
+ parser = argparse.ArgumentParser(
32
+ description="Modifies a video file to play at different speeds when there is sound vs. silence.",
33
+ )
34
+
35
+ # Add version argument
36
+ pkg_version = _resolve_version()
37
+
38
+ parser.add_argument(
39
+ "--version",
40
+ action="version",
41
+ version=f"talks-reducer {pkg_version}",
42
+ )
43
+
44
+ parser.add_argument(
45
+ "input_file",
46
+ type=str,
47
+ nargs="+",
48
+ help="The video file(s) you want modified. Can be one or more directories and / or single files.",
49
+ )
50
+ parser.add_argument(
51
+ "-o",
52
+ "--output_file",
53
+ type=str,
54
+ dest="output_file",
55
+ help="The output file. Only usable if a single file is given. If not included, it'll append _ALTERED to the name.",
56
+ )
57
+ parser.add_argument(
58
+ "--temp_folder",
59
+ type=str,
60
+ default=str(default_temp_folder()),
61
+ help="The file path of the temporary working folder.",
62
+ )
63
+ parser.add_argument(
64
+ "-t",
65
+ "--silent_threshold",
66
+ type=float,
67
+ dest="silent_threshold",
68
+ help="The volume amount that frames' audio needs to surpass to be considered sounded. Defaults to 0.05.",
69
+ )
70
+ parser.add_argument(
71
+ "-S",
72
+ "--sounded_speed",
73
+ type=float,
74
+ dest="sounded_speed",
75
+ help="The speed that sounded (spoken) frames should be played at. Defaults to 1.",
76
+ )
77
+ parser.add_argument(
78
+ "-s",
79
+ "--silent_speed",
80
+ type=float,
81
+ dest="silent_speed",
82
+ help="The speed that silent frames should be played at. Defaults to 4.",
83
+ )
84
+ parser.add_argument(
85
+ "-fm",
86
+ "--frame_margin",
87
+ type=float,
88
+ dest="frame_spreadage",
89
+ help="Some silent frames adjacent to sounded frames are included to provide context. Defaults to 2.",
90
+ )
91
+ parser.add_argument(
92
+ "-sr",
93
+ "--sample_rate",
94
+ type=float,
95
+ dest="sample_rate",
96
+ help="Sample rate of the input and output videos. Usually extracted automatically by FFmpeg.",
97
+ )
98
+ parser.add_argument(
99
+ "--small",
100
+ action="store_true",
101
+ help="Apply small file optimizations: resize video to 720p, audio to 128k bitrate, best compression (uses CUDA if available).",
102
+ )
103
+ parser.add_argument(
104
+ "--url",
105
+ dest="server_url",
106
+ default=None,
107
+ help="Process videos via a Talks Reducer server at the provided base URL (for example, http://localhost:9005).",
108
+ )
109
+ parser.add_argument(
110
+ "--host",
111
+ dest="host",
112
+ default=None,
113
+ help="Shortcut for --url when targeting a Talks Reducer server on port 9005 (for example, localhost).",
114
+ )
115
+ parser.add_argument(
116
+ "--server-stream",
117
+ action="store_true",
118
+ help="Stream remote progress updates when using --url.",
119
+ )
120
+ return parser
121
+
122
+
123
+ def _resolve_version() -> str:
124
+ """Determine the package version for CLI reporting."""
125
+
126
+ if _about_version:
127
+ return _about_version
128
+
129
+ try:
130
+ return version("talks-reducer")
131
+ except (PackageNotFoundError, Exception):
132
+ return "unknown"
133
+
134
+
135
+ def gather_input_files(paths: List[str]) -> List[str]:
136
+ """Expand provided paths into a flat list of files that contain audio streams."""
137
+
138
+ files: List[str] = []
139
+ for input_path in paths:
140
+ if os.path.isfile(input_path) and audio.is_valid_input_file(input_path):
141
+ files.append(os.path.abspath(input_path))
142
+ elif os.path.isdir(input_path):
143
+ for file in os.listdir(input_path):
144
+ candidate = os.path.join(input_path, file)
145
+ if audio.is_valid_input_file(candidate):
146
+ files.append(candidate)
147
+ return files
148
+
149
+
150
+ def _print_total_time(start_time: float) -> None:
151
+ """Print the elapsed processing time since *start_time*."""
152
+
153
+ end_time = time.time()
154
+ total_time = end_time - start_time
155
+ hours, remainder = divmod(total_time, 3600)
156
+ minutes, seconds = divmod(remainder, 60)
157
+ print(f"\nTime: {int(hours)}h {int(minutes)}m {seconds:.2f}s")
158
+
159
+
160
+ def _process_via_server(
161
+ files: Sequence[str], parsed_args: argparse.Namespace, *, start_time: float
162
+ ) -> Tuple[bool, Optional[str]]:
163
+ """Upload *files* to the configured server and download the results.
164
+
165
+ Returns a tuple of (success, error_message). When *success* is ``False``,
166
+ ``error_message`` contains the reason and the caller should fall back to the
167
+ local processing pipeline.
168
+ """
169
+
170
+ try:
171
+ from . import service_client
172
+ except ImportError as exc: # pragma: no cover - optional dependency guard
173
+ return False, ("Server mode requires the gradio_client dependency. " f"({exc})")
174
+
175
+ server_url = parsed_args.server_url
176
+ if not server_url:
177
+ return False, "Server URL was not provided."
178
+
179
+ output_override: Optional[Path] = None
180
+ if parsed_args.output_file and len(files) == 1:
181
+ output_override = Path(parsed_args.output_file).expanduser()
182
+ elif parsed_args.output_file and len(files) > 1:
183
+ print(
184
+ "Warning: --output is ignored when processing multiple files via the server.",
185
+ file=sys.stderr,
186
+ )
187
+
188
+ unsupported_options = []
189
+ for name in (
190
+ "silent_threshold",
191
+ "silent_speed",
192
+ "sounded_speed",
193
+ "frame_spreadage",
194
+ "sample_rate",
195
+ "temp_folder",
196
+ ):
197
+ if getattr(parsed_args, name) is not None:
198
+ unsupported_options.append(f"--{name.replace('_', '-')}")
199
+
200
+ if unsupported_options:
201
+ print(
202
+ "Warning: the following options are ignored when using --url: "
203
+ + ", ".join(sorted(unsupported_options)),
204
+ file=sys.stderr,
205
+ )
206
+
207
+ for index, file in enumerate(files, start=1):
208
+ basename = os.path.basename(file)
209
+ print(
210
+ f"Processing file {index}/{len(files)} '{basename}' via server {server_url}"
211
+ )
212
+ printed_log_header = False
213
+ progress_state: dict[str, tuple[Optional[int], Optional[int], str]] = {}
214
+ stream_updates = bool(getattr(parsed_args, "server_stream", False))
215
+
216
+ def _stream_server_log(line: str) -> None:
217
+ nonlocal printed_log_header
218
+ if not printed_log_header:
219
+ print("\nServer log:", flush=True)
220
+ printed_log_header = True
221
+ print(line, flush=True)
222
+
223
+ def _stream_progress(
224
+ desc: str, current: Optional[int], total: Optional[int], unit: str
225
+ ) -> None:
226
+ key = desc or "Processing"
227
+ state = (current, total, unit)
228
+ if progress_state.get(key) == state:
229
+ return
230
+ progress_state[key] = state
231
+
232
+ parts: list[str] = []
233
+ if current is not None and total and total > 0:
234
+ percent = (current / total) * 100
235
+ parts.append(f"{current}/{total}")
236
+ parts.append(f"{percent:.1f}%")
237
+ elif current is not None:
238
+ parts.append(str(current))
239
+ if unit:
240
+ parts.append(unit)
241
+ message = " ".join(parts).strip()
242
+ print(f"{key}: {message or 'update'}", flush=True)
243
+
244
+ try:
245
+ destination, summary, log_text = service_client.send_video(
246
+ input_path=Path(file),
247
+ output_path=output_override,
248
+ server_url=server_url,
249
+ small=bool(parsed_args.small),
250
+ log_callback=_stream_server_log,
251
+ stream_updates=stream_updates,
252
+ progress_callback=_stream_progress if stream_updates else None,
253
+ )
254
+ except Exception as exc: # pragma: no cover - network failure safeguard
255
+ return False, f"Failed to process {basename} via server: {exc}"
256
+
257
+ print(summary)
258
+ print(f"Saved processed video to {destination}")
259
+ if log_text.strip() and not printed_log_header:
260
+ print("\nServer log:\n" + log_text)
261
+
262
+ _print_total_time(start_time)
263
+ return True, None
264
+
265
+
266
+ def _launch_gui(argv: Sequence[str]) -> bool:
267
+ """Attempt to launch the GUI with the provided arguments."""
268
+
269
+ try:
270
+ gui_module = import_module(".gui", __package__)
271
+ except ImportError:
272
+ return False
273
+
274
+ gui_main = getattr(gui_module, "main", None)
275
+ if gui_main is None:
276
+ return False
277
+
278
+ return bool(gui_main(list(argv)))
279
+
280
+
281
+ def _launch_server(argv: Sequence[str]) -> bool:
282
+ """Attempt to launch the Gradio server with the provided arguments."""
283
+
284
+ try:
285
+ server_module = import_module(".server", __package__)
286
+ except ImportError:
287
+ return False
288
+
289
+ server_main = getattr(server_module, "main", None)
290
+ if server_main is None:
291
+ return False
292
+
293
+ server_main(list(argv))
294
+ return True
295
+
296
+
297
+ def _find_server_tray_binary() -> Optional[Path]:
298
+ """Return the best available path to the server tray executable."""
299
+
300
+ binary_name = "talks-reducer-server-tray"
301
+ candidates: List[Path] = []
302
+
303
+ which_path = shutil.which(binary_name)
304
+ if which_path:
305
+ candidates.append(Path(which_path))
306
+
307
+ try:
308
+ launcher_dir = Path(sys.argv[0]).resolve().parent
309
+ except Exception:
310
+ launcher_dir = None
311
+
312
+ potential_names = [binary_name]
313
+ if sys.platform == "win32":
314
+ potential_names = [f"{binary_name}.exe", binary_name]
315
+
316
+ if launcher_dir is not None:
317
+ for name in potential_names:
318
+ candidates.append(launcher_dir / name)
319
+
320
+ for candidate in candidates:
321
+ if candidate and candidate.exists() and os.access(candidate, os.X_OK):
322
+ return candidate
323
+
324
+ return None
325
+
326
+
327
+ def _should_hide_subprocess_console() -> bool:
328
+ """Return ``True` ` when a detached Windows launch should hide the console."""
329
+
330
+ if sys.platform != "win32":
331
+ return False
332
+
333
+ try:
334
+ import ctypes
335
+ except Exception: # pragma: no cover - optional runtime dependency
336
+ return False
337
+
338
+ try:
339
+ get_console_window = ctypes.windll.kernel32.GetConsoleWindow # type: ignore[attr-defined]
340
+ except Exception: # pragma: no cover - platform specific guard
341
+ return False
342
+
343
+ try:
344
+ handle = get_console_window()
345
+ except Exception: # pragma: no cover - defensive fallback
346
+ return False
347
+
348
+ return handle == 0
349
+
350
+
351
+ def _launch_server_tray_binary(argv: Sequence[str]) -> bool:
352
+ """Launch the packaged server tray executable when available."""
353
+
354
+ command = _find_server_tray_binary()
355
+ if command is None:
356
+ return False
357
+
358
+ tray_args = [str(command), *list(argv)]
359
+
360
+ run_kwargs: Dict[str, object] = {"check": False}
361
+
362
+ if sys.platform == "win32":
363
+ no_window_flag = getattr(subprocess, "CREATE_NO_WINDOW", 0)
364
+ if no_window_flag and _should_hide_subprocess_console():
365
+ run_kwargs["creationflags"] = no_window_flag
366
+
367
+ try:
368
+ result = subprocess.run(tray_args, **run_kwargs)
369
+ except OSError:
370
+ return False
371
+
372
+ return result.returncode == 0
373
+
374
+
375
+ def _launch_server_tray(argv: Sequence[str]) -> bool:
376
+ """Attempt to launch the server tray helper with the provided arguments."""
377
+
378
+ if _launch_server_tray_binary(argv):
379
+ return True
380
+
381
+ try:
382
+ tray_module = import_module(".server_tray", __package__)
383
+ except ImportError:
384
+ return False
385
+
386
+ tray_main = getattr(tray_module, "main", None)
387
+ if tray_main is None:
388
+ return False
389
+
390
+ tray_main(list(argv))
391
+ return True
392
+
393
+
394
+ def main(argv: Optional[Sequence[str]] = None) -> None:
395
+ """Entry point for the command line interface.
396
+
397
+ Launch the GUI when run without arguments, otherwise defer to the CLI.
398
+ """
399
+
400
+ if argv is None:
401
+ argv_list = sys.argv[1:]
402
+ else:
403
+ argv_list = list(argv)
404
+
405
+ if "--server" in argv_list:
406
+ index = argv_list.index("--server")
407
+ tray_args = argv_list[index + 1 :]
408
+ if not _launch_server_tray(tray_args):
409
+ print("Server tray mode is unavailable.", file=sys.stderr)
410
+ sys.exit(1)
411
+ return
412
+
413
+ if argv_list and argv_list[0] in {"server", "serve"}:
414
+ if not _launch_server(argv_list[1:]):
415
+ print("Gradio server mode is unavailable.", file=sys.stderr)
416
+ sys.exit(1)
417
+ return
418
+
419
+ if not argv_list:
420
+ if _launch_gui(argv_list):
421
+ return
422
+
423
+ parser = _build_parser()
424
+ parser.print_help()
425
+ return
426
+
427
+ parser = _build_parser()
428
+ parsed_args = parser.parse_args(argv_list)
429
+
430
+ host_value = getattr(parsed_args, "host", None)
431
+ if host_value:
432
+ parsed_args.server_url = f"http://{host_value}:9005"
433
+
434
+ start_time = time.time()
435
+
436
+ files = gather_input_files(parsed_args.input_file)
437
+
438
+ args: Dict[str, object] = {
439
+ k: v for k, v in vars(parsed_args).items() if v is not None
440
+ }
441
+ del args["input_file"]
442
+
443
+ if "host" in args:
444
+ del args["host"]
445
+
446
+ if len(files) > 1 and "output_file" in args:
447
+ del args["output_file"]
448
+
449
+ fallback_messages: List[str] = []
450
+ if parsed_args.server_url:
451
+ server_success, error_message = _process_via_server(
452
+ files, parsed_args, start_time=start_time
453
+ )
454
+ if server_success:
455
+ return
456
+
457
+ fallback_reason = error_message or "Server processing is unavailable."
458
+ print(fallback_reason, file=sys.stderr)
459
+ fallback_messages.append(fallback_reason)
460
+
461
+ fallback_notice = "Falling back to local processing pipeline."
462
+ print(fallback_notice, file=sys.stderr)
463
+ fallback_messages.append(fallback_notice)
464
+
465
+ reporter = TqdmProgressReporter()
466
+
467
+ for message in fallback_messages:
468
+ reporter.log(message)
469
+
470
+ for index, file in enumerate(files):
471
+ print(f"Processing file {index + 1}/{len(files)} '{os.path.basename(file)}'")
472
+ local_options = dict(args)
473
+
474
+ option_kwargs: Dict[str, object] = {"input_file": Path(file)}
475
+
476
+ if "output_file" in local_options:
477
+ option_kwargs["output_file"] = Path(local_options["output_file"])
478
+ if "temp_folder" in local_options:
479
+ option_kwargs["temp_folder"] = Path(local_options["temp_folder"])
480
+ if "silent_threshold" in local_options:
481
+ option_kwargs["silent_threshold"] = float(local_options["silent_threshold"])
482
+ if "silent_speed" in local_options:
483
+ option_kwargs["silent_speed"] = float(local_options["silent_speed"])
484
+ if "sounded_speed" in local_options:
485
+ option_kwargs["sounded_speed"] = float(local_options["sounded_speed"])
486
+ if "frame_spreadage" in local_options:
487
+ option_kwargs["frame_spreadage"] = int(local_options["frame_spreadage"])
488
+ if "sample_rate" in local_options:
489
+ option_kwargs["sample_rate"] = int(local_options["sample_rate"])
490
+ if "small" in local_options:
491
+ option_kwargs["small"] = bool(local_options["small"])
492
+ options = ProcessingOptions(**option_kwargs)
493
+
494
+ try:
495
+ result = speed_up_video(options, reporter=reporter)
496
+ except FFmpegNotFoundError as exc:
497
+ print(str(exc), file=sys.stderr)
498
+ sys.exit(1)
499
+
500
+ reporter.log(f"Completed: {result.output_file}")
501
+ summary_parts = []
502
+ time_ratio = getattr(result, "time_ratio", None)
503
+ size_ratio = getattr(result, "size_ratio", None)
504
+ if time_ratio is not None:
505
+ summary_parts.append(f"{time_ratio * 100:.0f}% time")
506
+ if size_ratio is not None:
507
+ summary_parts.append(f"{size_ratio * 100:.0f}% size")
508
+ if summary_parts:
509
+ reporter.log("Result: " + ", ".join(summary_parts))
510
+
511
+ _print_total_time(start_time)
512
+
513
+
514
+ if __name__ == "__main__":
515
+ main()