talks-reducer 0.7.2__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {talks_reducer-0.7.2/talks_reducer.egg-info → talks_reducer-0.8.1}/PKG-INFO +2 -2
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/README.md +1 -1
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/pyproject.toml +10 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/__about__.py +1 -1
- talks_reducer-0.8.1/talks_reducer/cli.py +549 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/discovery.py +78 -22
- talks_reducer-0.8.1/talks_reducer/gui/__init__.py +21 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/gui/__main__.py +1 -1
- talks_reducer-0.7.2/talks_reducer/gui/__init__.py → talks_reducer-0.8.1/talks_reducer/gui/app.py +248 -413
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/gui/discovery.py +1 -1
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/gui/layout.py +1 -1
- talks_reducer-0.8.1/talks_reducer/gui/progress.py +80 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/gui/remote.py +11 -3
- talks_reducer-0.8.1/talks_reducer/gui/startup.py +202 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/icons.py +2 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/pipeline.py +65 -31
- talks_reducer-0.8.1/talks_reducer/resources/icons/app-256.png +0 -0
- talks_reducer-0.8.1/talks_reducer/resources/icons/app.icns +0 -0
- talks_reducer-0.8.1/talks_reducer/resources/icons/app.ico +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/server.py +106 -41
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/server_tray.py +116 -39
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/service_client.py +77 -14
- {talks_reducer-0.7.2 → talks_reducer-0.8.1/talks_reducer.egg-info}/PKG-INFO +2 -2
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer.egg-info/SOURCES.txt +19 -0
- talks_reducer-0.8.1/tests/test_audio.py +253 -0
- talks_reducer-0.8.1/tests/test_chunks.py +123 -0
- talks_reducer-0.8.1/tests/test_cli.py +680 -0
- talks_reducer-0.8.1/tests/test_discovery.py +316 -0
- talks_reducer-0.8.1/tests/test_entrypoints.py +62 -0
- talks_reducer-0.8.1/tests/test_ffmpeg.py +401 -0
- talks_reducer-0.8.1/tests/test_gui_app.py +270 -0
- talks_reducer-0.8.1/tests/test_gui_discovery.py +332 -0
- talks_reducer-0.8.1/tests/test_gui_layout.py +604 -0
- talks_reducer-0.8.1/tests/test_gui_progress.py +38 -0
- talks_reducer-0.8.1/tests/test_gui_remote.py +330 -0
- talks_reducer-0.8.1/tests/test_gui_startup.py +145 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/tests/test_gui_summary_parsing.py +1 -1
- talks_reducer-0.8.1/tests/test_icons.py +37 -0
- talks_reducer-0.8.1/tests/test_models_version.py +149 -0
- talks_reducer-0.8.1/tests/test_pipeline.py +154 -0
- talks_reducer-0.8.1/tests/test_pipeline_service.py +292 -0
- talks_reducer-0.8.1/tests/test_progress.py +101 -0
- talks_reducer-0.8.1/tests/test_server.py +494 -0
- talks_reducer-0.8.1/tests/test_server_tray.py +297 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/tests/test_service_client.py +171 -0
- talks_reducer-0.7.2/talks_reducer/cli.py +0 -505
- talks_reducer-0.7.2/tests/test_audio.py +0 -47
- talks_reducer-0.7.2/tests/test_cli.py +0 -264
- talks_reducer-0.7.2/tests/test_discovery.py +0 -119
- talks_reducer-0.7.2/tests/test_gui_remote.py +0 -171
- talks_reducer-0.7.2/tests/test_pipeline_service.py +0 -105
- talks_reducer-0.7.2/tests/test_server.py +0 -181
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/LICENSE +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/setup.cfg +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/__init__.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/__main__.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/audio.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/chunks.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/ffmpeg.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/gui/preferences.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/gui/theme.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/models.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/progress.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/resources/__init__.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer/version_utils.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer.egg-info/dependency_links.txt +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer.egg-info/entry_points.txt +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer.egg-info/requires.txt +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/talks_reducer.egg-info/top_level.txt +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/tests/test_gui_preferences.py +0 -0
- {talks_reducer-0.7.2 → talks_reducer-0.8.1}/tests/test_gui_theme.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: talks-reducer
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8.1
|
4
4
|
Summary: CLI for speeding up long-form talks by removing silence
|
5
5
|
Author: Talks Reducer Maintainers
|
6
6
|
License-Expression: MIT
|
@@ -26,7 +26,7 @@ Requires-Dist: bump-my-version>=0.5.0; extra == "dev"
|
|
26
26
|
Requires-Dist: pyinstaller>=6.4.0; extra == "dev"
|
27
27
|
Dynamic: license-file
|
28
28
|
|
29
|
-
# Talks Reducer
|
29
|
+
# Talks Reducer [](https://coveralls.io/github/popstas/talks-reducer?branch=master)
|
30
30
|
|
31
31
|
Talks Reducer shortens long-form presentations by removing silent gaps and optionally re-encoding them to smaller files. The
|
32
32
|
project was renamed from **jumpcutter** to emphasize its focus on conference talks and screencasts.
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Talks Reducer
|
1
|
+
# Talks Reducer [](https://coveralls.io/github/popstas/talks-reducer?branch=master)
|
2
2
|
|
3
3
|
Talks Reducer shortens long-form presentations by removing silent gaps and optionally re-encoding them to smaller files. The
|
4
4
|
project was renamed from **jumpcutter** to emphasize its focus on conference talks and screencasts.
|
@@ -53,3 +53,13 @@ profile = "black"
|
|
53
53
|
line_length = 88
|
54
54
|
known_first_party = ["talks_reducer"]
|
55
55
|
|
56
|
+
[tool.setuptools]
|
57
|
+
include-package-data = true
|
58
|
+
|
59
|
+
[tool.setuptools.package-data]
|
60
|
+
"talks_reducer.resources" = [
|
61
|
+
"icons/*.png",
|
62
|
+
"icons/*.ico",
|
63
|
+
"icons/*.icns",
|
64
|
+
]
|
65
|
+
|
@@ -0,0 +1,549 @@
|
|
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 pathlib import Path
|
13
|
+
from typing import Callable, Dict, List, Optional, Sequence, Tuple
|
14
|
+
|
15
|
+
from . import audio
|
16
|
+
from .ffmpeg import FFmpegNotFoundError
|
17
|
+
from .models import ProcessingOptions, default_temp_folder
|
18
|
+
from .pipeline import speed_up_video
|
19
|
+
from .progress import TqdmProgressReporter
|
20
|
+
from .version_utils import resolve_version
|
21
|
+
|
22
|
+
|
23
|
+
def _build_parser() -> argparse.ArgumentParser:
|
24
|
+
"""Create the argument parser used by the command line interface."""
|
25
|
+
|
26
|
+
parser = argparse.ArgumentParser(
|
27
|
+
description="Modifies a video file to play at different speeds when there is sound vs. silence.",
|
28
|
+
)
|
29
|
+
|
30
|
+
# Add version argument
|
31
|
+
pkg_version = resolve_version()
|
32
|
+
|
33
|
+
parser.add_argument(
|
34
|
+
"--version",
|
35
|
+
action="version",
|
36
|
+
version=f"talks-reducer {pkg_version}",
|
37
|
+
)
|
38
|
+
|
39
|
+
parser.add_argument(
|
40
|
+
"input_file",
|
41
|
+
type=str,
|
42
|
+
nargs="+",
|
43
|
+
help="The video file(s) you want modified. Can be one or more directories and / or single files.",
|
44
|
+
)
|
45
|
+
parser.add_argument(
|
46
|
+
"-o",
|
47
|
+
"--output_file",
|
48
|
+
type=str,
|
49
|
+
dest="output_file",
|
50
|
+
help="The output file. Only usable if a single file is given. If not included, it'll append _ALTERED to the name.",
|
51
|
+
)
|
52
|
+
parser.add_argument(
|
53
|
+
"--temp_folder",
|
54
|
+
type=str,
|
55
|
+
default=str(default_temp_folder()),
|
56
|
+
help="The file path of the temporary working folder.",
|
57
|
+
)
|
58
|
+
parser.add_argument(
|
59
|
+
"-t",
|
60
|
+
"--silent_threshold",
|
61
|
+
type=float,
|
62
|
+
dest="silent_threshold",
|
63
|
+
help="The volume amount that frames' audio needs to surpass to be considered sounded. Defaults to 0.05.",
|
64
|
+
)
|
65
|
+
parser.add_argument(
|
66
|
+
"-S",
|
67
|
+
"--sounded_speed",
|
68
|
+
type=float,
|
69
|
+
dest="sounded_speed",
|
70
|
+
help="The speed that sounded (spoken) frames should be played at. Defaults to 1.",
|
71
|
+
)
|
72
|
+
parser.add_argument(
|
73
|
+
"-s",
|
74
|
+
"--silent_speed",
|
75
|
+
type=float,
|
76
|
+
dest="silent_speed",
|
77
|
+
help="The speed that silent frames should be played at. Defaults to 4.",
|
78
|
+
)
|
79
|
+
parser.add_argument(
|
80
|
+
"-fm",
|
81
|
+
"--frame_margin",
|
82
|
+
type=float,
|
83
|
+
dest="frame_spreadage",
|
84
|
+
help="Some silent frames adjacent to sounded frames are included to provide context. Defaults to 2.",
|
85
|
+
)
|
86
|
+
parser.add_argument(
|
87
|
+
"-sr",
|
88
|
+
"--sample_rate",
|
89
|
+
type=float,
|
90
|
+
dest="sample_rate",
|
91
|
+
help="Sample rate of the input and output videos. Usually extracted automatically by FFmpeg.",
|
92
|
+
)
|
93
|
+
parser.add_argument(
|
94
|
+
"--small",
|
95
|
+
action="store_true",
|
96
|
+
help="Apply small file optimizations: resize video to 720p, audio to 128k bitrate, best compression (uses CUDA if available).",
|
97
|
+
)
|
98
|
+
parser.add_argument(
|
99
|
+
"--url",
|
100
|
+
dest="server_url",
|
101
|
+
default=None,
|
102
|
+
help="Process videos via a Talks Reducer server at the provided base URL (for example, http://localhost:9005).",
|
103
|
+
)
|
104
|
+
parser.add_argument(
|
105
|
+
"--host",
|
106
|
+
dest="host",
|
107
|
+
default=None,
|
108
|
+
help="Shortcut for --url when targeting a Talks Reducer server on port 9005 (for example, localhost).",
|
109
|
+
)
|
110
|
+
parser.add_argument(
|
111
|
+
"--server-stream",
|
112
|
+
action="store_true",
|
113
|
+
help="Stream remote progress updates when using --url.",
|
114
|
+
)
|
115
|
+
return parser
|
116
|
+
|
117
|
+
|
118
|
+
def gather_input_files(paths: List[str]) -> List[str]:
|
119
|
+
"""Expand provided paths into a flat list of files that contain audio streams."""
|
120
|
+
|
121
|
+
files: List[str] = []
|
122
|
+
for input_path in paths:
|
123
|
+
if os.path.isfile(input_path) and audio.is_valid_input_file(input_path):
|
124
|
+
files.append(os.path.abspath(input_path))
|
125
|
+
elif os.path.isdir(input_path):
|
126
|
+
for file in os.listdir(input_path):
|
127
|
+
candidate = os.path.join(input_path, file)
|
128
|
+
if audio.is_valid_input_file(candidate):
|
129
|
+
files.append(candidate)
|
130
|
+
return files
|
131
|
+
|
132
|
+
|
133
|
+
def _print_total_time(start_time: float) -> None:
|
134
|
+
"""Print the elapsed processing time since *start_time*."""
|
135
|
+
|
136
|
+
end_time = time.time()
|
137
|
+
total_time = end_time - start_time
|
138
|
+
hours, remainder = divmod(total_time, 3600)
|
139
|
+
minutes, seconds = divmod(remainder, 60)
|
140
|
+
print(f"\nTime: {int(hours)}h {int(minutes)}m {seconds:.2f}s")
|
141
|
+
|
142
|
+
|
143
|
+
class CliApplication:
|
144
|
+
"""Coordinator for CLI processing with dependency injection support."""
|
145
|
+
|
146
|
+
def __init__(
|
147
|
+
self,
|
148
|
+
*,
|
149
|
+
gather_files: Callable[[List[str]], List[str]],
|
150
|
+
send_video: Optional[Callable[..., Tuple[Path, str, str]]],
|
151
|
+
speed_up: Callable[[ProcessingOptions, object], object],
|
152
|
+
reporter_factory: Callable[[], object],
|
153
|
+
remote_error_message: Optional[str] = None,
|
154
|
+
) -> None:
|
155
|
+
self._gather_files = gather_files
|
156
|
+
self._send_video = send_video
|
157
|
+
self._speed_up = speed_up
|
158
|
+
self._reporter_factory = reporter_factory
|
159
|
+
self._remote_error_message = remote_error_message
|
160
|
+
|
161
|
+
def run(self, parsed_args: argparse.Namespace) -> Tuple[int, List[str]]:
|
162
|
+
"""Execute the CLI pipeline for *parsed_args*."""
|
163
|
+
|
164
|
+
start_time = time.time()
|
165
|
+
files = self._gather_files(parsed_args.input_file)
|
166
|
+
|
167
|
+
args: Dict[str, object] = {
|
168
|
+
key: value for key, value in vars(parsed_args).items() if value is not None
|
169
|
+
}
|
170
|
+
del args["input_file"]
|
171
|
+
|
172
|
+
if "host" in args:
|
173
|
+
del args["host"]
|
174
|
+
|
175
|
+
if len(files) > 1 and "output_file" in args:
|
176
|
+
del args["output_file"]
|
177
|
+
|
178
|
+
error_messages: List[str] = []
|
179
|
+
reporter_logs: List[str] = []
|
180
|
+
|
181
|
+
if getattr(parsed_args, "server_url", None):
|
182
|
+
remote_success, remote_errors, fallback_logs = self._process_via_server(
|
183
|
+
files, parsed_args, start_time
|
184
|
+
)
|
185
|
+
error_messages.extend(remote_errors)
|
186
|
+
reporter_logs.extend(fallback_logs)
|
187
|
+
if remote_success:
|
188
|
+
return 0, error_messages
|
189
|
+
|
190
|
+
reporter = self._reporter_factory()
|
191
|
+
for message in reporter_logs:
|
192
|
+
reporter.log(message)
|
193
|
+
|
194
|
+
for index, file in enumerate(files):
|
195
|
+
print(
|
196
|
+
f"Processing file {index + 1}/{len(files)} '{os.path.basename(file)}'"
|
197
|
+
)
|
198
|
+
local_options = dict(args)
|
199
|
+
|
200
|
+
option_kwargs: Dict[str, object] = {"input_file": Path(file)}
|
201
|
+
|
202
|
+
if "output_file" in local_options:
|
203
|
+
option_kwargs["output_file"] = Path(local_options["output_file"])
|
204
|
+
if "temp_folder" in local_options:
|
205
|
+
option_kwargs["temp_folder"] = Path(local_options["temp_folder"])
|
206
|
+
if "silent_threshold" in local_options:
|
207
|
+
option_kwargs["silent_threshold"] = float(
|
208
|
+
local_options["silent_threshold"]
|
209
|
+
)
|
210
|
+
if "silent_speed" in local_options:
|
211
|
+
option_kwargs["silent_speed"] = float(local_options["silent_speed"])
|
212
|
+
if "sounded_speed" in local_options:
|
213
|
+
option_kwargs["sounded_speed"] = float(local_options["sounded_speed"])
|
214
|
+
if "frame_spreadage" in local_options:
|
215
|
+
option_kwargs["frame_spreadage"] = int(local_options["frame_spreadage"])
|
216
|
+
if "sample_rate" in local_options:
|
217
|
+
option_kwargs["sample_rate"] = int(local_options["sample_rate"])
|
218
|
+
if "small" in local_options:
|
219
|
+
option_kwargs["small"] = bool(local_options["small"])
|
220
|
+
options = ProcessingOptions(**option_kwargs)
|
221
|
+
|
222
|
+
try:
|
223
|
+
result = self._speed_up(options, reporter=reporter)
|
224
|
+
except FFmpegNotFoundError as exc:
|
225
|
+
message = str(exc)
|
226
|
+
return 1, [*error_messages, message]
|
227
|
+
|
228
|
+
reporter.log(f"Completed: {result.output_file}")
|
229
|
+
summary_parts: List[str] = []
|
230
|
+
time_ratio = getattr(result, "time_ratio", None)
|
231
|
+
size_ratio = getattr(result, "size_ratio", None)
|
232
|
+
if time_ratio is not None:
|
233
|
+
summary_parts.append(f"{time_ratio * 100:.0f}% time")
|
234
|
+
if size_ratio is not None:
|
235
|
+
summary_parts.append(f"{size_ratio * 100:.0f}% size")
|
236
|
+
if summary_parts:
|
237
|
+
reporter.log("Result: " + ", ".join(summary_parts))
|
238
|
+
|
239
|
+
_print_total_time(start_time)
|
240
|
+
return 0, error_messages
|
241
|
+
|
242
|
+
def _process_via_server(
|
243
|
+
self,
|
244
|
+
files: Sequence[str],
|
245
|
+
parsed_args: argparse.Namespace,
|
246
|
+
start_time: float,
|
247
|
+
) -> Tuple[bool, List[str], List[str]]:
|
248
|
+
"""Upload *files* to the configured server and download the results."""
|
249
|
+
|
250
|
+
if not self._send_video:
|
251
|
+
message = self._remote_error_message or "Server processing is unavailable."
|
252
|
+
fallback_notice = "Falling back to local processing pipeline."
|
253
|
+
return False, [message, fallback_notice], [message, fallback_notice]
|
254
|
+
|
255
|
+
server_url = parsed_args.server_url
|
256
|
+
if not server_url:
|
257
|
+
message = "Server URL was not provided."
|
258
|
+
fallback_notice = "Falling back to local processing pipeline."
|
259
|
+
return False, [message, fallback_notice], [message, fallback_notice]
|
260
|
+
|
261
|
+
output_override: Optional[Path] = None
|
262
|
+
if parsed_args.output_file and len(files) == 1:
|
263
|
+
output_override = Path(parsed_args.output_file).expanduser()
|
264
|
+
elif parsed_args.output_file and len(files) > 1:
|
265
|
+
print(
|
266
|
+
"Warning: --output is ignored when processing multiple files via the server.",
|
267
|
+
file=sys.stderr,
|
268
|
+
)
|
269
|
+
|
270
|
+
remote_option_values: Dict[str, float] = {}
|
271
|
+
if parsed_args.silent_threshold is not None:
|
272
|
+
remote_option_values["silent_threshold"] = float(
|
273
|
+
parsed_args.silent_threshold
|
274
|
+
)
|
275
|
+
if parsed_args.silent_speed is not None:
|
276
|
+
remote_option_values["silent_speed"] = float(parsed_args.silent_speed)
|
277
|
+
if parsed_args.sounded_speed is not None:
|
278
|
+
remote_option_values["sounded_speed"] = float(parsed_args.sounded_speed)
|
279
|
+
|
280
|
+
unsupported_options: List[str] = []
|
281
|
+
for name in ("frame_spreadage", "sample_rate", "temp_folder"):
|
282
|
+
if getattr(parsed_args, name) is not None:
|
283
|
+
unsupported_options.append(f"--{name.replace('_', '-')}")
|
284
|
+
|
285
|
+
if unsupported_options:
|
286
|
+
print(
|
287
|
+
"Warning: the following options are ignored when using --url: "
|
288
|
+
+ ", ".join(sorted(unsupported_options)),
|
289
|
+
file=sys.stderr,
|
290
|
+
)
|
291
|
+
|
292
|
+
for index, file in enumerate(files, start=1):
|
293
|
+
basename = os.path.basename(file)
|
294
|
+
print(
|
295
|
+
f"Processing file {index}/{len(files)} '{basename}' via server {server_url}"
|
296
|
+
)
|
297
|
+
printed_log_header = False
|
298
|
+
progress_state: dict[str, tuple[Optional[int], Optional[int], str]] = {}
|
299
|
+
stream_updates = bool(getattr(parsed_args, "server_stream", False))
|
300
|
+
|
301
|
+
def _stream_server_log(line: str) -> None:
|
302
|
+
nonlocal printed_log_header
|
303
|
+
if not printed_log_header:
|
304
|
+
print("\nServer log:", flush=True)
|
305
|
+
printed_log_header = True
|
306
|
+
print(line, flush=True)
|
307
|
+
|
308
|
+
def _stream_progress(
|
309
|
+
desc: str, current: Optional[int], total: Optional[int], unit: str
|
310
|
+
) -> None:
|
311
|
+
key = desc or "Processing"
|
312
|
+
state = (current, total, unit)
|
313
|
+
if progress_state.get(key) == state:
|
314
|
+
return
|
315
|
+
progress_state[key] = state
|
316
|
+
|
317
|
+
parts: List[str] = []
|
318
|
+
if current is not None and total and total > 0:
|
319
|
+
percent = (current / total) * 100
|
320
|
+
parts.append(f"{current}/{total}")
|
321
|
+
parts.append(f"{percent:.1f}%")
|
322
|
+
elif current is not None:
|
323
|
+
parts.append(str(current))
|
324
|
+
if unit:
|
325
|
+
parts.append(unit)
|
326
|
+
message = " ".join(parts).strip()
|
327
|
+
print(f"{key}: {message or 'update'}", flush=True)
|
328
|
+
|
329
|
+
try:
|
330
|
+
destination, summary, log_text = self._send_video(
|
331
|
+
input_path=Path(file),
|
332
|
+
output_path=output_override,
|
333
|
+
server_url=server_url,
|
334
|
+
small=bool(parsed_args.small),
|
335
|
+
**remote_option_values,
|
336
|
+
log_callback=_stream_server_log,
|
337
|
+
stream_updates=stream_updates,
|
338
|
+
progress_callback=_stream_progress if stream_updates else None,
|
339
|
+
)
|
340
|
+
except Exception as exc: # pragma: no cover - network failure safeguard
|
341
|
+
message = f"Failed to process {basename} via server: {exc}"
|
342
|
+
fallback_notice = "Falling back to local processing pipeline."
|
343
|
+
return False, [message, fallback_notice], [message, fallback_notice]
|
344
|
+
|
345
|
+
print(summary)
|
346
|
+
print(f"Saved processed video to {destination}")
|
347
|
+
if log_text.strip() and not printed_log_header:
|
348
|
+
print("\nServer log:\n" + log_text)
|
349
|
+
|
350
|
+
_print_total_time(start_time)
|
351
|
+
return True, [], []
|
352
|
+
|
353
|
+
|
354
|
+
def _launch_gui(argv: Sequence[str]) -> bool:
|
355
|
+
"""Attempt to launch the GUI with the provided arguments."""
|
356
|
+
|
357
|
+
try:
|
358
|
+
gui_module = import_module(".gui", __package__)
|
359
|
+
except ImportError:
|
360
|
+
return False
|
361
|
+
|
362
|
+
gui_main = getattr(gui_module, "main", None)
|
363
|
+
if gui_main is None:
|
364
|
+
return False
|
365
|
+
|
366
|
+
return bool(gui_main(list(argv)))
|
367
|
+
|
368
|
+
|
369
|
+
def _launch_server(argv: Sequence[str]) -> bool:
|
370
|
+
"""Attempt to launch the Gradio server with the provided arguments."""
|
371
|
+
|
372
|
+
try:
|
373
|
+
server_module = import_module(".server", __package__)
|
374
|
+
except ImportError:
|
375
|
+
return False
|
376
|
+
|
377
|
+
server_main = getattr(server_module, "main", None)
|
378
|
+
if server_main is None:
|
379
|
+
return False
|
380
|
+
|
381
|
+
server_main(list(argv))
|
382
|
+
return True
|
383
|
+
|
384
|
+
|
385
|
+
def _find_server_tray_binary() -> Optional[Path]:
|
386
|
+
"""Return the best available path to the server tray executable."""
|
387
|
+
|
388
|
+
binary_name = "talks-reducer-server-tray"
|
389
|
+
candidates: List[Path] = []
|
390
|
+
|
391
|
+
which_path = shutil.which(binary_name)
|
392
|
+
if which_path:
|
393
|
+
candidates.append(Path(which_path))
|
394
|
+
|
395
|
+
try:
|
396
|
+
launcher_dir = Path(sys.argv[0]).resolve().parent
|
397
|
+
except Exception:
|
398
|
+
launcher_dir = None
|
399
|
+
|
400
|
+
potential_names = [binary_name]
|
401
|
+
if sys.platform == "win32":
|
402
|
+
potential_names = [f"{binary_name}.exe", binary_name]
|
403
|
+
|
404
|
+
if launcher_dir is not None:
|
405
|
+
for name in potential_names:
|
406
|
+
candidates.append(launcher_dir / name)
|
407
|
+
|
408
|
+
for candidate in candidates:
|
409
|
+
if candidate and candidate.exists() and os.access(candidate, os.X_OK):
|
410
|
+
return candidate
|
411
|
+
|
412
|
+
return None
|
413
|
+
|
414
|
+
|
415
|
+
def _should_hide_subprocess_console() -> bool:
|
416
|
+
"""Return ``True` ` when a detached Windows launch should hide the console."""
|
417
|
+
|
418
|
+
if sys.platform != "win32":
|
419
|
+
return False
|
420
|
+
|
421
|
+
try:
|
422
|
+
import ctypes
|
423
|
+
except Exception: # pragma: no cover - optional runtime dependency
|
424
|
+
return False
|
425
|
+
|
426
|
+
try:
|
427
|
+
get_console_window = ctypes.windll.kernel32.GetConsoleWindow # type: ignore[attr-defined]
|
428
|
+
except Exception: # pragma: no cover - platform specific guard
|
429
|
+
return False
|
430
|
+
|
431
|
+
try:
|
432
|
+
handle = get_console_window()
|
433
|
+
except Exception: # pragma: no cover - defensive fallback
|
434
|
+
return False
|
435
|
+
|
436
|
+
return handle == 0
|
437
|
+
|
438
|
+
|
439
|
+
def _launch_server_tray_binary(argv: Sequence[str]) -> bool:
|
440
|
+
"""Launch the packaged server tray executable when available."""
|
441
|
+
|
442
|
+
command = _find_server_tray_binary()
|
443
|
+
if command is None:
|
444
|
+
return False
|
445
|
+
|
446
|
+
tray_args = [str(command), *list(argv)]
|
447
|
+
|
448
|
+
run_kwargs: Dict[str, object] = {"check": False}
|
449
|
+
|
450
|
+
if sys.platform == "win32":
|
451
|
+
no_window_flag = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
452
|
+
if no_window_flag and _should_hide_subprocess_console():
|
453
|
+
run_kwargs["creationflags"] = no_window_flag
|
454
|
+
|
455
|
+
try:
|
456
|
+
result = subprocess.run(tray_args, **run_kwargs)
|
457
|
+
except OSError:
|
458
|
+
return False
|
459
|
+
|
460
|
+
return result.returncode == 0
|
461
|
+
|
462
|
+
|
463
|
+
def _launch_server_tray(argv: Sequence[str]) -> bool:
|
464
|
+
"""Attempt to launch the server tray helper with the provided arguments."""
|
465
|
+
|
466
|
+
if _launch_server_tray_binary(argv):
|
467
|
+
return True
|
468
|
+
|
469
|
+
try:
|
470
|
+
tray_module = import_module(".server_tray", __package__)
|
471
|
+
except ImportError:
|
472
|
+
return False
|
473
|
+
|
474
|
+
tray_main = getattr(tray_module, "main", None)
|
475
|
+
if tray_main is None:
|
476
|
+
return False
|
477
|
+
|
478
|
+
tray_main(list(argv))
|
479
|
+
return True
|
480
|
+
|
481
|
+
|
482
|
+
def main(argv: Optional[Sequence[str]] = None) -> None:
|
483
|
+
"""Entry point for the command line interface.
|
484
|
+
|
485
|
+
Launch the GUI when run without arguments, otherwise defer to the CLI.
|
486
|
+
"""
|
487
|
+
|
488
|
+
if argv is None:
|
489
|
+
argv_list = sys.argv[1:]
|
490
|
+
else:
|
491
|
+
argv_list = list(argv)
|
492
|
+
|
493
|
+
if "--server" in argv_list:
|
494
|
+
index = argv_list.index("--server")
|
495
|
+
tray_args = argv_list[index + 1 :]
|
496
|
+
if not _launch_server_tray(tray_args):
|
497
|
+
print("Server tray mode is unavailable.", file=sys.stderr)
|
498
|
+
sys.exit(1)
|
499
|
+
return
|
500
|
+
|
501
|
+
if argv_list and argv_list[0] in {"server", "serve"}:
|
502
|
+
if not _launch_server(argv_list[1:]):
|
503
|
+
print("Gradio server mode is unavailable.", file=sys.stderr)
|
504
|
+
sys.exit(1)
|
505
|
+
return
|
506
|
+
|
507
|
+
if not argv_list:
|
508
|
+
if _launch_gui(argv_list):
|
509
|
+
return
|
510
|
+
|
511
|
+
parser = _build_parser()
|
512
|
+
parser.print_help()
|
513
|
+
return
|
514
|
+
|
515
|
+
parser = _build_parser()
|
516
|
+
parsed_args = parser.parse_args(argv_list)
|
517
|
+
|
518
|
+
host_value = getattr(parsed_args, "host", None)
|
519
|
+
if host_value:
|
520
|
+
parsed_args.server_url = f"http://{host_value}:9005"
|
521
|
+
|
522
|
+
send_video = None
|
523
|
+
remote_error_message: Optional[str] = None
|
524
|
+
try: # pragma: no cover - optional dependency guard
|
525
|
+
from . import service_client
|
526
|
+
except ImportError as exc:
|
527
|
+
remote_error_message = (
|
528
|
+
"Server mode requires the gradio_client dependency. " f"({exc})"
|
529
|
+
)
|
530
|
+
else:
|
531
|
+
send_video = service_client.send_video
|
532
|
+
|
533
|
+
application = CliApplication(
|
534
|
+
gather_files=gather_input_files,
|
535
|
+
send_video=send_video,
|
536
|
+
speed_up=speed_up_video,
|
537
|
+
reporter_factory=TqdmProgressReporter,
|
538
|
+
remote_error_message=remote_error_message,
|
539
|
+
)
|
540
|
+
|
541
|
+
exit_code, error_messages = application.run(parsed_args)
|
542
|
+
for message in error_messages:
|
543
|
+
print(message, file=sys.stderr)
|
544
|
+
if exit_code:
|
545
|
+
sys.exit(exit_code)
|
546
|
+
|
547
|
+
|
548
|
+
if __name__ == "__main__":
|
549
|
+
main()
|