talks-reducer 0.5.1__py3-none-any.whl → 0.5.2__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/gui.py +43 -1
- talks_reducer/server.py +4 -3
- talks_reducer/server_tray.py +417 -0
- {talks_reducer-0.5.1.dist-info → talks_reducer-0.5.2.dist-info}/METADATA +142 -119
- {talks_reducer-0.5.1.dist-info → talks_reducer-0.5.2.dist-info}/RECORD +10 -9
- {talks_reducer-0.5.1.dist-info → talks_reducer-0.5.2.dist-info}/entry_points.txt +1 -0
- {talks_reducer-0.5.1.dist-info → talks_reducer-0.5.2.dist-info}/WHEEL +0 -0
- {talks_reducer-0.5.1.dist-info → talks_reducer-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.5.1.dist-info → talks_reducer-0.5.2.dist-info}/top_level.txt +0 -0
talks_reducer/__about__.py
CHANGED
talks_reducer/gui.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import argparse
|
5
6
|
import json
|
6
7
|
import os
|
7
8
|
import re
|
@@ -10,7 +11,7 @@ import sys
|
|
10
11
|
import threading
|
11
12
|
from importlib.metadata import version
|
12
13
|
from pathlib import Path
|
13
|
-
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Sequence
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Sequence
|
14
15
|
|
15
16
|
if TYPE_CHECKING:
|
16
17
|
import tkinter as tk
|
@@ -149,6 +150,32 @@ DARK_THEME = {
|
|
149
150
|
}
|
150
151
|
|
151
152
|
|
153
|
+
_TRAY_LOCK = threading.Lock()
|
154
|
+
_TRAY_PROCESS: Optional[subprocess.Popen[Any]] = None
|
155
|
+
|
156
|
+
|
157
|
+
def _ensure_server_tray_running(extra_args: Optional[Sequence[str]] = None) -> None:
|
158
|
+
"""Start the server tray in a background process if one is not active."""
|
159
|
+
|
160
|
+
global _TRAY_PROCESS
|
161
|
+
|
162
|
+
with _TRAY_LOCK:
|
163
|
+
if _TRAY_PROCESS is not None and _TRAY_PROCESS.poll() is None:
|
164
|
+
return
|
165
|
+
|
166
|
+
command = [sys.executable, "-m", "talks_reducer.server_tray"]
|
167
|
+
if extra_args:
|
168
|
+
command.extend(extra_args)
|
169
|
+
|
170
|
+
try:
|
171
|
+
_TRAY_PROCESS = subprocess.Popen(command)
|
172
|
+
except Exception as exc: # pragma: no cover - best-effort fallback
|
173
|
+
_TRAY_PROCESS = None
|
174
|
+
sys.stderr.write(
|
175
|
+
f"Warning: failed to launch Talks Reducer server tray: {exc}\n"
|
176
|
+
)
|
177
|
+
|
178
|
+
|
152
179
|
class _GuiProgressHandle(ProgressHandle):
|
153
180
|
"""Simple progress handle that records totals but only logs milestones."""
|
154
181
|
|
@@ -1493,6 +1520,17 @@ def main(argv: Optional[Sequence[str]] = None) -> bool:
|
|
1493
1520
|
if argv is None:
|
1494
1521
|
argv = sys.argv[1:]
|
1495
1522
|
|
1523
|
+
parser = argparse.ArgumentParser(add_help=False)
|
1524
|
+
parser.add_argument(
|
1525
|
+
"--no-tray",
|
1526
|
+
action="store_true",
|
1527
|
+
help="Do not start the Talks Reducer server tray alongside the GUI.",
|
1528
|
+
)
|
1529
|
+
|
1530
|
+
parsed_args, remaining = parser.parse_known_args(argv)
|
1531
|
+
no_tray = parsed_args.no_tray
|
1532
|
+
argv = remaining
|
1533
|
+
|
1496
1534
|
if argv:
|
1497
1535
|
launch_gui = False
|
1498
1536
|
if sys.platform == "win32" and not any(arg.startswith("-") for arg in argv):
|
@@ -1506,6 +1544,8 @@ def main(argv: Optional[Sequence[str]] = None) -> bool:
|
|
1506
1544
|
if launch_gui:
|
1507
1545
|
try:
|
1508
1546
|
app = TalksReducerGUI(argv, auto_run=True)
|
1547
|
+
if not no_tray:
|
1548
|
+
_ensure_server_tray_running()
|
1509
1549
|
app.run()
|
1510
1550
|
return True
|
1511
1551
|
except Exception:
|
@@ -1567,6 +1607,8 @@ def main(argv: Optional[Sequence[str]] = None) -> bool:
|
|
1567
1607
|
# Catch and report any errors during GUI initialization
|
1568
1608
|
try:
|
1569
1609
|
app = TalksReducerGUI()
|
1610
|
+
if not no_tray:
|
1611
|
+
_ensure_server_tray_running()
|
1570
1612
|
app.run()
|
1571
1613
|
return True
|
1572
1614
|
except Exception as e:
|
talks_reducer/server.py
CHANGED
@@ -273,8 +273,9 @@ def build_interface() -> gr.Blocks:
|
|
273
273
|
gr.Markdown(
|
274
274
|
"""
|
275
275
|
## Talks Reducer — Simple Server
|
276
|
-
Drop a video into the zone below or click to browse.
|
277
|
-
apply the 720p/128k preset before processing starts
|
276
|
+
Drop a video into the zone below or click to browse. **Small video** is enabled
|
277
|
+
by default to apply the 720p/128k preset before processing starts—clear it to
|
278
|
+
keep the original resolution.
|
278
279
|
""".strip()
|
279
280
|
)
|
280
281
|
|
@@ -284,7 +285,7 @@ def build_interface() -> gr.Blocks:
|
|
284
285
|
file_types=["video"],
|
285
286
|
type="filepath",
|
286
287
|
)
|
287
|
-
small_checkbox = gr.Checkbox(label="Small video", value=
|
288
|
+
small_checkbox = gr.Checkbox(label="Small video", value=True)
|
288
289
|
|
289
290
|
video_output = gr.Video(label="Processed video")
|
290
291
|
summary_output = gr.Markdown()
|
@@ -0,0 +1,417 @@
|
|
1
|
+
"""System tray launcher for the Talks Reducer Gradio server."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import atexit
|
7
|
+
import logging
|
8
|
+
import subprocess
|
9
|
+
import sys
|
10
|
+
import threading
|
11
|
+
import time
|
12
|
+
import webbrowser
|
13
|
+
from contextlib import suppress
|
14
|
+
from importlib import resources
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Any, Optional, Sequence
|
17
|
+
|
18
|
+
from PIL import Image
|
19
|
+
|
20
|
+
from .server import build_interface
|
21
|
+
|
22
|
+
try: # pragma: no cover - import guarded for clearer error message at runtime
|
23
|
+
import pystray
|
24
|
+
except ModuleNotFoundError as exc: # pragma: no cover - handled in ``main``
|
25
|
+
PYSTRAY_IMPORT_ERROR = exc
|
26
|
+
pystray = None # type: ignore[assignment]
|
27
|
+
else:
|
28
|
+
PYSTRAY_IMPORT_ERROR = None
|
29
|
+
|
30
|
+
|
31
|
+
LOGGER = logging.getLogger(__name__)
|
32
|
+
|
33
|
+
|
34
|
+
def _guess_local_url(host: Optional[str], port: int) -> str:
|
35
|
+
"""Return the URL the server is most likely reachable at locally."""
|
36
|
+
|
37
|
+
if host in (None, "", "0.0.0.0", "::"):
|
38
|
+
hostname = "127.0.0.1"
|
39
|
+
else:
|
40
|
+
hostname = host
|
41
|
+
return f"http://{hostname}:{port}/"
|
42
|
+
|
43
|
+
|
44
|
+
def _load_icon() -> Image.Image:
|
45
|
+
"""Load the tray icon image, falling back to a solid accent square."""
|
46
|
+
|
47
|
+
LOGGER.debug("Attempting to load tray icon image.")
|
48
|
+
|
49
|
+
candidates = [
|
50
|
+
Path(__file__).resolve().parent.parent / "docs" / "assets" / "icon.png",
|
51
|
+
Path(__file__).resolve().parent / "icon.png",
|
52
|
+
]
|
53
|
+
|
54
|
+
for candidate in candidates:
|
55
|
+
LOGGER.debug("Checking icon candidate at %s", candidate)
|
56
|
+
if candidate.exists():
|
57
|
+
try:
|
58
|
+
image = Image.open(candidate).copy()
|
59
|
+
except Exception as exc: # pragma: no cover - diagnostic log
|
60
|
+
LOGGER.warning("Failed to load tray icon from %s: %s", candidate, exc)
|
61
|
+
else:
|
62
|
+
LOGGER.debug("Loaded tray icon from %s", candidate)
|
63
|
+
return image
|
64
|
+
|
65
|
+
with suppress(FileNotFoundError):
|
66
|
+
resource_icon = resources.files("talks_reducer") / "assets" / "icon.png"
|
67
|
+
if resource_icon.is_file():
|
68
|
+
LOGGER.debug("Loading tray icon from package resources")
|
69
|
+
with resource_icon.open("rb") as handle:
|
70
|
+
try:
|
71
|
+
return Image.open(handle).copy()
|
72
|
+
except Exception as exc: # pragma: no cover - diagnostic log
|
73
|
+
LOGGER.warning(
|
74
|
+
"Failed to load tray icon from package resources: %s", exc
|
75
|
+
)
|
76
|
+
|
77
|
+
LOGGER.warning("Falling back to generated tray icon; packaged image not found")
|
78
|
+
# Fallback to a simple accent-colored square to avoid import errors
|
79
|
+
image = Image.new("RGBA", (64, 64), color=(37, 99, 235, 255))
|
80
|
+
return image
|
81
|
+
|
82
|
+
|
83
|
+
class _ServerTrayApplication:
|
84
|
+
"""Coordinate the Gradio server lifecycle and the system tray icon."""
|
85
|
+
|
86
|
+
def __init__(
|
87
|
+
self,
|
88
|
+
*,
|
89
|
+
host: Optional[str],
|
90
|
+
port: int,
|
91
|
+
share: bool,
|
92
|
+
open_browser: bool,
|
93
|
+
tray_mode: str,
|
94
|
+
) -> None:
|
95
|
+
self._host = host
|
96
|
+
self._port = port
|
97
|
+
self._share = share
|
98
|
+
self._open_browser_on_start = open_browser
|
99
|
+
self._tray_mode = tray_mode
|
100
|
+
|
101
|
+
self._stop_event = threading.Event()
|
102
|
+
self._ready_event = threading.Event()
|
103
|
+
self._gui_lock = threading.Lock()
|
104
|
+
|
105
|
+
self._server_handle: Optional[Any] = None
|
106
|
+
self._local_url: Optional[str] = None
|
107
|
+
self._share_url: Optional[str] = None
|
108
|
+
self._icon: Optional[pystray.Icon] = None
|
109
|
+
self._gui_process: Optional[subprocess.Popen[Any]] = None
|
110
|
+
|
111
|
+
# Server lifecycle -------------------------------------------------
|
112
|
+
|
113
|
+
def _launch_server(self) -> None:
|
114
|
+
"""Start the Gradio server in the background and record its URLs."""
|
115
|
+
|
116
|
+
LOGGER.info(
|
117
|
+
"Starting Talks Reducer server on host=%s port=%s share=%s",
|
118
|
+
self._host or "127.0.0.1",
|
119
|
+
self._port,
|
120
|
+
self._share,
|
121
|
+
)
|
122
|
+
demo = build_interface()
|
123
|
+
server = demo.launch(
|
124
|
+
server_name=self._host,
|
125
|
+
server_port=self._port,
|
126
|
+
share=self._share,
|
127
|
+
inbrowser=False,
|
128
|
+
prevent_thread_lock=True,
|
129
|
+
show_error=True,
|
130
|
+
)
|
131
|
+
|
132
|
+
self._server_handle = server
|
133
|
+
self._local_url = getattr(
|
134
|
+
server, "local_url", _guess_local_url(self._host, self._port)
|
135
|
+
)
|
136
|
+
self._share_url = getattr(server, "share_url", None)
|
137
|
+
self._ready_event.set()
|
138
|
+
LOGGER.info("Server ready at %s", self._local_url)
|
139
|
+
|
140
|
+
# Keep checking for a share URL while the server is running.
|
141
|
+
while not self._stop_event.is_set():
|
142
|
+
share_url = getattr(server, "share_url", None)
|
143
|
+
if share_url:
|
144
|
+
self._share_url = share_url
|
145
|
+
LOGGER.info("Share URL available: %s", share_url)
|
146
|
+
time.sleep(0.5)
|
147
|
+
|
148
|
+
# Tray helpers -----------------------------------------------------
|
149
|
+
|
150
|
+
def _resolve_url(self) -> Optional[str]:
|
151
|
+
if self._share_url:
|
152
|
+
return self._share_url
|
153
|
+
return self._local_url
|
154
|
+
|
155
|
+
def _handle_open_webui(
|
156
|
+
self,
|
157
|
+
_icon: Optional[pystray.Icon] = None,
|
158
|
+
_item: Optional[pystray.MenuItem] = None,
|
159
|
+
) -> None:
|
160
|
+
url = self._resolve_url()
|
161
|
+
if url:
|
162
|
+
webbrowser.open(url)
|
163
|
+
LOGGER.debug("Opened browser to %s", url)
|
164
|
+
else:
|
165
|
+
LOGGER.warning("Server URL not yet available; please try again.")
|
166
|
+
|
167
|
+
def _gui_is_running(self) -> bool:
|
168
|
+
"""Return whether the GUI subprocess is currently active."""
|
169
|
+
|
170
|
+
process = self._gui_process
|
171
|
+
if process is None:
|
172
|
+
return False
|
173
|
+
if process.poll() is None:
|
174
|
+
return True
|
175
|
+
self._gui_process = None
|
176
|
+
return False
|
177
|
+
|
178
|
+
def _monitor_gui_process(self, process: subprocess.Popen[Any]) -> None:
|
179
|
+
"""Reset the GUI handle once the subprocess exits."""
|
180
|
+
|
181
|
+
try:
|
182
|
+
process.wait()
|
183
|
+
except Exception as exc: # pragma: no cover - best-effort cleanup
|
184
|
+
LOGGER.debug("GUI process monitor exited with %s", exc)
|
185
|
+
finally:
|
186
|
+
with self._gui_lock:
|
187
|
+
if self._gui_process is process:
|
188
|
+
self._gui_process = None
|
189
|
+
LOGGER.info("Talks Reducer GUI closed")
|
190
|
+
|
191
|
+
def _launch_gui(
|
192
|
+
self,
|
193
|
+
_icon: Optional[pystray.Icon] = None,
|
194
|
+
_item: Optional[pystray.MenuItem] = None,
|
195
|
+
) -> None:
|
196
|
+
"""Launch the Talks Reducer GUI in a background subprocess."""
|
197
|
+
|
198
|
+
with self._gui_lock:
|
199
|
+
if self._gui_is_running():
|
200
|
+
LOGGER.info(
|
201
|
+
"Talks Reducer GUI already running; focusing existing window"
|
202
|
+
)
|
203
|
+
return
|
204
|
+
|
205
|
+
try:
|
206
|
+
LOGGER.info("Launching Talks Reducer GUI via %s", sys.executable)
|
207
|
+
process = subprocess.Popen(
|
208
|
+
[sys.executable, "-m", "talks_reducer.gui", "--no-tray"]
|
209
|
+
)
|
210
|
+
except Exception as exc: # pragma: no cover - platform specific
|
211
|
+
LOGGER.error("Failed to launch Talks Reducer GUI: %s", exc)
|
212
|
+
self._gui_process = None
|
213
|
+
return
|
214
|
+
|
215
|
+
self._gui_process = process
|
216
|
+
|
217
|
+
watcher = threading.Thread(
|
218
|
+
target=self._monitor_gui_process,
|
219
|
+
args=(process,),
|
220
|
+
name="talks-reducer-gui-monitor",
|
221
|
+
daemon=True,
|
222
|
+
)
|
223
|
+
watcher.start()
|
224
|
+
|
225
|
+
def _handle_quit(
|
226
|
+
self,
|
227
|
+
icon: Optional[pystray.Icon] = None,
|
228
|
+
_item: Optional[pystray.MenuItem] = None,
|
229
|
+
) -> None:
|
230
|
+
self.stop()
|
231
|
+
if icon is not None:
|
232
|
+
icon.stop()
|
233
|
+
|
234
|
+
# Public API -------------------------------------------------------
|
235
|
+
|
236
|
+
def run(self) -> None:
|
237
|
+
"""Start the server and block until the tray icon exits."""
|
238
|
+
|
239
|
+
server_thread = threading.Thread(
|
240
|
+
target=self._launch_server, name="talks-reducer-server", daemon=True
|
241
|
+
)
|
242
|
+
server_thread.start()
|
243
|
+
|
244
|
+
if not self._ready_event.wait(timeout=30):
|
245
|
+
raise RuntimeError(
|
246
|
+
"Timed out while waiting for the Talks Reducer server to start."
|
247
|
+
)
|
248
|
+
|
249
|
+
if self._open_browser_on_start:
|
250
|
+
self._handle_open_webui()
|
251
|
+
|
252
|
+
if self._tray_mode == "headless":
|
253
|
+
LOGGER.warning(
|
254
|
+
"Tray icon disabled (tray_mode=headless); press Ctrl+C to stop the server."
|
255
|
+
)
|
256
|
+
try:
|
257
|
+
while not self._stop_event.wait(0.5):
|
258
|
+
pass
|
259
|
+
finally:
|
260
|
+
self.stop()
|
261
|
+
return
|
262
|
+
|
263
|
+
icon_image = _load_icon()
|
264
|
+
menu = pystray.Menu(
|
265
|
+
pystray.MenuItem(
|
266
|
+
"Open GUI",
|
267
|
+
self._launch_gui,
|
268
|
+
default=True,
|
269
|
+
),
|
270
|
+
pystray.MenuItem("Open WebUI", self._handle_open_webui),
|
271
|
+
pystray.MenuItem("Quit", self._handle_quit),
|
272
|
+
)
|
273
|
+
self._icon = pystray.Icon(
|
274
|
+
"talks-reducer", icon_image, "Talks Reducer Server", menu=menu
|
275
|
+
)
|
276
|
+
|
277
|
+
if self._tray_mode == "pystray-detached":
|
278
|
+
LOGGER.info("Running tray icon in detached mode")
|
279
|
+
self._icon.run_detached()
|
280
|
+
try:
|
281
|
+
while not self._stop_event.wait(0.5):
|
282
|
+
pass
|
283
|
+
finally:
|
284
|
+
self.stop()
|
285
|
+
return
|
286
|
+
|
287
|
+
LOGGER.info("Running tray icon in blocking mode")
|
288
|
+
self._icon.run()
|
289
|
+
|
290
|
+
def stop(self) -> None:
|
291
|
+
"""Stop the tray icon and shut down the Gradio server."""
|
292
|
+
|
293
|
+
self._stop_event.set()
|
294
|
+
|
295
|
+
if self._icon is not None:
|
296
|
+
with suppress(Exception):
|
297
|
+
self._icon.visible = False
|
298
|
+
with suppress(Exception):
|
299
|
+
self._icon.stop()
|
300
|
+
|
301
|
+
self._stop_gui()
|
302
|
+
|
303
|
+
if self._server_handle is not None:
|
304
|
+
with suppress(Exception):
|
305
|
+
self._server_handle.close()
|
306
|
+
LOGGER.info("Shut down Talks Reducer server")
|
307
|
+
|
308
|
+
def _stop_gui(self) -> None:
|
309
|
+
"""Terminate the GUI subprocess if it is still running."""
|
310
|
+
|
311
|
+
with self._gui_lock:
|
312
|
+
process = self._gui_process
|
313
|
+
if process is None:
|
314
|
+
return
|
315
|
+
|
316
|
+
if process.poll() is None:
|
317
|
+
LOGGER.info("Stopping Talks Reducer GUI")
|
318
|
+
try:
|
319
|
+
process.terminate()
|
320
|
+
process.wait(timeout=5)
|
321
|
+
except subprocess.TimeoutExpired:
|
322
|
+
LOGGER.warning(
|
323
|
+
"GUI process did not exit cleanly; forcing termination"
|
324
|
+
)
|
325
|
+
process.kill()
|
326
|
+
process.wait(timeout=5)
|
327
|
+
except Exception as exc: # pragma: no cover - defensive cleanup
|
328
|
+
LOGGER.debug("Error while terminating GUI process: %s", exc)
|
329
|
+
|
330
|
+
self._gui_process = None
|
331
|
+
|
332
|
+
|
333
|
+
def main(argv: Optional[Sequence[str]] = None) -> None:
|
334
|
+
"""Launch the Gradio server with a companion system tray icon."""
|
335
|
+
|
336
|
+
parser = argparse.ArgumentParser(
|
337
|
+
description="Launch the Talks Reducer server with a system tray icon."
|
338
|
+
)
|
339
|
+
parser.add_argument(
|
340
|
+
"--host", dest="host", default=None, help="Custom host to bind."
|
341
|
+
)
|
342
|
+
parser.add_argument(
|
343
|
+
"--port",
|
344
|
+
dest="port",
|
345
|
+
type=int,
|
346
|
+
default=9005,
|
347
|
+
help="Port number for the web server (default: 9005).",
|
348
|
+
)
|
349
|
+
parser.add_argument(
|
350
|
+
"--share",
|
351
|
+
action="store_true",
|
352
|
+
help="Create a temporary public Gradio link.",
|
353
|
+
)
|
354
|
+
browser_group = parser.add_mutually_exclusive_group()
|
355
|
+
browser_group.add_argument(
|
356
|
+
"--open-browser",
|
357
|
+
dest="open_browser",
|
358
|
+
action="store_true",
|
359
|
+
help="Automatically open the web interface after startup.",
|
360
|
+
)
|
361
|
+
browser_group.add_argument(
|
362
|
+
"--no-browser",
|
363
|
+
dest="open_browser",
|
364
|
+
action="store_false",
|
365
|
+
help="Do not open the web interface automatically (default).",
|
366
|
+
)
|
367
|
+
parser.set_defaults(open_browser=False)
|
368
|
+
parser.add_argument(
|
369
|
+
"--tray-mode",
|
370
|
+
choices=("pystray", "pystray-detached", "headless"),
|
371
|
+
default="pystray",
|
372
|
+
help=(
|
373
|
+
"Select how the tray runs: foreground pystray (default), detached "
|
374
|
+
"pystray worker, or disable the tray entirely."
|
375
|
+
),
|
376
|
+
)
|
377
|
+
parser.add_argument(
|
378
|
+
"--debug",
|
379
|
+
action="store_true",
|
380
|
+
help="Enable verbose logging for troubleshooting.",
|
381
|
+
)
|
382
|
+
|
383
|
+
args = parser.parse_args(argv)
|
384
|
+
|
385
|
+
logging.basicConfig(
|
386
|
+
level=logging.DEBUG if args.debug else logging.INFO,
|
387
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
388
|
+
datefmt="%H:%M:%S",
|
389
|
+
)
|
390
|
+
|
391
|
+
if args.tray_mode != "headless" and PYSTRAY_IMPORT_ERROR is not None:
|
392
|
+
raise RuntimeError(
|
393
|
+
"System tray mode requires the 'pystray' dependency. Install it with "
|
394
|
+
"`pip install pystray` or `pip install talks-reducer[dev]` and try again."
|
395
|
+
) from PYSTRAY_IMPORT_ERROR
|
396
|
+
|
397
|
+
app = _ServerTrayApplication(
|
398
|
+
host=args.host,
|
399
|
+
port=args.port,
|
400
|
+
share=args.share,
|
401
|
+
open_browser=args.open_browser,
|
402
|
+
tray_mode=args.tray_mode,
|
403
|
+
)
|
404
|
+
|
405
|
+
atexit.register(app.stop)
|
406
|
+
|
407
|
+
try:
|
408
|
+
app.run()
|
409
|
+
except KeyboardInterrupt: # pragma: no cover - interactive convenience
|
410
|
+
app.stop()
|
411
|
+
|
412
|
+
|
413
|
+
__all__ = ["main"]
|
414
|
+
|
415
|
+
|
416
|
+
if __name__ == "__main__": # pragma: no cover - convenience entry point
|
417
|
+
main()
|
@@ -1,119 +1,142 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: talks-reducer
|
3
|
-
Version: 0.5.
|
4
|
-
Summary: CLI for speeding up long-form talks by removing silence
|
5
|
-
Author: Talks Reducer Maintainers
|
6
|
-
License-Expression: MIT
|
7
|
-
Requires-Python: >=3.9
|
8
|
-
Description-Content-Type: text/markdown
|
9
|
-
License-File: LICENSE
|
10
|
-
Requires-Dist: audiotsm>=0.1.2
|
11
|
-
Requires-Dist: scipy>=1.10.0
|
12
|
-
Requires-Dist: numpy>=1.22.0
|
13
|
-
Requires-Dist: tqdm>=4.65.0
|
14
|
-
Requires-Dist: tkinterdnd2>=0.3.0
|
15
|
-
Requires-Dist: Pillow>=9.0.0
|
16
|
-
Requires-Dist:
|
17
|
-
Requires-Dist:
|
18
|
-
|
19
|
-
|
20
|
-
Requires-Dist:
|
21
|
-
Requires-Dist:
|
22
|
-
Requires-Dist:
|
23
|
-
Requires-Dist:
|
24
|
-
Requires-Dist:
|
25
|
-
Requires-Dist:
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
Talks Reducer
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
- 1h
|
37
|
-
- 1h 19m,
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
- **
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
the
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: talks-reducer
|
3
|
+
Version: 0.5.2
|
4
|
+
Summary: CLI for speeding up long-form talks by removing silence
|
5
|
+
Author: Talks Reducer Maintainers
|
6
|
+
License-Expression: MIT
|
7
|
+
Requires-Python: >=3.9
|
8
|
+
Description-Content-Type: text/markdown
|
9
|
+
License-File: LICENSE
|
10
|
+
Requires-Dist: audiotsm>=0.1.2
|
11
|
+
Requires-Dist: scipy>=1.10.0
|
12
|
+
Requires-Dist: numpy>=1.22.0
|
13
|
+
Requires-Dist: tqdm>=4.65.0
|
14
|
+
Requires-Dist: tkinterdnd2>=0.3.0
|
15
|
+
Requires-Dist: Pillow>=9.0.0
|
16
|
+
Requires-Dist: pystray>=0.19.5
|
17
|
+
Requires-Dist: imageio-ffmpeg>=0.4.8
|
18
|
+
Requires-Dist: gradio>=4.0.0
|
19
|
+
Provides-Extra: dev
|
20
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
21
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
22
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
23
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
24
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
25
|
+
Requires-Dist: bump-my-version>=0.5.0; extra == "dev"
|
26
|
+
Requires-Dist: pyinstaller>=6.4.0; extra == "dev"
|
27
|
+
Dynamic: license-file
|
28
|
+
|
29
|
+
# Talks Reducer
|
30
|
+
Talks Reducer shortens long-form presentations by removing silent gaps and optionally re-encoding them to smaller files. The
|
31
|
+
project was renamed from **jumpcutter** to emphasize its focus on conference talks and screencasts.
|
32
|
+
|
33
|
+

|
34
|
+
|
35
|
+
## Example
|
36
|
+
- 1h 37m, 571 MB — Original OBS video recording
|
37
|
+
- 1h 19m, 751 MB — Talks Reducer
|
38
|
+
- 1h 19m, 171 MB — Talks Reducer `--small`
|
39
|
+
|
40
|
+
## Changelog
|
41
|
+
|
42
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
43
|
+
|
44
|
+
## Install GUI (Windows, macOS)
|
45
|
+
Go to the [releases page](https://github.com/popstas/talks-reducer/releases) and download the appropriate artifact:
|
46
|
+
|
47
|
+
- **Windows** — `talks-reducer-windows-0.4.0.zip`
|
48
|
+
- **macOS** — `talks-reducer.app.zip`
|
49
|
+
|
50
|
+
> **Troubleshooting:** If launching the bundle (or running `python talks_reducer/gui.py`) prints `macOS 26 (2600) or later required, have instead 16 (1600)!`, make sure you're using a Python build that ships a modern Tk. The stock [python.org 3.13.5 installer](https://www.python.org/downloads/release/python-3135/) includes Tk 8.6 and has been verified to work.
|
51
|
+
|
52
|
+
When extracted on Windows the bundled `talks-reducer.exe` behaves like the
|
53
|
+
`python talks_reducer/gui.py` entry point: double-clicking it launches the GUI
|
54
|
+
and passing a video file path (for example via *Open with…* or drag-and-drop
|
55
|
+
onto the executable) automatically queues that recording for processing.
|
56
|
+
|
57
|
+
## Install CLI (Linux, Windows, macOS)
|
58
|
+
```
|
59
|
+
pip install talks-reducer
|
60
|
+
```
|
61
|
+
|
62
|
+
**Note:** FFmpeg is now bundled automatically with the package, so you don't need to install it separately. You you need, don't know actually.
|
63
|
+
|
64
|
+
The `--small` preset applies a 720p video scale and 128 kbps audio bitrate, making it useful for sharing talks over constrained
|
65
|
+
connections. Without `--small`, the script aims to preserve original quality while removing silence.
|
66
|
+
|
67
|
+
Example CLI usage:
|
68
|
+
|
69
|
+
```sh
|
70
|
+
talks-reducer --small input.mp4
|
71
|
+
```
|
72
|
+
|
73
|
+
### Speech detection
|
74
|
+
|
75
|
+
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.
|
76
|
+
|
77
|
+
When CUDA-capable hardware is available the pipeline leans on GPU encoders to keep export times low, but it still runs great on
|
78
|
+
CPUs.
|
79
|
+
|
80
|
+
## Simple web server
|
81
|
+
|
82
|
+
Prefer a lightweight browser interface? Launch the Gradio-powered simple mode with:
|
83
|
+
|
84
|
+
```sh
|
85
|
+
talks-reducer server
|
86
|
+
```
|
87
|
+
|
88
|
+
Want the server to live in your system tray instead of a terminal window? Use:
|
89
|
+
|
90
|
+
```sh
|
91
|
+
talks-reducer server-tray
|
92
|
+
```
|
93
|
+
|
94
|
+
Pass `--debug` to print verbose logs about the tray icon lifecycle, and
|
95
|
+
`--tray-mode pystray-detached` to try pystray's alternate detached runner. If
|
96
|
+
the icon backend refuses to appear, fall back to `--tray-mode headless` to keep
|
97
|
+
the web server running without a tray process. The tray menu includes an **Open GUI**
|
98
|
+
item (also triggered by double-clicking the icon) that launches the desktop
|
99
|
+
Talks Reducer interface alongside an **Open WebUI** entry that opens the Gradio
|
100
|
+
page in your browser. Close the GUI window to return to the tray without
|
101
|
+
stopping the server. Launching the GUI directly now starts the tray-backed
|
102
|
+
server in the background before the window appears so the icon stays available
|
103
|
+
after you close it; add `--no-tray` when running `python -m talks_reducer.gui`
|
104
|
+
if you prefer to skip the background server entirely. The tray command itself
|
105
|
+
never launches the GUI automatically, so use the menu item (or rerun the GUI
|
106
|
+
with `--no-tray`) whenever you want to reopen it. The tray no longer opens a
|
107
|
+
browser automatically—pass `--open-browser` if you prefer the web page to
|
108
|
+
launch as soon as the server is ready.
|
109
|
+
|
110
|
+
This opens a local web page featuring a drag-and-drop upload zone, a **Small video** checkbox that mirrors the CLI preset, a live
|
111
|
+
progress indicator, and automatic previews of the processed output. Once the job completes you can inspect the resulting compression
|
112
|
+
ratio and download the rendered video directly from the page.
|
113
|
+
|
114
|
+
### Uploading and retrieving a processed video
|
115
|
+
|
116
|
+
1. Open the printed `http://localhost:<port>` address (the default port is `9005`).
|
117
|
+
2. Drag a video onto the **Video file** drop zone or click to browse and select one from disk.
|
118
|
+
3. **Small video** starts enabled to apply the 720p/128 kbps preset. Clear the box before the upload finishes if you want to keep the original resolution and bitrate.
|
119
|
+
4. Wait for the progress bar and log to report completion—the interface queues work automatically after the file arrives.
|
120
|
+
5. Watch the processed preview in the **Processed video** player and click **Download processed file** to save the result locally.
|
121
|
+
|
122
|
+
Need to change where the server listens? Run `talks-reducer server --host 0.0.0.0 --port 7860` (or any other port) to bind to a
|
123
|
+
different address.
|
124
|
+
|
125
|
+
### Automating uploads from the command line
|
126
|
+
|
127
|
+
Prefer to script uploads instead of using the browser UI? Start the server and use the bundled helper to submit a job and save
|
128
|
+
the processed video locally:
|
129
|
+
|
130
|
+
```sh
|
131
|
+
python -m talks_reducer.service_client --server http://127.0.0.1:9005/ --input demo.mp4 --output output/demo_processed.mp4
|
132
|
+
```
|
133
|
+
|
134
|
+
The helper wraps the Gradio API exposed by `server.py`, waits for processing to complete, then copies the rendered file to the
|
135
|
+
path you provide. Pass `--small` to mirror the **Small video** checkbox or `--print-log` to stream the server log after the
|
136
|
+
download finishes.
|
137
|
+
|
138
|
+
## Contributing
|
139
|
+
See `CONTRIBUTION.md` for development setup details and guidance on sharing improvements.
|
140
|
+
|
141
|
+
## License
|
142
|
+
Talks Reducer is released under the MIT License. See `LICENSE` for the full text.
|
@@ -1,19 +1,20 @@
|
|
1
|
-
talks_reducer/__about__.py,sha256=
|
1
|
+
talks_reducer/__about__.py,sha256=IrybQ0W3057SlpgRUduOAhBgwbyN7X0mC3g7VQj2SxA,92
|
2
2
|
talks_reducer/__init__.py,sha256=Kzh1hXaw6Vq3DyTqrnJGOq8pn0P8lvaDcsg1bFUjFKk,208
|
3
3
|
talks_reducer/__main__.py,sha256=azR_vh8HFPLaOnh-L6gUFWsL67I6iHtbeH5rQhsipGY,299
|
4
4
|
talks_reducer/audio.py,sha256=sjHMeY0H9ESG-Gn5BX0wFRBX7sXjWwsgS8u9Vb0bJ88,4396
|
5
5
|
talks_reducer/chunks.py,sha256=IpdZxRFPURSG5wP-OQ_p09CVP8wcKwIFysV29zOTSWI,2959
|
6
6
|
talks_reducer/cli.py,sha256=VOFZni7rl2CTCK3aLUO_iCV6PNva9ylIrmxxwNltQG4,8031
|
7
7
|
talks_reducer/ffmpeg.py,sha256=dsHBOBcr5XCSg0q3xmzLOcibBiEdyrXdEQa-ze5vQsM,12551
|
8
|
-
talks_reducer/gui.py,sha256
|
8
|
+
talks_reducer/gui.py,sha256=-zBvuPv9T4Z1mZ90QwRZEUUY65mUpWRKCgRZyV5aEPM,61121
|
9
9
|
talks_reducer/models.py,sha256=6Q_8rmHLyImXp88D4B7ptTbFaH_xXa_yxs8A2dypz2Y,2004
|
10
10
|
talks_reducer/pipeline.py,sha256=JnWa84sMwYncGv7crhGLZu0cW1Xx0eGQyFP-nLp-DHk,12222
|
11
11
|
talks_reducer/progress.py,sha256=Mh43M6VWhjjUv9CI22xfD2EJ_7Aq3PCueqefQ9Bd5-o,4565
|
12
|
-
talks_reducer/server.py,sha256=
|
12
|
+
talks_reducer/server.py,sha256=Czc1q6N7LJKsq-dBEum21zw31kA1uXBKC8T1-NtiscA,11235
|
13
|
+
talks_reducer/server_tray.py,sha256=O8eiu31iWFvP2AROFG1OZ83n0aePtuSZqd7RE6NAqQw,13377
|
13
14
|
talks_reducer/service_client.py,sha256=Hv3hKBUn2n7M5hLUVHamNphUdMt9bCXUrJaJMZHlE0Q,3088
|
14
|
-
talks_reducer-0.5.
|
15
|
-
talks_reducer-0.5.
|
16
|
-
talks_reducer-0.5.
|
17
|
-
talks_reducer-0.5.
|
18
|
-
talks_reducer-0.5.
|
19
|
-
talks_reducer-0.5.
|
15
|
+
talks_reducer-0.5.2.dist-info/licenses/LICENSE,sha256=jN17mHNR3e84awmH3AbpWBcBDBzPxEH0rcOFoj1s7sQ,1124
|
16
|
+
talks_reducer-0.5.2.dist-info/METADATA,sha256=shQxqR1Z6PayVws-5cZ8FAhTeR_1jo7pfpEPFDloN4o,6702
|
17
|
+
talks_reducer-0.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
18
|
+
talks_reducer-0.5.2.dist-info/entry_points.txt,sha256=X2pjoh2vWBXXExVWorv1mbA1aTEVP3fyuZH4AixqZK4,208
|
19
|
+
talks_reducer-0.5.2.dist-info/top_level.txt,sha256=pJWGcy__LR9JIEKH3QJyFmk9XrIsiFtqvuMNxFdIzDU,14
|
20
|
+
talks_reducer-0.5.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|