talks-reducer 0.2.18__py3-none-any.whl → 0.2.21__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/audio.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import math
6
6
  import subprocess
7
+ import sys
7
8
  from typing import List, Sequence, Tuple
8
9
 
9
10
  import numpy as np
@@ -35,7 +36,19 @@ def is_valid_input_file(filename: str) -> bool:
35
36
  "-show_entries",
36
37
  "stream=codec_type",
37
38
  ]
38
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
39
+
40
+ # Hide console window on Windows
41
+ creationflags = 0
42
+ if sys.platform == "win32":
43
+ # CREATE_NO_WINDOW = 0x08000000
44
+ creationflags = 0x08000000
45
+
46
+ process = subprocess.Popen(
47
+ command,
48
+ stdout=subprocess.PIPE,
49
+ stderr=subprocess.PIPE,
50
+ creationflags=creationflags
51
+ )
39
52
  outs, errs = None, None
40
53
  try:
41
54
  outs, errs = process.communicate(timeout=1)
talks_reducer/cli.py CHANGED
@@ -113,14 +113,22 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
113
113
 
114
114
  # Launch GUI if no arguments provided
115
115
  if not argv_list:
116
+ gui_launched = False
117
+
116
118
  try:
117
119
  from .gui import main as gui_main
118
120
 
119
- gui_main()
120
- return
121
+ gui_launched = gui_main([])
121
122
  except ImportError:
122
123
  # GUI dependencies not available, show help instead
123
- pass
124
+ gui_launched = False
125
+
126
+ if gui_launched:
127
+ return
128
+
129
+ parser = _build_parser()
130
+ parser.print_help()
131
+ return
124
132
 
125
133
  parser = _build_parser()
126
134
  parsed_args = parser.parse_args(argv)
talks_reducer/ffmpeg.py CHANGED
@@ -35,6 +35,18 @@ def find_ffmpeg() -> Optional[str]:
35
35
  else env_override
36
36
  )
37
37
 
