talks-reducer 0.5.5__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- talks_reducer/__about__.py +1 -1
- talks_reducer/cli.py +192 -15
- talks_reducer/discovery.py +42 -12
- talks_reducer/gui.py +637 -162
- talks_reducer/pipeline.py +1 -1
- talks_reducer/server.py +103 -22
- talks_reducer/server_tray.py +216 -17
- talks_reducer/service_client.py +258 -4
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/METADATA +13 -4
- talks_reducer-0.6.0.dist-info/RECORD +21 -0
- talks_reducer-0.5.5.dist-info/RECORD +0 -21
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/WHEEL +0 -0
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.5.5.dist-info → talks_reducer-0.6.0.dist-info}/top_level.txt +0 -0
talks_reducer/__about__.py
CHANGED
talks_reducer/cli.py
CHANGED
@@ -4,12 +4,14 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import argparse
|
6
6
|
import os
|
7
|
+
import shutil
|
8
|
+
import subprocess
|
7
9
|
import sys
|
8
10
|
import time
|
9
11
|
from importlib import import_module
|
10
12
|
from importlib.metadata import PackageNotFoundError, version
|
11
13
|
from pathlib import Path
|
12
|
-
from typing import Dict, List, Optional, Sequence
|
14
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
13
15
|
|
14
16
|
from . import audio
|
15
17
|
|
@@ -104,6 +106,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
104
106
|
default=None,
|
105
107
|
help="Process videos via a Talks Reducer server at the provided base URL (for example, http://localhost:9005).",
|
106
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
|
+
)
|
107
120
|
return parser
|
108
121
|
|
109
122
|
|
@@ -146,21 +159,22 @@ def _print_total_time(start_time: float) -> None:
|
|
146
159
|
|
147
160
|
def _process_via_server(
|
148
161
|
files: Sequence[str], parsed_args: argparse.Namespace, *, start_time: float
|
149
|
-
) ->
|
150
|
-
"""Upload *files* to the configured server and download the results.
|
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
|
+
"""
|
151
169
|
|
152
170
|
try:
|
153
171
|
from . import service_client
|
154
172
|
except ImportError as exc: # pragma: no cover - optional dependency guard
|
155
|
-
|
156
|
-
"Server mode requires the gradio_client dependency." f" ({exc})",
|
157
|
-
file=sys.stderr,
|
158
|
-
)
|
159
|
-
sys.exit(1)
|
173
|
+
return False, ("Server mode requires the gradio_client dependency. " f"({exc})")
|
160
174
|
|
161
175
|
server_url = parsed_args.server_url
|
162
176
|
if not server_url:
|
163
|
-
return
|
177
|
+
return False, "Server URL was not provided."
|
164
178
|
|
165
179
|
output_override: Optional[Path] = None
|
166
180
|
if parsed_args.output_file and len(files) == 1:
|
@@ -195,23 +209,58 @@ def _process_via_server(
|
|
195
209
|
print(
|
196
210
|
f"Processing file {index}/{len(files)} '{basename}' via server {server_url}"
|
197
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
|
+
|
198
244
|
try:
|
199
245
|
destination, summary, log_text = service_client.send_video(
|
200
246
|
input_path=Path(file),
|
201
247
|
output_path=output_override,
|
202
248
|
server_url=server_url,
|
203
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,
|
204
253
|
)
|
205
254
|
except Exception as exc: # pragma: no cover - network failure safeguard
|
206
|
-
|
207
|
-
sys.exit(1)
|
255
|
+
return False, f"Failed to process {basename} via server: {exc}"
|
208
256
|
|
209
257
|
print(summary)
|
210
258
|
print(f"Saved processed video to {destination}")
|
211
|
-
if log_text.strip():
|
259
|
+
if log_text.strip() and not printed_log_header:
|
212
260
|
print("\nServer log:\n" + log_text)
|
213
261
|
|
214
262
|
_print_total_time(start_time)
|
263
|
+
return True, None
|
215
264
|
|
216
265
|
|
217
266
|
def _launch_gui(argv: Sequence[str]) -> bool:
|
@@ -230,7 +279,7 @@ def _launch_gui(argv: Sequence[str]) -> bool:
|
|
230
279
|
|
231
280
|
|
232
281
|
def _launch_server(argv: Sequence[str]) -> bool:
|
233
|
-
"""Attempt to launch the Gradio
|
282
|
+
"""Attempt to launch the Gradio server with the provided arguments."""
|
234
283
|
|
235
284
|
try:
|
236
285
|
server_module = import_module(".server", __package__)
|
@@ -245,6 +294,103 @@ def _launch_server(argv: Sequence[str]) -> bool:
|
|
245
294
|
return True
|
246
295
|
|
247
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
|
+
|
248
394
|
def main(argv: Optional[Sequence[str]] = None) -> None:
|
249
395
|
"""Entry point for the command line interface.
|
250
396
|
|
@@ -256,6 +402,14 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
256
402
|
else:
|
257
403
|
argv_list = list(argv)
|
258
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
|
+
|
259
413
|
if argv_list and argv_list[0] in {"server", "serve"}:
|
260
414
|
if not _launch_server(argv_list[1:]):
|
261
415
|
print("Gradio server mode is unavailable.", file=sys.stderr)
|
@@ -272,6 +426,11 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
272
426
|
|
273
427
|
parser = _build_parser()
|
274
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
|
+
|
275
434
|
start_time = time.time()
|
276
435
|
|
277
436
|
files = gather_input_files(parsed_args.input_file)
|
@@ -281,15 +440,33 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
281
440
|
}
|
282
441
|
del args["input_file"]
|
283
442
|
|
443
|
+
if "host" in args:
|
444
|
+
del args["host"]
|
445
|
+
|
284
446
|
if len(files) > 1 and "output_file" in args:
|
285
447
|
del args["output_file"]
|
286
448
|
|
449
|
+
fallback_messages: List[str] = []
|
287
450
|
if parsed_args.server_url:
|
288
|
-
|
289
|
-
|
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)
|
290
464
|
|
291
465
|
reporter = TqdmProgressReporter()
|
292
466
|
|
467
|
+
for message in fallback_messages:
|
468
|
+
reporter.log(message)
|
469
|
+
|
293
470
|
for index, file in enumerate(files):
|
294
471
|
print(f"Processing file {index + 1}/{len(files)} '{os.path.basename(file)}'")
|
295
472
|
local_options = dict(args)
|
talks_reducer/discovery.py
CHANGED
@@ -7,11 +7,21 @@ import socket
|
|
7
7
|
from concurrent.futures import ThreadPoolExecutor
|
8
8
|
from contextlib import closing
|
9
9
|
from http.client import HTTPConnection
|
10
|
-
from typing import Iterable, Iterator, List, Optional, Set
|
10
|
+
from typing import Callable, Iterable, Iterator, List, Optional, Set
|
11
11
|
|
12
12
|
DEFAULT_PORT = 9005
|
13
13
|
DEFAULT_TIMEOUT = 0.4
|
14
14
|
|
15
|
+
_EXCLUDED_HOSTS = {"127.0.0.1", "localhost", "0.0.0.0"}
|
16
|
+
|
17
|
+
|
18
|
+
def _should_include_host(host: Optional[str]) -> bool:
|
19
|
+
"""Return ``True`` when *host* should be scanned for discovery."""
|
20
|
+
|
21
|
+
if not host:
|
22
|
+
return False
|
23
|
+
return host not in _EXCLUDED_HOSTS
|
24
|
+
|
15
25
|
|
16
26
|
def _iter_local_ipv4_addresses() -> Iterator[str]:
|
17
27
|
"""Yield IPv4 addresses that belong to the local machine."""
|
@@ -43,16 +53,19 @@ def _iter_local_ipv4_addresses() -> Iterator[str]:
|
|
43
53
|
def _build_default_host_candidates(prefix_length: int = 24) -> List[str]:
|
44
54
|
"""Return a list of host candidates based on detected local networks."""
|
45
55
|
|
46
|
-
hosts: Set[str] =
|
56
|
+
hosts: Set[str] = set()
|
47
57
|
|
48
58
|
for address in _iter_local_ipv4_addresses():
|
49
|
-
|
59
|
+
if _should_include_host(address):
|
60
|
+
hosts.add(address)
|
50
61
|
try:
|
51
62
|
network = ipaddress.ip_network(f"{address}/{prefix_length}", strict=False)
|
52
63
|
except ValueError:
|
53
64
|
continue
|
54
65
|
for host in network.hosts():
|
55
|
-
|
66
|
+
host_str = str(host)
|
67
|
+
if _should_include_host(host_str):
|
68
|
+
hosts.add(host_str)
|
56
69
|
|
57
70
|
return sorted(hosts)
|
58
71
|
|
@@ -82,36 +95,53 @@ def _probe_host(host: str, port: int, timeout: float) -> Optional[str]:
|
|
82
95
|
return None
|
83
96
|
|
84
97
|
|
98
|
+
ProgressCallback = Callable[[int, int], None]
|
99
|
+
|
100
|
+
|
85
101
|
def discover_servers(
|
86
102
|
*,
|
87
103
|
port: int = DEFAULT_PORT,
|
88
104
|
timeout: float = DEFAULT_TIMEOUT,
|
89
105
|
hosts: Optional[Iterable[str]] = None,
|
106
|
+
progress_callback: Optional[ProgressCallback] = None,
|
90
107
|
) -> List[str]:
|
91
108
|
"""Scan *hosts* for running Talks Reducer servers on *port*.
|
92
109
|
|
93
110
|
When *hosts* is omitted, the local /24 networks derived from available IPv4
|
94
|
-
addresses are scanned. ``localhost
|
95
|
-
|
111
|
+
addresses are scanned. ``127.0.0.1``, ``localhost``, and ``0.0.0.0`` are
|
112
|
+
excluded to avoid duplicating local endpoints. The optional
|
113
|
+
*progress_callback* receives the number of scanned hosts and the total
|
114
|
+
candidate count whenever discovery advances. The function returns a sorted
|
115
|
+
list of unique base URLs.
|
96
116
|
"""
|
97
117
|
|
98
118
|
if hosts is None:
|
99
|
-
candidates =
|
119
|
+
candidates = sorted(
|
120
|
+
{
|
121
|
+
host
|
122
|
+
for host in _build_default_host_candidates()
|
123
|
+
if _should_include_host(host)
|
124
|
+
}
|
125
|
+
)
|
100
126
|
else:
|
101
|
-
candidates = sorted(
|
102
|
-
if "127.0.0.1" not in candidates:
|
103
|
-
candidates.append("127.0.0.1")
|
104
|
-
if "localhost" not in candidates:
|
105
|
-
candidates.append("localhost")
|
127
|
+
candidates = sorted({host for host in hosts if _should_include_host(host)})
|
106
128
|
|
107
129
|
results: List[str] = []
|
130
|
+
total = len(candidates)
|
131
|
+
|
132
|
+
if progress_callback is not None:
|
133
|
+
progress_callback(0, total)
|
108
134
|
|
109
135
|
with ThreadPoolExecutor(max_workers=32) as executor:
|
136
|
+
scanned = 0
|
110
137
|
for url in executor.map(
|
111
138
|
lambda host: _probe_host(host, port, timeout), candidates
|
112
139
|
):
|
113
140
|
if url and url not in results:
|
114
141
|
results.append(url)
|
142
|
+
scanned += 1
|
143
|
+
if progress_callback is not None:
|
144
|
+
progress_callback(scanned, total)
|
115
145
|
|
116
146
|
return results
|
117
147
|
|