talks-reducer 0.7.1__py3-none-any.whl → 0.7.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.
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.7.1"
5
+ __version__ = "0.7.2"
@@ -325,7 +325,7 @@ class TalksReducerGUI:
325
325
 
326
326
  self._full_size = (1000, 800)
327
327
  self._simple_size = (300, 270)
328
- self.root.geometry(f"{self._full_size[0]}x{self._full_size[1]}")
328
+ # self.root.geometry(f"{self._full_size[0]}x{self._full_size[1]}")
329
329
  self.style = self.ttk.Style(self.root)
330
330
 
331
331
  self._processing_thread: Optional[threading.Thread] = None
@@ -3,9 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sys
6
- from pathlib import Path
7
6
  from typing import TYPE_CHECKING, Callable
8
7
 
8
+ from ..icons import find_icon_path
9
9
  from ..models import default_temp_folder
10
10
 
11
11
  if TYPE_CHECKING: # pragma: no cover - imported for type checking only
@@ -454,37 +454,24 @@ def reset_basic_defaults(gui: "TalksReducerGUI") -> None:
454
454
  def apply_window_icon(gui: "TalksReducerGUI") -> None:
455
455
  """Configure the application icon when the asset is available."""
456
456
 
457
- base_path = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent.parent))
458
-
459
- icon_candidates: list[tuple[Path, str]] = []
460
- if sys.platform.startswith("win"):
461
- icon_candidates.append(
462
- (
463
- base_path / "talks_reducer" / "resources" / "icons" / "icon.ico",
464
- "ico",
465
- )
466
- )
467
- icon_candidates.append(
468
- (
469
- base_path / "talks_reducer" / "resources" / "icons" / "icon.png",
470
- "png",
471
- )
457
+ icon_filenames = (
458
+ ("app.ico", "app.png")
459
+ if sys.platform.startswith("win")
460
+ else ("app.png", "app.ico")
472
461
  )
462
+ icon_path = find_icon_path(filenames=icon_filenames)
463
+ if icon_path is None:
464
+ return
473
465
 
474
- for icon_path, icon_type in icon_candidates:
475
- if not icon_path.is_file():
476
- continue
477
-
478
- try:
479
- if icon_type == "ico" and sys.platform.startswith("win"):
480
- # On Windows, iconbitmap works better without the 'default' parameter
481
- gui.root.iconbitmap(str(icon_path))
482
- else:
483
- gui.root.iconphoto(False, gui.tk.PhotoImage(file=str(icon_path)))
484
- return
485
- except (gui.tk.TclError, Exception):
486
- # Missing Tk image support or invalid icon format - try next candidate
487
- continue
466
+ try:
467
+ if icon_path.suffix.lower() == ".ico" and sys.platform.startswith("win"):
468
+ # On Windows, iconbitmap works better without the 'default' parameter.
469
+ gui.root.iconbitmap(str(icon_path))
470
+ else:
471
+ gui.root.iconphoto(False, gui.tk.PhotoImage(file=str(icon_path)))
472
+ except (gui.tk.TclError, Exception):
473
+ # Missing Tk image support or invalid icon format - fail silently.
474
+ return
488
475
 
489
476
 
490
477
  def apply_window_size(gui: "TalksReducerGUI", *, simple: bool) -> None:
talks_reducer/icons.py ADDED
@@ -0,0 +1,121 @@
1
+ """Icon discovery helpers shared across Talks Reducer entry points."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from contextlib import suppress
8
+ from pathlib import Path
9
+ from typing import Iterator, Optional, Sequence
10
+
11
+ LOGGER = logging.getLogger(__name__)
12
+
13
+ _ICON_RELATIVE_PATHS: Sequence[Path] = (
14
+ Path("talks_reducer") / "resources" / "icons",
15
+ Path("docs") / "assets",
16
+ )
17
+ _ICON_PATH_SUFFIXES: Sequence[Path] = (
18
+ Path(""),
19
+ Path("_internal"),
20
+ Path("Contents") / "Resources",
21
+ Path("Resources"),
22
+ )
23
+
24
+
25
+ def _iter_base_roots(module_file: Optional[Path | str] = None) -> Iterator[Path]:
26
+ """Yield base directories where icon assets may live."""
27
+
28
+ module_path = Path(module_file or __file__).resolve()
29
+ package_root = module_path.parent
30
+ project_root = package_root.parent
31
+
32
+ seen: set[Path] = set()
33
+
34
+ def _yield(path: Optional[Path]) -> Iterator[Path]:
35
+ if path is None:
36
+ return iter(())
37
+ resolved = path.resolve()
38
+ if resolved in seen:
39
+ return iter(())
40
+ seen.add(resolved)
41
+ return iter((resolved,))
42
+
43
+ for root in (
44
+ package_root,
45
+ project_root,
46
+ ):
47
+ yield from _yield(root)
48
+
49
+ frozen_root: Optional[Path] = None
50
+ frozen_value = getattr(sys, "_MEIPASS", None)
51
+ if frozen_value:
52
+ with suppress(Exception):
53
+ frozen_root = Path(str(frozen_value))
54
+ if frozen_root is not None:
55
+ yield from _yield(frozen_root)
56
+
57
+ with suppress(Exception):
58
+ executable_root = Path(sys.executable).resolve().parent
59
+ yield from _yield(executable_root)
60
+
61
+ with suppress(Exception):
62
+ launcher_root = Path(sys.argv[0]).resolve().parent
63
+ yield from _yield(launcher_root)
64
+
65
+
66
+ def iter_icon_candidates(
67
+ *,
68
+ filenames: Sequence[str],
69
+ relative_paths: Sequence[Path] | None = None,
70
+ module_file: Optional[Path | str] = None,
71
+ ) -> Iterator[Path]:
72
+ """Yield possible icon paths ordered from most to least specific."""
73
+
74
+ if relative_paths is None:
75
+ relative_paths = _ICON_RELATIVE_PATHS
76
+
77
+ seen: set[Path] = set()
78
+ for base_root in _iter_base_roots(module_file=module_file):
79
+ for suffix in _ICON_PATH_SUFFIXES:
80
+ candidate_root = (base_root / suffix).resolve()
81
+ if candidate_root in seen:
82
+ continue
83
+ seen.add(candidate_root)
84
+ LOGGER.debug("Considering icon root: %s", candidate_root)
85
+
86
+ if not candidate_root.exists():
87
+ LOGGER.debug("Skipping missing icon root: %s", candidate_root)
88
+ continue
89
+
90
+ for relative in relative_paths:
91
+ candidate_base = (candidate_root / relative).resolve()
92
+ if not candidate_base.exists():
93
+ LOGGER.debug("Skipping missing icon directory: %s", candidate_base)
94
+ continue
95
+ for name in filenames:
96
+ candidate = (candidate_base / name).resolve()
97
+ LOGGER.debug("Checking icon candidate: %s", candidate)
98
+ yield candidate
99
+
100
+
101
+ def find_icon_path(
102
+ *,
103
+ filenames: Sequence[str],
104
+ relative_paths: Sequence[Path] | None = None,
105
+ module_file: Optional[Path | str] = None,
106
+ ) -> Optional[Path]:
107
+ """Return the first existing icon path matching *filenames* or ``None``."""
108
+
109
+ for candidate in iter_icon_candidates(
110
+ filenames=filenames,
111
+ relative_paths=relative_paths,
112
+ module_file=module_file,
113
+ ):
114
+ if candidate.is_file():
115
+ LOGGER.info("Found icon at %s", candidate)
116
+ return candidate
117
+ LOGGER.warning("Unable to locate Talks Reducer icon; checked %s", filenames)
118
+ return None
119
+
120
+
121
+ __all__ = ["find_icon_path", "iter_icon_candidates"]
talks_reducer/server.py CHANGED
@@ -6,6 +6,7 @@ import argparse
6
6
  import atexit
7
7
  import shutil
8
8
  import socket
9
+ import sys
9
10
  import tempfile
10
11
  from contextlib import AbstractContextManager, suppress
11
12
  from pathlib import Path
@@ -16,6 +17,7 @@ from typing import Callable, Iterator, Optional, Sequence, cast
16
17
  import gradio as gr
17
18
 
18
19
  from talks_reducer.ffmpeg import FFmpegNotFoundError
20
+ from talks_reducer.icons import find_icon_path
19
21
  from talks_reducer.models import ProcessingOptions, ProcessingResult
20
22
  from talks_reducer.pipeline import speed_up_video
21
23
  from talks_reducer.progress import ProgressHandle, SignalProgressReporter
@@ -144,13 +146,10 @@ class GradioProgressReporter(SignalProgressReporter):
144
146
  self._progress_callback(bounded_current, total_value, display_desc)
145
147
 
146
148
 
147
- _FAVICON_CANDIDATES = (
148
- Path(__file__).resolve().parent / "resources" / "icons" / "icon.ico",
149
- Path(__file__).resolve().parent.parent / "docs" / "assets" / "icon.ico",
150
- )
151
- _FAVICON_PATH: Optional[Path] = next(
152
- (path for path in _FAVICON_CANDIDATES if path.exists()), None
149
+ _FAVICON_FILENAMES = (
150
+ ("app.ico", "app.png") if sys.platform.startswith("win") else ("app.png", "app.ico")
153
151
  )
152
+ _FAVICON_PATH = find_icon_path(filenames=_FAVICON_FILENAMES)
154
153
  _FAVICON_PATH_STR = str(_FAVICON_PATH) if _FAVICON_PATH else None
155
154
  _WORKSPACES: list[Path] = []
156
155
 
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import argparse
6
6
  import atexit
7
- import base64
8
7
  import logging
9
8
  import subprocess
10
9
  import sys
@@ -12,14 +11,13 @@ import threading
12
11
  import time
13
12
  import webbrowser
14
13
  from contextlib import suppress
15
- from importlib import resources
16
- from io import BytesIO
17
14
  from pathlib import Path
18
15
  from typing import Any, Iterator, Optional, Sequence
19
16
  from urllib.parse import urlsplit, urlunsplit
20
17
 
21
18
  from PIL import Image
22
19
 
20
+ from .icons import iter_icon_candidates
23
21
  from .server import build_interface
24
22
  from .version_utils import resolve_version
25
23
 
@@ -78,208 +76,59 @@ def _normalize_local_url(url: str, host: Optional[str], port: int) -> str:
78
76
  return url
79
77
 
80
78
 
79
+ if sys.platform.startswith("win"):
80
+ _TRAY_ICON_FILENAMES = ("icon.ico", "icon.png", "app.ico", "app.png", "app-256.png")
81
+ else:
82
+ _TRAY_ICON_FILENAMES = ("icon.png", "icon.ico", "app.png", "app.ico", "app-256.png")
83
+ _ICON_RELATIVE_PATHS = (
84
+ Path("talks_reducer") / "resources" / "icons",
85
+ Path("docs") / "assets",
86
+ )
87
+
88
+
81
89
  def _iter_icon_candidates() -> Iterator[Path]:
82
90
  """Yield possible tray icon paths ordered from most to least specific."""
83
91
 
84
- module_path = Path(__file__).resolve()
85
- package_root = module_path.parent
86
- project_root = package_root.parent
87
-
88
- frozen_root: Optional[Path] = None
89
- frozen_value = getattr(sys, "_MEIPASS", None)
90
- if frozen_value:
91
- with suppress(Exception):
92
- frozen_root = Path(str(frozen_value)).resolve()
93
-
94
- executable_root: Optional[Path] = None
95
- with suppress(Exception):
96
- executable_root = Path(sys.executable).resolve().parent
97
-
98
- launcher_root: Optional[Path] = None
99
- with suppress(Exception):
100
- launcher_root = Path(sys.argv[0]).resolve().parent
101
-
102
- base_roots: list[Path] = []
103
- for candidate in (
104
- package_root,
105
- project_root,
106
- frozen_root,
107
- executable_root,
108
- launcher_root,
109
- ):
110
- if candidate and candidate not in base_roots:
111
- base_roots.append(candidate)
112
-
113
- expanded_roots: list[Path] = []
114
- suffixes = (
115
- Path(""),
116
- Path("_internal"),
117
- Path("Contents") / "Resources",
118
- Path("Resources"),
92
+ yield from iter_icon_candidates(
93
+ filenames=_TRAY_ICON_FILENAMES,
94
+ relative_paths=_ICON_RELATIVE_PATHS,
95
+ module_file=Path(__file__),
119
96
  )
120
- for root in base_roots:
121
- for suffix in suffixes:
122
- candidate_root = (root / suffix).resolve()
123
- if candidate_root not in expanded_roots:
124
- expanded_roots.append(candidate_root)
125
-
126
- icon_names = ("icon.ico", "icon.png") if sys.platform == "win32" else ("icon.png", "icon.ico")
127
- relative_paths = (
128
- Path("talks_reducer") / "resources" / "icons",
129
- Path("talks_reducer") / "assets",
130
- Path("docs") / "assets",
131
- Path("assets"),
132
- Path(""),
133
- )
134
-
135
- seen: set[Path] = set()
136
- for root in expanded_roots:
137
- if not root.exists():
138
- continue
139
- for relative in relative_paths:
140
- for icon_name in icon_names:
141
- candidate = (root / relative / icon_name).resolve()
142
- if candidate in seen:
143
- continue
144
- seen.add(candidate)
145
- yield candidate
146
97
 
147
98
 
148
- def _load_icon() -> Image.Image:
149
- """Load the tray icon image, falling back to the embedded pen artwork."""
150
-
151
- LOGGER.debug("Attempting to load tray icon image.")
99
+ def _generate_fallback_icon() -> Image.Image:
100
+ """Return a simple multi-color square used when packaged icons are missing."""
152
101
 
153
- for candidate in _iter_icon_candidates():
154
- LOGGER.debug("Checking icon candidate at %s", candidate)
155
- if candidate.exists():
156
- try:
157
- with Image.open(candidate) as image:
158
- loaded = image.copy()
159
- except Exception as exc: # pragma: no cover - diagnostic log
160
- LOGGER.warning("Failed to load tray icon from %s: %s", candidate, exc)
161
- else:
162
- LOGGER.debug("Loaded tray icon from %s", candidate)
163
- return loaded
164
-
165
- LOGGER.warning("Falling back to generated tray icon; packaged image not found")
166
102
  image = Image.new("RGBA", (64, 64), color=(37, 99, 235, 255))
103
+ for index in range(64):
104
+ image.putpixel((index, index), (17, 24, 39, 255))
105
+ image.putpixel((63 - index, index), (59, 130, 246, 255))
167
106
  image.putpixel((0, 0), (255, 255, 255, 255))
168
- image.putpixel((63, 63), (17, 24, 39, 255))
107
+ image.putpixel((63, 63), (59, 130, 246, 255))
169
108
  return image
170
109
 
171
110
 
172
- _EMBEDDED_ICON_BASE64 = (
173
- "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACx"
174
- "jwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAA3MSURBVHhe5Zt7cB31dcc/Z/deyVfPK11JDsLW"
175
- "xAEmMJDGk+mEFGza8oiZ8l+GSWiwMTBJQzDpA2xT4kcKpsWxoSl0SiCTEAPFDYWkEJtCeYSnG0oh"
176
- "sQHbacCWbcmyZOlKV5L1vLt7+sdvf3d3rx6hnfwT6Tuzurvnd36Pc37nd37nd3YlzID65o+1OOn0"
177
- "FSCXIXKeQJuq1pTz/RYggJYTZ8Fs/AIgIqOgHaq8B/p8UCzuHuzt7ilnxlaIo64+W+VU1a4Tx7lR"
178
- "RFoiFgUx9xL2r/Hq4ZAibsMfqw2qhiMhgiJhu4qG3YQ9qGGSkF9j/YdFEcIyiOrFnntVg+/6Y6e2"
179
- "DQ0MjMSLEgrILmz9pKRSO0Wcz5jCSNQ5AQ32Br6/stB9fL8llRRQv7D1HCeVfl7EWWQFNgqYI8Jb"
180
- "qPYGXnFFoafrl1gF1DY21aUz1W8g8qnZFticgeoH/uT4Hwz29uQdgNSCzCZEPlXON2chcpaTrrwD"
181
- "QLItpy1x0hX7xHFqAbTcgcwxxJz0WFAsLnXETX1BRGoteb5ARDLipq5yELkkuVfNbUQiCiJymSMi"
182
- "54KAzjl//1Hwe46ii6AskJgnUKhzUJzygrkPG1GCMz9WfjlK8a3Ow9lPIlLAPLQDQCIFzD8fCAkL"
183
- "mFcozXbSByjQ32mugYm5vDOW1rs4Vh0aKANdynVXCbetcThrkZLvUIK57RtUGlrbAnFd6e8IeOD2"
184
- "FKsvqqTSFY4MeDz0UpE77/PBgVyrSY4oIPrRfEagSuH40RglRdPixQT/hwOXAPlhYLC85P+BZshV"
185
- "hjKIEAQB0tDaFkyqKyNVAe3fyfDxOgd8wAVP4M3DRbb9uMiu3YrbDNlKCFSSy2MahfiBMth1lNu3"
186
- "bOFz55/PyMgIj//rEzz+LzvJLV7ykU6drkBvN3zpIuGK3xcy6WBKtisOEUEVRMKUmU3JCRRGhEde"
187
- "DXj9V9DUCEqkAB3zXSqaA97fmmFxjYv6CmHqjjQUxpWn3y5y7Q+K8CFkW8FxZjeB/s4j7Hj4YVat"
188
- "WoUTaqswOMg99/w9d2654zcqwRHoy8NNK4QtqzNks6lo6QrJvJ/YSSgbU2liDEPHiUn+8sFxfrIP"
189
- "cjVGAaETVPzA+AHbiW1TJyGbElYvq+TDuzN88xsOhS6l/4SajKFdFzEURsc545zzuHzFipLwANn6"
190
- "ev761vVs3LSZfEd7oqwcgQKjyp/+cZpsQxr1FQ3HGN2Hl29+sWW+hrTk8+LWCq6/LAUDkf4cAClN"
191
- "t4FVcImuQBHOyDr8zZUZXv1BJVd8Xsh3wtCETlF8UCzS1JiloqIiWQBUV1dz6/p13HzLWvo62n+D"
192
- "JQmZSjuAj4AEW7KO7cW0FxFKZwHDbqi2qk1Tq4b3gZD2hIvOTPPoX2XYcVcKLwv5TjMjFvW11fzX"
193
- "njc4fLg9IsZQU1PDxg3fZNU1q+k7Nr0SLOX1d4toMUBcQVwQR2KX2cfshVtGiz2TgsnxgBf3Gqdu"
194
- "23cztfXf8lREquCmS9PUVzpGAyLGNsRah0SqCiDjCkuXpLh6mUtVLuDFnyljo1BVY+qMDRUYHhll"
195
- "+bJlVFdXlwSzyGQynH/+Z/ng0GH2vvUm1dmGKQ6ushaeelNpqfapSgUMDHnkC2XXgE9+0Cdf8MkP"
196
- "euQLPv3hc1/BPnt0nvR45MVJbn8kINdq5FNVJNvaFoz7jrg55cDWDG21rvEFYpIk0RqIz1LMXlJQ"
197
- "BPZ8WGT7k0X+/Vkl1QwNGYfeY+2svGY1d2/fxsKWllj9CMeOHeOGG9fw7DO7aVq8ZMoWGSgUjjPF"
198
- "pGdFfNUkVpCQM9kPsNvgjAqYUttsKeYpPhhBRCEF/ePKLrtbHILcIod8ZzurVl/L9m3fnlEJ7e3t"
199
- "rLr2eva89kpJCdavWH14Cv4MOnDEKCo+TWp28lI7TshnYeOAcAk44lYpa2JLoOQAw0pJR2nM3FyG"
200
- "oj5UucLSj7usXJ6iuinghZd8qGvk3bdeoftkL8suvGDa5dDQ0MCyCy/gtT0/p/1/DlBd32DaDIUa"
201
- "6ISJYaU4zLTX5DQ0S5+YEGrDlG8cJmZQlWzr4mDcdyXdpLx/l7EAmx+0b4aMjFbS6W5DZ2mfXSgK"
202
- "7PnAY9OOSd54T2DQWMLd27fR0txsx5HAgYMH+dKXV/L+3n00LW7DV2WgD+5a6XDZZ1wq09YijCkI"
203
- "gtqNSs07RvuLwNCY8MRrHv/wZEDTonBrDSEIgfo2EHJIN4cKqDEKMN1Ys7d34fYYM6U4bGAj9k+F"
204
- "sK/TY+nN49TiMNzVzvVf+Qrbtm4ll8uVVwdg//79fOGqq2k/0U0xn+HbX3O55coMbrr84BqXJu6v"
205
- "YnDg1LDPhodGuW+30hjTu10CpVYdjU+pifvjwmu4HRIzzSSmUphUzlnocs2FDsMnlKa2T/DQ97/P"
206
- "bRs2UChMH9yfe+65/Nl1qynmewDl4k+7uGkHDZ2AerFAxwuDIC8wv6Wgx1wUlZoalys+68LENOOz"
207
- "gZACKRVzyFE7k/bNsFFCuXKtyVv+uPO2dIAJT+kfVqgKNQdkFmSmthdDKp0q3Re9qOHEBhENLjnz"
208
- "oQzm1tDHi2U8MTix5qcecEqXjQHKoDrz22PHnCOee89j93NKrkHo62jn5rVr2WLH7WTr68trAHC4"
209
- "vZ0dj+6E+maoEJ76T49Tw54Jglwb3IQBjpsMhMSRMGAKeVzoOenx2M98yJb3ZCDZ0AfUNSm/uCvD"
210
- "ongcENOb2iURyitmfcRKwnsxscGJ4YB/frXI+ns86hc6DHa1c8vadWzatJH6urqQP4kjR4/y9TU3"
211
- "8dwzu2luM4elvm746ueFS5c6VKQil5wwM2KOJ7ReERiZhCf3+Dz139DUFDlBK1mgJg7QMd+hvkl5"
212
- "J6GAqP34FphQQJyA8f5jvvLCAY9NjxZ59y2l4XSHgePt3LJuHZs3bqRuBuGPHj3GV792Ay/8x7M0"
213
- "tS0hCEcrQL4AnCIpcKk0jtI0ReUN0FST3AEgcoKSbW3TUd8h26S8szXDohq37Jha3okR2n7WUpp1"
214
- "F97v9vmnXZM88GgAdUJzvdDb0c7adevZtHHDjMIf6+jghq+v4dlndpUCIRvcYKPByWnkp0zeEAvS"
215
- "UOUmaeWIKyAY8x2ZsgRKnOGvxgQO6YIx997RgCd+XmTN/R70Co2LwBGz5tetv5VNGzdQO100AnR3"
216
- "d3PjTd/g33785JRQ2BHomwR64IKlSk2FEtgNOWaKcatU4KVjgg5Ac8vM0aOJA2IKqG1SfmktILCC"
217
- "x2rHFCCYGZ8EXv/A486dE7zyMmQWCpm04ervPMJNf/4X/O2WO2ac+ZMnT3Lz2nU89ugjCbO3GPTg"
218
- "EzXw4A1plp6ZxgkFjH+4k7BPMX/6BpV7nxrnH3crueYpBgJJC1isY75LTU7ZuzVuAWaKBbPNRekm"
219
- "M+sf5n2+99wk2x/0YQHkmqJtc6LoMdLTycGDv+Lssz9Z1rVBPp9n7br17PjhQ9MKT5ihfnxzii9e"
220
- "XAWx4/a0a93+qDkWd5wosmLzKAcHoTGclDisBTgmxSmJg0KpQZsHCK1B0krBC3h4zwRnrR1j+4M+"
221
- "DadDYy4yQ4CRoRGW/9HFnHbaxyJiDP39/WzYtHlW4Q1JOXuxOZtoEMsI2Webq1A1fstmtXxlYYPL"
222
- "5860znMahCKWIsGE/Gh0qdljPQfeOORxzb1jXHtbEXcIcotKAXKiAbeqko6uE4yOjUXEEEb4TTz4"
223
- "3ftnFJ5wPYPQ1RfmJ0uXIPYwFvqD0l1YjgPDoz4f9ACZ8paTcAQlUKXSSR4XCQchFXB0KGDLT8ZZ"
224
- "ft0Eu55VcouE+srQ5EWSH0wC9ZkFHPn1QZ5++mk8zyvRe3t72bj5Wzxw//3G4c0gPHZCcvCdp4oc"
225
- "PDTBZFHxvGCaK0YvKr6v9Pf77Hh+nDfegcYF5S0nIdnWNi2qw0ha+fC+DGdkXWN/Lpzy4Ll3i6zd"
226
- "McnRfbFssF2CFmVLkdBvDBw/ym0bNvCHy5fTPzDAYzt/xDO7np515uNwBPpOQbYavrhUqF1gT6kR"
227
- "yleuIvyiHV5+W8mVvnicikQc4Lou+Y6Av7vF5YbLF5BJCQe6PO796SSPPB5AA+Sqky9GSsdQylZA"
228
- "QgkwcPxIRHCqaTq9ZUrWZzYIcMqHiUGTmJ0VttnsR5j5eBzguq4ISl+HcsmlQnOD8KOXFfqYVYu/"
229
- "yzAK8CMFEJpt/xAwArUtUOHOTeGJWUBpF7Dhb2MdNJ4G6bksfOw+2gbLdoC5jGhi5+UXYnFIbAmU"
230
- "7eXzBeWxz7xD6QuR+aSIhBMUkcnY8zyD4iAcM1vgXN30ZsWgg2r4D0TzaBFECd/3HFV9IaLPIyWY"
231
- "4O9lRz3vp6hO/5pmDsJOsqoW1fefcAonT3SoBg9ZHzCnraAkmqJB8Hihp+s9B8AfH9uigf46wTwn"
232
- "Ec5+oN3qFTdgzwJD/X0Dge9frar9EV/4eczvOkrfMYRvulXH1PdXFk6eOEb8MFTo7nxbPe9yDYJD"
233
- "iS9BbTLOPMxymfIoUxc+xzqfymOo09RKtBmrHD7GCYYo4Thtf8lxm11eg6Az8Ip/MtDd+ZIlJ96f"
234
- "jJ8a6qqoXLBTHLca4TxESgll03Akb7leTKchzSYvEwMPRUzwmMpRu3bwlseUSVxAq5C4kJYU664E"
235
- "c8yf0CB4WIuTXy6cPPF+vHjaOgD1Laed6abSVyJyCfDpQLW57B2xTB89WXI4qhJCmrUutbRpEG/Z"
236
- "tjEDa4ktORoRkR5F96P6ivr+k4WeroOl0hj+F2nUsotZ+OvIAAAAAElFTkSuQmCC"
237
- )
238
-
239
-
240
- def _load_embedded_icon() -> Image.Image:
241
- """Decode and return the embedded Talks Reducer tray icon."""
242
-
243
- data = base64.b64decode(_EMBEDDED_ICON_BASE64)
244
- with Image.open(BytesIO(data)) as image:
245
- return image.copy()
246
-
247
-
248
111
  def _load_icon() -> Image.Image:
249
- """Load the tray icon image, falling back to the embedded pen artwork."""
112
+ """Load the tray icon image, falling back to a generated placeholder."""
250
113
 
251
- LOGGER.debug("Attempting to load tray icon image.")
114
+ LOGGER.info("Attempting to load tray icon image.")
252
115
 
253
116
  for candidate in _iter_icon_candidates():
254
- LOGGER.debug("Checking icon candidate at %s", candidate)
255
- if candidate.exists():
256
- try:
257
- with Image.open(candidate) as image:
258
- loaded = image.copy()
259
- except Exception as exc: # pragma: no cover - diagnostic log
260
- LOGGER.warning("Failed to load tray icon from %s: %s", candidate, exc)
261
- else:
262
- LOGGER.debug("Loaded tray icon from %s", candidate)
263
- return loaded
264
-
265
- with suppress(FileNotFoundError):
266
- resource_icon = resources.files("talks_reducer") / "assets" / "icon.png"
267
- if resource_icon.is_file():
268
- LOGGER.debug("Loading tray icon from package resources")
269
- with resource_icon.open("rb") as handle:
270
- try:
271
- with Image.open(handle) as image:
272
- return image.copy()
273
- except Exception as exc: # pragma: no cover - diagnostic log
274
- LOGGER.warning(
275
- "Failed to load tray icon from package resources: %s", exc
276
- )
117
+ LOGGER.info("Checking icon candidate at %s", candidate)
118
+ if not candidate.exists():
119
+ continue
120
+ try:
121
+ with Image.open(candidate) as image:
122
+ loaded = image.copy()
123
+ except Exception as exc: # pragma: no cover - diagnostic log
124
+ LOGGER.warning("Failed to load tray icon from %s: %s", candidate, exc)
125
+ continue
126
+
127
+ LOGGER.info("Loaded tray icon from %s", candidate)
128
+ return loaded
277
129
 
278
130
  LOGGER.warning("Falling back to generated tray icon; packaged image not found")
279
- image = Image.new("RGBA", (64, 64), color=(37, 99, 235, 255))
280
- image.putpixel((0, 0), (255, 255, 255, 255))
281
- image.putpixel((63, 63), (17, 24, 39, 255))
282
- return image
131
+ return _generate_fallback_icon()
283
132
 
284
133
 
285
134
  class _ServerTrayApplication:
@@ -309,6 +158,7 @@ class _ServerTrayApplication:
309
158
  self._share_url: Optional[str] = None
310
159
  self._icon: Optional[pystray.Icon] = None
311
160
  self._gui_process: Optional[subprocess.Popen[Any]] = None
161
+ self._startup_error: Optional[BaseException] = None
312
162
 
313
163
  # Server lifecycle -------------------------------------------------
314
164
 
@@ -362,7 +212,7 @@ class _ServerTrayApplication:
362
212
  url = self._resolve_url()
363
213
  if url:
364
214
  webbrowser.open(url)
365
- LOGGER.debug("Opened browser to %s", url)
215
+ LOGGER.info("Opened browser to %s", url)
366
216
  else:
367
217
  LOGGER.warning("Server URL not yet available; please try again.")
368
218
 
@@ -383,7 +233,7 @@ class _ServerTrayApplication:
383
233
  try:
384
234
  process.wait()
385
235
  except Exception as exc: # pragma: no cover - best-effort cleanup
386
- LOGGER.debug("GUI process monitor exited with %s", exc)
236
+ LOGGER.info("GUI process monitor exited with %s", exc)
387
237
  finally:
388
238
  with self._gui_lock:
389
239
  if self._gui_process is process:
@@ -406,9 +256,7 @@ class _ServerTrayApplication:
406
256
 
407
257
  try:
408
258
  LOGGER.info("Launching Talks Reducer GUI via %s", sys.executable)
409
- process = subprocess.Popen(
410
- [sys.executable, "-m", "talks_reducer.gui"]
411
- )
259
+ process = subprocess.Popen([sys.executable, "-m", "talks_reducer.gui"])
412
260
  except Exception as exc: # pragma: no cover - platform specific
413
261
  LOGGER.error("Failed to launch Talks Reducer GUI: %s", exc)
414
262
  self._gui_process = None
@@ -435,26 +283,45 @@ class _ServerTrayApplication:
435
283
 
436
284
  # Public API -------------------------------------------------------
437
285
 
438
- def run(self) -> None:
439
- """Start the server and block until the tray icon exits."""
286
+ def _await_server_start(self, icon: Optional[pystray.Icon]) -> None:
287
+ """Wait for the server to signal readiness or trigger shutdown on failure."""
440
288
 
441
- server_thread = threading.Thread(
442
- target=self._launch_server, name="talks-reducer-server", daemon=True
289
+ if self._ready_event.wait(timeout=30):
290
+ if self._open_browser_on_start and not self._stop_event.is_set():
291
+ self._handle_open_webui()
292
+ return
293
+
294
+ if self._stop_event.is_set():
295
+ return
296
+
297
+ error = RuntimeError(
298
+ "Timed out while waiting for the Talks Reducer server to start."
443
299
  )
444
- server_thread.start()
300
+ self._startup_error = error
301
+ LOGGER.error("%s", error)
445
302
 
446
- if not self._ready_event.wait(timeout=30):
447
- raise RuntimeError(
448
- "Timed out while waiting for the Talks Reducer server to start."
449
- )
303
+ if icon is not None:
304
+ with suppress(Exception):
305
+ icon.notify("Talks Reducer server failed to start.")
450
306
 
451
- if self._open_browser_on_start:
452
- self._handle_open_webui()
307
+ self.stop()
308
+
309
+ def run(self) -> None:
310
+ """Start the server and block until the tray icon exits."""
311
+
312
+ self._startup_error = None
313
+
314
+ threading.Thread(
315
+ target=self._launch_server, name="talks-reducer-server", daemon=True
316
+ ).start()
453
317
 
454
318
  if self._tray_mode == "headless":
455
319
  LOGGER.warning(
456
320
  "Tray icon disabled (tray_mode=headless); press Ctrl+C to stop the server."
457
321
  )
322
+ self._await_server_start(None)
323
+ if self._startup_error is not None:
324
+ raise self._startup_error
458
325
  try:
459
326
  while not self._stop_event.wait(0.5):
460
327
  pass
@@ -484,6 +351,14 @@ class _ServerTrayApplication:
484
351
  menu=menu,
485
352
  )
486
353
 
354
+ watcher = threading.Thread(
355
+ target=self._await_server_start,
356
+ args=(self._icon,),
357
+ name="talks-reducer-server-watcher",
358
+ daemon=True,
359
+ )
360
+ watcher.start()
361
+
487
362
  if self._tray_mode == "pystray-detached":
488
363
  LOGGER.info("Running tray icon in detached mode")
489
364
  self._icon.run_detached()
@@ -492,10 +367,14 @@ class _ServerTrayApplication:
492
367
  pass
493
368
  finally:
494
369
  self.stop()
370
+ if self._startup_error is not None:
371
+ raise self._startup_error
495
372
  return
496
373
 
497
374
  LOGGER.info("Running tray icon in blocking mode")
498
375
  self._icon.run()
376
+ if self._startup_error is not None:
377
+ raise self._startup_error
499
378
 
500
379
  def stop(self) -> None:
501
380
  """Stop the tray icon and shut down the Gradio server."""
@@ -535,7 +414,7 @@ class _ServerTrayApplication:
535
414
  process.kill()
536
415
  process.wait(timeout=5)
537
416
  except Exception as exc: # pragma: no cover - defensive cleanup
538
- LOGGER.debug("Error while terminating GUI process: %s", exc)
417
+ LOGGER.info("Error while terminating GUI process: %s", exc)
539
418
 
540
419
  self._gui_process = None
541
420
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: talks-reducer
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: CLI for speeding up long-form talks by removing silence
5
5
  Author: Talks Reducer Maintainers
6
6
  License-Expression: MIT
@@ -174,6 +174,28 @@ The helper wraps the Gradio API exposed by `server.py`, waits for processing to
174
174
  path you provide. Pass `--small` to mirror the **Small video** checkbox or `--print-log` to stream the server log after the
175
175
  download finishes.
176
176
 
177
+ ## Faster PyInstaller builds
178
+
179
+ PyInstaller spends most of its time walking imports. To keep GUI builds snappy:
180
+
181
+ - Create a dedicated virtual environment for packaging the GUI and install only
182
+ the runtime dependencies you need (for example `pip install -r
183
+ requirements.txt -r scripts/requirements-pyinstaller.txt`). Avoid installing
184
+ heavy ML stacks such as Torch or TensorFlow in that environment so PyInstaller
185
+ never attempts to analyze them.
186
+ - Use the committed `talks-reducer.spec` file via `./scripts/build-gui.sh`.
187
+ The spec excludes Torch, TensorFlow, TensorBoard, torchvision/torchaudio,
188
+ Pandas, Qt bindings, setuptools' vendored helpers, and other bulky modules
189
+ that previously slowed the analysis stage. Set
190
+ `PYINSTALLER_EXTRA_EXCLUDES=module1,module2` if you need to drop additional
191
+ imports for an experimental build.
192
+ - Keep optional imports in the codebase lazy (wrapped in `try/except` or moved
193
+ inside functions) so the analyzer only sees the dependencies required for the
194
+ shipping GUI.
195
+
196
+ The script keeps incremental build artifacts in `build/` between runs. Pass
197
+ `--clean` to `scripts/build-gui.sh` when you want a full rebuild.
198
+
177
199
  ## Contributing
178
200
  See `CONTRIBUTION.md` for development setup details and guidance on sharing improvements.
179
201
 
@@ -1,4 +1,4 @@
1
- talks_reducer/__about__.py,sha256=wwxwKZDkQSt_KMTilch61vXb8z9MEha3gfTpYfebr70,92
1
+ talks_reducer/__about__.py,sha256=xjrdvQModGBv_84s8dFgz5jQVuohfdJ0TqtjSXpya4A,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
@@ -6,24 +6,25 @@ talks_reducer/chunks.py,sha256=IpdZxRFPURSG5wP-OQ_p09CVP8wcKwIFysV29zOTSWI,2959
6
6
  talks_reducer/cli.py,sha256=MeL5feATcJc0bGPDuW_L3HgepKQUi335zs3HVg47WrE,16474
7
7
  talks_reducer/discovery.py,sha256=BJ-iMir65cJMs0u-_EYdknBQT_grvCZaJNOx1xGi2PU,4590
8
8
  talks_reducer/ffmpeg.py,sha256=dsHBOBcr5XCSg0q3xmzLOcibBiEdyrXdEQa-ze5vQsM,12551
9
+ talks_reducer/icons.py,sha256=Htkef_iyqVkQQ4R3Kh1Wfbkiq3-3aJosA_nTWlY0vDA,3769
9
10
  talks_reducer/models.py,sha256=a1cHCVTNTJYh9I437CuANiaz5R_s-uECeGyK7WB67HQ,2018
10
11
  talks_reducer/pipeline.py,sha256=OGZG_3G1fh6LFQw9NuhnLq7gwJ5YcJ6l76QNWJydD7c,13630
11
12
  talks_reducer/progress.py,sha256=Mh43M6VWhjjUv9CI22xfD2EJ_7Aq3PCueqefQ9Bd5-o,4565
12
- talks_reducer/server.py,sha256=CLcgEyfBjsglSG1VkqiaP-4NsZZklL5o7ayYwnNMqbs,15782
13
- talks_reducer/server_tray.py,sha256=GBjx7Fr18Uy5O38ZjM5VXR77ou2_AEUqx2wN8MOZuss,23380
13
+ talks_reducer/server.py,sha256=sgnUr0tDHXf1dZuYbu3Kkx0LXegHAn5GD2vP6KoRDFI,15732
14
+ talks_reducer/server_tray.py,sha256=Gy8RXhYm0v_Uu9rHTq8K7EJFfZJ3TW-aIwUbXfXqreA,16087
14
15
  talks_reducer/service_client.py,sha256=8C2v2aNj8UAECfy1pw7oIzCK3Ktx5E6kZoNSYWH-8m8,12656
15
16
  talks_reducer/version_utils.py,sha256=TkYrTznVb2JqxFXzVzPd6PEnYP2MH7dxKl1J4-3DjMA,755
16
- talks_reducer/gui/__init__.py,sha256=UQJtyb87wwZyvauPo0mM_aiau9NAhKbl4ggwJoPCNC0,59870
17
+ talks_reducer/gui/__init__.py,sha256=gL_epCXuf6c5yOdWwT9kmvmYta7rq5CbDyJBwFFwKwo,59872
17
18
  talks_reducer/gui/__main__.py,sha256=9YWkGopLypanfMMq_RoQjjpPScTOxA7-biqMhQq-SSM,140
18
19
  talks_reducer/gui/discovery.py,sha256=6AXPcFGXqHZNhSBE1O5PyoH_CEMCb0Jk-9JGFwyAuRk,4108
19
- talks_reducer/gui/layout.py,sha256=rFzNt78sf6TQzHkEBUmINdD5-iJAWkBKHkIo_v5f7iU,17146
20
+ talks_reducer/gui/layout.py,sha256=ev6AbHOxxowhmZq8ZhT1C2-d88SzCMSdjBNkp8QgM7Q,16739
20
21
  talks_reducer/gui/preferences.py,sha256=ahDLPNIzvL71sw8WvgY9-TV_kaWTl_JTkn1gf2Z1EaA,3531
21
22
  talks_reducer/gui/remote.py,sha256=92HebrIo009GgRD7RBriw9yR8sbYHocsPzmjPe4ybhA,12071
22
23
  talks_reducer/gui/theme.py,sha256=ueqpPVPOwnLkeHlOlWkUcdcoClJrAqz9LWT79p33Xic,7718
23
24
  talks_reducer/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- talks_reducer-0.7.1.dist-info/licenses/LICENSE,sha256=jN17mHNR3e84awmH3AbpWBcBDBzPxEH0rcOFoj1s7sQ,1124
25
- talks_reducer-0.7.1.dist-info/METADATA,sha256=tiabxtCJb9UUqczxKpW0j27BWPWmfMfBAmb2n6jRt_U,8810
26
- talks_reducer-0.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- talks_reducer-0.7.1.dist-info/entry_points.txt,sha256=X2pjoh2vWBXXExVWorv1mbA1aTEVP3fyuZH4AixqZK4,208
28
- talks_reducer-0.7.1.dist-info/top_level.txt,sha256=pJWGcy__LR9JIEKH3QJyFmk9XrIsiFtqvuMNxFdIzDU,14
29
- talks_reducer-0.7.1.dist-info/RECORD,,
25
+ talks_reducer-0.7.2.dist-info/licenses/LICENSE,sha256=jN17mHNR3e84awmH3AbpWBcBDBzPxEH0rcOFoj1s7sQ,1124
26
+ talks_reducer-0.7.2.dist-info/METADATA,sha256=BJnDrkCMyXiNm_ByyjbyQOE4J2Bbbzl29qJfvbiy5fA,9994
27
+ talks_reducer-0.7.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ talks_reducer-0.7.2.dist-info/entry_points.txt,sha256=X2pjoh2vWBXXExVWorv1mbA1aTEVP3fyuZH4AixqZK4,208
29
+ talks_reducer-0.7.2.dist-info/top_level.txt,sha256=pJWGcy__LR9JIEKH3QJyFmk9XrIsiFtqvuMNxFdIzDU,14
30
+ talks_reducer-0.7.2.dist-info/RECORD,,