38
+ # Try bundled ffmpeg from imageio-ffmpeg first
39
+ try:
40
+ import imageio_ffmpeg
41
+ bundled_path = imageio_ffmpeg.get_ffmpeg_exe()
42
+ if bundled_path and os.path.isfile(bundled_path):
43
+ return bundled_path
44
+ except ImportError:
45
+ pass
46
+ except Exception:
47
+ # If imageio_ffmpeg is installed but fails, continue to other methods
48
+ pass
49
+
38
50
  common_paths = [
39
51
  "C:\\ProgramData\\chocolatey\\bin\\ffmpeg.exe",
40
52
  "C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe",
@@ -95,7 +107,8 @@ def _resolve_ffmpeg_path() -> str:
95
107
  ffmpeg_path = find_ffmpeg()
96
108
  if not ffmpeg_path:
97
109
  raise FFmpegNotFoundError(
98
- "FFmpeg not found. Install FFmpeg and add it to PATH or provide TALKS_REDUCER_FFMPEG."
110
+ "FFmpeg not found. Please install imageio-ffmpeg (pip install imageio-ffmpeg) "
111
+ "or install FFmpeg manually and add it to PATH, or set TALKS_REDUCER_FFMPEG environment variable."
99
112
  )
100
113
 
101
114
  print(f"Using FFmpeg at: {ffmpeg_path}")
@@ -139,10 +152,20 @@ def get_ffprobe_path() -> str:
139
152
  def check_cuda_available(ffmpeg_path: Optional[str] = None) -> bool:
140
153
  """Return whether CUDA hardware encoders are available in the FFmpeg build."""
141
154
 
155
+ # Hide console window on Windows
156
+ creationflags = 0
157
+ if sys.platform == "win32":
158
+ # CREATE_NO_WINDOW = 0x08000000
159
+ creationflags = 0x08000000
160
+
142
161
  try:
143
162
  ffmpeg_path = ffmpeg_path or get_ffmpeg_path()
144
163
  result = subprocess.run(
145
- [ffmpeg_path, "-encoders"], capture_output=True, text=True, timeout=5
164
+ [ffmpeg_path, "-encoders"],
165
+ capture_output=True,
166
+ text=True,
167
+ timeout=5,
168
+ creationflags=creationflags
146
169
  )
147
170
  except (
148
171
  subprocess.TimeoutExpired,
@@ -178,6 +201,12 @@ def run_timed_ffmpeg_command(
178
201
  print(f"Error parsing command: {exc}", file=sys.stderr)
179
202
  raise
180
203
 
204
+ # Hide console window on Windows
205
+ creationflags = 0
206
+ if sys.platform == "win32":
207
+ # CREATE_NO_WINDOW = 0x08000000
208
+ creationflags = 0x08000000
209
+
181
210
  try:
182
211
  process = subprocess.Popen(
183
212
  args,
@@ -186,6 +215,7 @@ def run_timed_ffmpeg_command(
186
215
  universal_newlines=True,
187
216
  bufsize=1,
188
217
  errors="replace",
218
+ creationflags=creationflags,
189
219
  )
190
220
  except Exception as exc: # pragma: no cover - defensive logging
191
221
  print(f"Error starting FFmpeg: {exc}", file=sys.stderr)
talks_reducer/gui.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import os
6
7
  import subprocess
7
8
  import sys
@@ -39,29 +40,70 @@ except ImportError: # pragma: no cover - handled at runtime
39
40
  def _check_tkinter_available() -> tuple[bool, str]:
40
41
  """Check if tkinter can create windows without importing it globally."""
41
42
  # Test in a subprocess to avoid crashing the main process
42
- test_code = """import tkinter as tk
43
- try:
44
- root = tk.Tk()
45
- root.destroy()
46
- print("SUCCESS")
47
- except Exception as e:
48
- print(f"ERROR: {e}")"""
43
+ test_code = """
44
+ import json
45
+
46
+ def run_check():
47
+ try:
48
+ import tkinter as tk # noqa: F401 - imported for side effect
49
+ except Exception as exc: # pragma: no cover - runs in subprocess
50
+ return {
51
+ "status": "import_error",
52
+ "error": f"{exc.__class__.__name__}: {exc}",
53
+ }
54
+
55
+ try:
56
+ import tkinter as tk
57
+
58
+ root = tk.Tk()
59
+ root.destroy()
60
+ except Exception as exc: # pragma: no cover - runs in subprocess
61
+ return {
62
+ "status": "init_error",
63
+ "error": f"{exc.__class__.__name__}: {exc}",
64
+ }
65
+
66
+ return {"status": "ok"}
67
+
68
+
69
+ if __name__ == "__main__":
70
+ print(json.dumps(run_check()))
71
+ """
49
72
 
50
73
  try:
51
74
  result = subprocess.run(
52
75
  [sys.executable, "-c", test_code], capture_output=True, text=True, timeout=5
53
76
  )
54
77
 
55
- if result.returncode == 0 and "SUCCESS" in result.stdout:
78
+ output = result.stdout.strip() or result.stderr.strip()
79
+
80
+ if not output:
81
+ return False, "Window creation failed"
82
+
83
+ try:
84
+ payload = json.loads(output)
85
+ except json.JSONDecodeError:
86
+ return False, output
87
+
88
+ status = payload.get("status")
89
+
90
+ if status == "ok":
56
91
  return True, ""
57
- else:
58
- error_output = (
59
- result.stdout.strip()
60
- or result.stderr.strip()
61
- or "Window creation failed"
92
+
93
+ if status == "import_error":
94
+ return (
95
+ False,
96
+ f"tkinter is not installed ({payload.get('error', 'unknown error')})",
62
97
  )
63
- return False, error_output
64
- except Exception as e:
98
+
99
+ if status == "init_error":
100
+ return (
101
+ False,
102
+ f"tkinter could not open a window ({payload.get('error', 'unknown error')})",
103
+ )
104
+
105
+ return False, output
106
+ except Exception as e: # pragma: no cover - defensive fallback
65
107
  return False, f"Error testing tkinter: {e}"
66
108
 
67
109
 
@@ -213,17 +255,30 @@ class TalksReducerGUI:
213
255
  def _apply_window_icon(self) -> None:
214
256
  """Configure the application icon when the asset is available."""
215
257
 
216
- icon_path = (
217
- Path(__file__).resolve().parent.parent / "docs" / "assets" / "icon.png"
258
+ base_path = Path(
259
+ getattr(sys, "_MEIPASS", Path(__file__).resolve().parent.parent)
218
260
  )
219
- if not icon_path.is_file():
220
- return
221
261
 
222
- try:
223
- self.root.iconphoto(False, self.tk.PhotoImage(file=str(icon_path)))
224
- except self.tk.TclError:
225
- # Missing Tk image support (e.g. headless environments) should not crash the GUI.
226
- pass
262
+ icon_candidates: list[tuple[Path, str]] = []
263
+ if sys.platform.startswith("win"):
264
+ icon_candidates.append((base_path / "docs" / "assets" / "icon.ico", "ico"))
265
+ icon_candidates.append((base_path / "docs" / "assets" / "icon.png", "png"))
266
+
267
+ for icon_path, icon_type in icon_candidates:
268
+ if not icon_path.is_file():
269
+ continue
270
+
271
+ try:
272
+ if icon_type == "ico" and sys.platform.startswith("win"):
273
+ # On Windows, iconbitmap works better without the 'default' parameter
274
+ self.root.iconbitmap(str(icon_path))
275
+ else:
276
+ self.root.iconphoto(False, self.tk.PhotoImage(file=str(icon_path)))
277
+ # If we got here without exception, icon was set successfully
278
+ return
279
+ except (self.tk.TclError, Exception) as e:
280
+ # Missing Tk image support or invalid icon format - try next candidate
281
+ continue
227
282
 
228
283
  def _build_layout(self) -> None:
229
284
  main = self.ttk.Frame(self.root, padding=16)
@@ -621,7 +676,7 @@ class TalksReducerGUI:
621
676
 
622
677
  # -------------------------------------------------------------- actions --
623
678
  def _add_files(self) -> None:
624
- files = filedialog.askopenfilenames(
679
+ files = self.filedialog.askopenfilenames(
625
680
  title="Select input files",
626
681
  filetypes=[
627
682
  ("Video files", "*.mp4 *.mkv *.mov *.avi *.m4v"),
@@ -631,7 +686,7 @@ class TalksReducerGUI:
631
686
  self._extend_inputs(files)
632
687
 
633
688
  def _add_directory(self) -> None:
634
- directory = filedialog.askdirectory(title="Select input folder")
689
+ directory = self.filedialog.askdirectory(title="Select input folder")
635
690
  if directory:
636
691
  self._extend_inputs([directory])
637
692
 
@@ -659,22 +714,26 @@ class TalksReducerGUI:
659
714
  cleaned = [path.strip("{}") for path in paths]
660
715
  self._extend_inputs(cleaned, auto_run=True)
661
716
 
662
- def _browse_path(self, variable, label: str) -> None: # type: (tk.StringVar, str) -> None
717
+ def _browse_path(
718
+ self, variable, label: str
719
+ ) -> None: # type: (tk.StringVar, str) -> None
663
720
  if "folder" in label.lower():
664
- result = filedialog.askdirectory()
721
+ result = self.filedialog.askdirectory()
665
722
  else:
666
723
  initial = variable.get() or os.getcwd()
667
- result = filedialog.asksaveasfilename(initialfile=os.path.basename(initial))
724
+ result = self.filedialog.asksaveasfilename(
725
+ initialfile=os.path.basename(initial)
726
+ )
668
727
  if result:
669
728
  variable.set(result)
670
729
 
671
730
  def _start_run(self) -> None:
672
731
  if self._processing_thread and self._processing_thread.is_alive():
673
- messagebox.showinfo("Processing", "A job is already running.")
732
+ self.messagebox.showinfo("Processing", "A job is already running.")
674
733
  return
675
734
 
676
735
  if not self.input_files:
677
- messagebox.showwarning(
736
+ self.messagebox.showwarning(
678
737
  "Missing input", "Please add at least one file or folder."
679
738
  )
680
739
  return
@@ -682,11 +741,11 @@ class TalksReducerGUI:
682
741
  try:
683
742
  args = self._collect_arguments()
684
743
  except ValueError as exc:
685
- messagebox.showerror("Invalid value", str(exc))
744
+ self.messagebox.showerror("Invalid value", str(exc))
686
745
  return
687
746
 
688
747
  self._append_log("Starting processing…")
689
- self.run_button.configure(state=tk.DISABLED)
748
+ self.run_button.configure(state=self.tk.DISABLED)
690
749
 
691
750
  def worker() -> None:
692
751
  reporter = _TkProgressReporter(self._append_log)
@@ -694,7 +753,7 @@ class TalksReducerGUI:
694
753
  files = gather_input_files(self.input_files)
695
754
  if not files:
696
755
  self._notify(
697
- lambda: messagebox.showwarning(
756
+ lambda: self.messagebox.showwarning(
698
757
  "No files", "No supported media files were found."
699
758
  )
700
759
  )
@@ -716,11 +775,15 @@ class TalksReducerGUI:
716
775
  self._append_log("All jobs finished successfully.")
717
776
  self._notify(lambda: self.open_button.configure(state=self.tk.NORMAL))
718
777
  except FFmpegNotFoundError as exc:
719
- self._notify(lambda: messagebox.showerror("FFmpeg not found", str(exc)))
778
+ self._notify(
779
+ lambda: self.messagebox.showerror("FFmpeg not found", str(exc))
780
+ )
720
781
  self._set_status("Error")
721
782
  except Exception as exc: # pragma: no cover - GUI level safeguard
722
783
  self._notify(
723
- lambda: messagebox.showerror("Error", f"Processing failed: {exc}")
784
+ lambda: self.messagebox.showerror(
785
+ "Error", f"Processing failed: {exc}"
786
+ )
724
787
  )
725
788
  self._set_status("Error")
726
789
  finally:
@@ -883,20 +946,24 @@ class TalksReducerGUI:
883
946
  self.root.mainloop()
884
947
 
885
948
 
886
- def main(argv: Optional[Sequence[str]] = None) -> None:
887
- """Launch the GUI when run without arguments, otherwise defer to the CLI."""
949
+ def main(argv: Optional[Sequence[str]] = None) -> bool:
950
+ """Launch the GUI when run without arguments, otherwise defer to the CLI.
951
+
952
+ Returns ``True`` if the GUI event loop started successfully. ``False``
953
+ indicates that execution was delegated to the CLI or aborted early.
954
+ """
888
955
 
889
956
  if argv is None:
890
957
  argv = sys.argv[1:]
891
958
 
892
959
  if argv:
893
960
  cli_main(argv)
894
- return
961
+ return False
895
962
 
896
963
  # Skip tkinter check if running as a PyInstaller frozen app
897
964
  # In that case, tkinter is bundled and the subprocess check would fail
898
- is_frozen = getattr(sys, 'frozen', False)
899
-
965
+ is_frozen = getattr(sys, "frozen", False)
966
+
900
967
  if not is_frozen:
901
968
  # Check if tkinter is available before creating GUI (only when not frozen)
902
969
  tkinter_available, error_msg = _check_tkinter_available()
@@ -926,14 +993,16 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
926
993
  except UnicodeEncodeError:
927
994
  # Fallback for extreme encoding issues
928
995
  sys.stderr.write("GUI not available. Use CLI mode instead.\n")
929
- return
996
+ return False
930
997
 
931
998
  # Catch and report any errors during GUI initialization
932
999
  try:
933
1000
  app = TalksReducerGUI()
934
1001
  app.run()
1002
+ return True
935
1003
  except Exception as e:
936
1004
  import traceback
1005
+
937
1006
  sys.stderr.write(f"Error starting GUI: {e}\n")
938
1007
  sys.stderr.write(traceback.format_exc())
939
1008
  sys.stderr.write("\nPlease use the CLI mode instead:\n")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: talks-reducer
3
- Version: 0.2.18
3
+ Version: 0.2.21
4
4
  Summary: CLI for speeding up long-form talks by removing silence
5
5
  Author: Talks Reducer Maintainers
6
6
  License-Expression: MIT
@@ -13,6 +13,7 @@ Requires-Dist: numpy<2.0.0,>=1.22.0
13
13
  Requires-Dist: tqdm>=4.65.0
14
14
  Requires-Dist: tkinterdnd2>=0.3.0
15
15
  Requires-Dist: Pillow>=9.0.0
16
+ Requires-Dist: imageio-ffmpeg>=0.4.8
16
17
  Provides-Extra: dev
17
18
  Requires-Dist: build>=1.0.0; extra == "dev"
18
19
  Requires-Dist: twine>=4.0.0; extra == "dev"
@@ -44,6 +45,8 @@ Go to the [releases page](https://github.com/popstas/talks-reducer/releases) and
44
45
  pip install talks-reducer
45
46
  ```
46
47
 
48
+ **Note:** FFmpeg is now bundled automatically with the package, so you don't need to install it separately. However, if you have FFmpeg already installed on your system, it will be used instead of the bundled version.
49
+
47
50
  The `--small` preset applies a 720p video scale and 128 kbps audio bitrate, making it useful for sharing talks over constrained
48
51
  connections. Without `--small`, the script aims to preserve original quality while removing silence.
49
52
 
@@ -122,6 +125,19 @@ file manager.
122
125
  - **October 2025** — Added `--small` preset with 720p/128 kbps defaults for bandwidth-friendly exports.
123
126
  - **October 2025** — Removed the `--cuda` flag; CUDA/NVENC support is now auto-detected.
124
127
 
128
+ ## Changelog
129
+ Major and minor releases are tracked in `CHANGELOG.md`. The log is generated from
130
+ Conventional Commits that start with either `feat:` or `fix:`. Only tags in the
131
+ form `v<major>.<minor>.0` are included so patch releases (for example,
132
+ `v1.1.1`) are omitted. Regenerate the file whenever you cut a release:
133
+
134
+ ```bash
135
+ python scripts/generate_changelog.py
136
+ ```
137
+
138
+ CI will fail if the generated changelog does not match the committed version, so
139
+ run the script before opening a pull request that updates release tags.
140
+
125
141
  ## Contributing
126
142
  See `CONTRIBUTION.md` for development setup details and guidance on sharing improvements.
127
143
 
@@ -0,0 +1,16 @@
1
+ talks_reducer/__init__.py,sha256=lb50C4_o_SLERyMyVpQfgHnXf49FJOIF9j05MZ8KAvM,158
2
+ talks_reducer/__main__.py,sha256=azR_vh8HFPLaOnh-L6gUFWsL67I6iHtbeH5rQhsipGY,299
3
+ talks_reducer/audio.py,sha256=we6-lyVjCJSFDEFUTPhtpiUmT2MZdPMpBrYN2ED_T9E,4440
4
+ talks_reducer/chunks.py,sha256=IpdZxRFPURSG5wP-OQ_p09CVP8wcKwIFysV29zOTSWI,2959
5
+ talks_reducer/cli.py,sha256=7zzH-HvdXtjRRKHnGLlNRrNQ3ldKgOe0mm6Ft13J_Ng,6191
6
+ talks_reducer/ffmpeg.py,sha256=GzHD1gnC7D3mgQJYZhz0waudDs2x-biI4x7ThrFMP50,11318
7
+ talks_reducer/gui.py,sha256=_J6Xh9ap5aHJ9LSiTotMbTg3_mN28LnJvbDsdrdSKlo,36759
8
+ talks_reducer/models.py,sha256=6cZRcJf0EBZIzNd-PWrh4Wdsoa4EBj5nSdB6BnFiOXM,1106
9
+ talks_reducer/pipeline.py,sha256=zcrN8VaWXPc1InY_mXX4yDrQ9goST3G5uBYKkEfJQu4,8623
10
+ talks_reducer/progress.py,sha256=Mh43M6VWhjjUv9CI22xfD2EJ_7Aq3PCueqefQ9Bd5-o,4565
11
+ talks_reducer-0.2.21.dist-info/licenses/LICENSE,sha256=jN17mHNR3e84awmH3AbpWBcBDBzPxEH0rcOFoj1s7sQ,1124
12
+ talks_reducer-0.2.21.dist-info/METADATA,sha256=OxSTzVSC0xXTCVmMxuEaD-IuM0Eqrii3ZcndcBFCN8E,7313
13
+ talks_reducer-0.2.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ talks_reducer-0.2.21.dist-info/entry_points.txt,sha256=LCzfSnh_7VXhvl9twoFSAj0C3sG7bayWs2LkxpH7hoI,100
15
+ talks_reducer-0.2.21.dist-info/top_level.txt,sha256=pJWGcy__LR9JIEKH3QJyFmk9XrIsiFtqvuMNxFdIzDU,14
16
+ talks_reducer-0.2.21.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- talks_reducer/__init__.py,sha256=lb50C4_o_SLERyMyVpQfgHnXf49FJOIF9j05MZ8KAvM,158
2
- talks_reducer/__main__.py,sha256=azR_vh8HFPLaOnh-L6gUFWsL67I6iHtbeH5rQhsipGY,299
3
- talks_reducer/audio.py,sha256=Sobugu0cwYEB9YYfCsgDkgwNyELSp06DPURdWnt9nrs,4184
4
- talks_reducer/chunks.py,sha256=IpdZxRFPURSG5wP-OQ_p09CVP8wcKwIFysV29zOTSWI,2959
5
- talks_reducer/cli.py,sha256=Hop3mKrD47QOeTaTsifF9HV3CI7tHSQfQnFQLamzYWg,6025
6
- talks_reducer/ffmpeg.py,sha256=UfGBdAVQvlZJCNPjxpZPztp1i31uldhumVgVyv2fGh4,10373
7
- talks_reducer/gui.py,sha256=fo0KaSxjywhes-t1Puz69fAN5jbo6p2A2GRnS1R2w1k,34645
8
- talks_reducer/models.py,sha256=6cZRcJf0EBZIzNd-PWrh4Wdsoa4EBj5nSdB6BnFiOXM,1106
9
- talks_reducer/pipeline.py,sha256=zcrN8VaWXPc1InY_mXX4yDrQ9goST3G5uBYKkEfJQu4,8623
10
- talks_reducer/progress.py,sha256=Mh43M6VWhjjUv9CI22xfD2EJ_7Aq3PCueqefQ9Bd5-o,4565
11
- talks_reducer-0.2.18.dist-info/licenses/LICENSE,sha256=jN17mHNR3e84awmH3AbpWBcBDBzPxEH0rcOFoj1s7sQ,1124
12
- talks_reducer-0.2.18.dist-info/METADATA,sha256=jD_KQvDWfYtcFMb034gRPuK_w4awAT6BmEXh93K6SJk,6519
13
- talks_reducer-0.2.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- talks_reducer-0.2.18.dist-info/entry_points.txt,sha256=LCzfSnh_7VXhvl9twoFSAj0C3sG7bayWs2LkxpH7hoI,100
15
- talks_reducer-0.2.18.dist-info/top_level.txt,sha256=pJWGcy__LR9JIEKH3QJyFmk9XrIsiFtqvuMNxFdIzDU,14
16
- talks_reducer-0.2.18.dist-info/RECORD,,