talks-reducer 0.3.1__py3-none-any.whl → 0.3.3__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/cli.py CHANGED
@@ -6,6 +6,7 @@ import argparse
6
6
  import os
7
7
  import sys
8
8
  import time
9
+ from importlib import import_module
9
10
  from importlib.metadata import version
10
11
  from pathlib import Path
11
12
  from typing import Dict, List, Optional, Sequence
@@ -23,19 +24,19 @@ def _build_parser() -> argparse.ArgumentParser:
23
24
  parser = argparse.ArgumentParser(
24
25
  description="Modifies a video file to play at different speeds when there is sound vs. silence.",
25
26
  )
26
-
27
+
27
28
  # Add version argument
28
29
  try:
29
30
  pkg_version = version("talks-reducer")
30
31
  except Exception:
31
32
  pkg_version = "unknown"
32
-
33
+
33
34
  parser.add_argument(
34
35
  "--version",
35
36
  action="version",
36
37
  version=f"talks-reducer {pkg_version}",
37
38
  )
38
-
39
+
39
40
  parser.add_argument(
40
41
  "input_file",
41
42
  type=str,
@@ -113,31 +114,34 @@ def gather_input_files(paths: List[str]) -> List[str]:
113
114
  return files
114
115
 
115
116
 
117
+ def _launch_gui(argv: Sequence[str]) -> bool:
118
+ """Attempt to launch the GUI with the provided arguments."""
119
+
120
+ try:
121
+ gui_module = import_module(".gui", __package__)
122
+ except ImportError:
123
+ return False
124
+
125
+ gui_main = getattr(gui_module, "main", None)
126
+ if gui_main is None:
127
+ return False
128
+
129
+ return bool(gui_main(list(argv)))
130
+
131
+
116
132
  def main(argv: Optional[Sequence[str]] = None) -> None:
117
133
  """Entry point for the command line interface.
118
134
 
119
135
  Launch the GUI when run without arguments, otherwise defer to the CLI.
120
136
  """
121
137
 
122
- # Check if running without arguments
123
138
  if argv is None:
124
139
  argv_list = sys.argv[1:]
125
140
  else:
126
141
  argv_list = list(argv)
127
142
 
128
- # Launch GUI if no arguments provided
129
143
  if not argv_list:
130
- gui_launched = False
131
-
132
- try:
133
- from .gui import main as gui_main
134
-
135
- gui_launched = gui_main([])
136
- except ImportError:
137
- # GUI dependencies not available, show help instead
138
- gui_launched = False
139
-
140
- if gui_launched:
144
+ if _launch_gui(argv_list):
141
145
  return
142
146
 
143
147
  parser = _build_parser()
@@ -145,7 +149,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
145
149
  return
146
150
 
147
151
  parser = _build_parser()
148
- parsed_args = parser.parse_args(argv)
152
+ parsed_args = parser.parse_args(argv_list)
149
153
  start_time = time.time()
150
154
 
151
155
  files = gather_input_files(parsed_args.input_file)
@@ -192,6 +196,15 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
192
196
  sys.exit(1)
193
197
 
194
198
  reporter.log(f"Completed: {result.output_file}")
199
+ summary_parts = []
200
+ time_ratio = getattr(result, "time_ratio", None)
201
+ size_ratio = getattr(result, "size_ratio", None)
202
+ if time_ratio is not None:
203
+ summary_parts.append(f"{time_ratio * 100:.0f}% time")
204
+ if size_ratio is not None:
205
+ summary_parts.append(f"{size_ratio * 100:.0f}% size")
206
+ if summary_parts:
207
+ reporter.log("Result: " + ", ".join(summary_parts))
195
208
 
196
209
  end_time = time.time()
197
210
  total_time = end_time - start_time
talks_reducer/ffmpeg.py CHANGED
@@ -38,6 +38,7 @@ def find_ffmpeg() -> Optional[str]:
38
38
  # Try bundled ffmpeg from imageio-ffmpeg first
39
39
  try:
40
40
  import imageio_ffmpeg
41
+
41
42
  bundled_path = imageio_ffmpeg.get_ffmpeg_exe()
42
43
  if bundled_path and os.path.isfile(bundled_path):
43
44
  return bundled_path
@@ -161,11 +162,11 @@ def check_cuda_available(ffmpeg_path: Optional[str] = None) -> bool:
161
162
  try:
162
163
  ffmpeg_path = ffmpeg_path or get_ffmpeg_path()
163
164
  result = subprocess.run(
164
- [ffmpeg_path, "-encoders"],
165
- capture_output=True,
166
- text=True,
165
+ [ffmpeg_path, "-encoders"],
166
+ capture_output=True,
167
+ text=True,
167
168
  timeout=5,
168
- creationflags=creationflags
169
+ creationflags=creationflags,
169
170
  )
170
171
  except (
171
172
  subprocess.TimeoutExpired,
@@ -193,7 +194,7 @@ def run_timed_ffmpeg_command(
193
194
  process_callback: Optional[callable] = None,
194
195
  ) -> None:
195
196
  """Execute an FFmpeg command while streaming progress information.
196
-
197
+
197
198
  Args:
198
199
  process_callback: Optional callback that receives the subprocess.Popen object
199
200
  """
@@ -243,7 +244,7 @@ def run_timed_ffmpeg_command(
243
244
 
244
245
  sys.stderr.write(line)
245
246
  sys.stderr.flush()
246
-
247
+
247
248
  # Send FFmpeg output to reporter for GUI display
248
249
  progress_reporter.log(line.strip())
249
250
 
talks_reducer/gui.py CHANGED
@@ -291,6 +291,8 @@ class TalksReducerGUI:
291
291
 
292
292
  self._processing_thread: Optional[threading.Thread] = None
293
293
  self._last_output: Optional[Path] = None
294
+ self._last_time_ratio: Optional[float] = None
295
+ self._last_size_ratio: Optional[float] = None
294
296
  self._status_state = "Idle"
295
297
  self.status_var = tk.StringVar(value=self._status_state)
296
298
  self._status_animation_job: Optional[str] = None
@@ -445,7 +447,9 @@ class TalksReducerGUI:
445
447
  variable=self.simple_mode_var,
446
448
  command=self._toggle_simple_mode,
447
449
  )
448
- self.simple_mode_check.grid(row=0, column=2, sticky="w", padx=(12, 0))
450
+ self.simple_mode_check.grid(
451
+ row=1, column=0, columnspan=3, sticky="w", pady=(8, 0)
452
+ )
449
453
 
450
454
  self.advanced_visible = self.tk.BooleanVar(value=False)
451
455
  self.advanced_button = self.ttk.Button(
@@ -980,7 +984,15 @@ class TalksReducerGUI:
980
984
  options = self._build_options(Path(file), args)
981
985
  result = speed_up_video(options, reporter=reporter)
982
986
  self._last_output = result.output_file
983
- self._append_log(f"Completed: {result.output_file}")
987
+ self._last_time_ratio = result.time_ratio
988
+ self._last_size_ratio = result.size_ratio
989
+
990
+ # Create completion message with ratios if available
991
+ completion_msg = f"Completed: {result.output_file}"
992
+ if result.time_ratio is not None and result.size_ratio is not None:
993
+ completion_msg += f" (Time: {result.time_ratio:.2%}, Size: {result.size_ratio:.2%})"
994
+
995
+ self._append_log(completion_msg)
984
996
  if open_after_convert:
985
997
  self._notify(
986
998
  lambda path=result.output_file: self._open_in_file_manager(
@@ -1135,17 +1147,22 @@ class TalksReducerGUI:
1135
1147
  def _update_status_from_message(self, message: str) -> None:
1136
1148
  normalized = message.strip().lower()
1137
1149
  if "all jobs finished successfully" in normalized:
1138
- self._set_status("Success")
1150
+ # Create status message with ratios if available
1151
+ status_msg = "Success"
1152
+ if self._last_time_ratio is not None and self._last_size_ratio is not None:
1153
+ status_msg = f"Time: {self._last_time_ratio:.0%}, Size: {self._last_size_ratio:.0%}"
1154
+
1155
+ self._set_status("success", status_msg)
1139
1156
  self._set_progress(100) # 100% on success
1140
1157
  self._video_duration_seconds = None # Reset for next video
1141
1158
  elif normalized.startswith("extracting audio"):
1142
- self._set_status("Extracting audio...")
1159
+ self._set_status("processing", "Extracting audio...")
1143
1160
  self._set_progress(0) # 0% on start
1144
1161
  self._video_duration_seconds = None # Reset for new processing
1145
1162
  elif normalized.startswith("starting processing") or normalized.startswith(
1146
1163
  "processing"
1147
1164
  ):
1148
- self._set_status("Processing")
1165
+ self._set_status("processing", "Processing")
1149
1166
  self._set_progress(0) # 0% on start
1150
1167
  self._video_duration_seconds = None # Reset for new processing
1151
1168
 
@@ -1174,10 +1191,10 @@ class TalksReducerGUI:
1174
1191
  percentage = min(
1175
1192
  100, int((current_seconds / self._video_duration_seconds) * 100)
1176
1193
  )
1177
- self._set_status(f"{time_str}, {speed_str}x ({percentage}%)")
1194
+ self._set_status("processing", f"{time_str}, {speed_str}x ({percentage}%)")
1178
1195
  self._set_progress(percentage) # Update progress bar
1179
1196
  else:
1180
- self._set_status(f"{time_str}, {speed_str}x")
1197
+ self._set_status("processing", f"{time_str}, {speed_str}x")
1181
1198
 
1182
1199
  def _apply_status_style(self, status: str) -> None:
1183
1200
  color = STATUS_COLORS.get(status.lower())
@@ -1185,40 +1202,46 @@ class TalksReducerGUI:
1185
1202
  self.status_label.configure(fg=color)
1186
1203
  else:
1187
1204
  # For extracting audio or FFmpeg progress messages, use processing color
1188
- if "extracting audio" in status.lower() or re.search(
1189
- r"\d{2}:\d{2}:\d{2}.*\d+\.?\d*x", status
1190
- ):
1191
- self.status_label.configure(fg=STATUS_COLORS["processing"])
1205
+ # Also handle the new "Time: X%, Size: Y%" format as success
1206
+ status_lower = status.lower()
1207
+ if ("extracting audio" in status_lower or
1208
+ re.search(r"\d{2}:\d{2}:\d{2}.*\d+\.?\d*x", status) or
1209
+ ("time:" in status_lower and "size:" in status_lower)):
1210
+ if "time:" in status_lower and "size:" in status_lower:
1211
+ # This is our new success format with ratios
1212
+ self.status_label.configure(fg=STATUS_COLORS["success"])
1213
+ else:
1214
+ self.status_label.configure(fg=STATUS_COLORS["processing"])
1192
1215
 
1193
- def _set_status(self, status: str) -> None:
1216
+ def _set_status(self, status: str, status_msg: str = "") -> None:
1194
1217
  def apply() -> None:
1195
- self._stop_status_animation()
1196
1218
  self._status_state = status
1197
- self.status_var.set(status)
1198
- self._apply_status_style(status)
1219
+ # Use status_msg if provided, otherwise use status
1220
+ display_text = status_msg if status_msg else status
1221
+ self.status_var.set(display_text)
1222
+ self._apply_status_style(status) # Colors depend on status, not display text
1199
1223
  self._set_progress_bar_style(status)
1200
1224
  lowered = status.lower()
1201
1225
  is_processing = lowered == "processing" or "extracting audio" in lowered
1202
1226
 
1203
1227
  if is_processing:
1204
- self._start_status_animation()
1205
1228
  # Show stop button during processing
1206
1229
  if hasattr(self, "status_frame"):
1207
1230
  self.status_frame.grid()
1208
1231
  self.stop_button.grid()
1209
1232
  self.drop_hint_button.grid_remove()
1210
1233
 
1211
- if lowered == "success":
1234
+ if lowered == "success" or "time:" in lowered and "size:" in lowered:
1212
1235
  if self.simple_mode_var.get() and hasattr(self, "status_frame"):
1213
1236
  self.status_frame.grid()
1214
1237
  self.stop_button.grid_remove()
1215
1238
  self.drop_hint_button.grid_remove()
1216
1239
  self.open_button.grid()
1217
1240
  self.open_button.lift() # Ensure open_button is above drop_hint_button
1218
- print("success status")
1241
+ # print("success status")
1219
1242
  else:
1220
1243
  self.open_button.grid_remove()
1221
- print("not success status")
1244
+ # print("not success status")
1222
1245
  if (
1223
1246
  self.simple_mode_var.get()
1224
1247
  and not is_processing
@@ -1232,30 +1255,6 @@ class TalksReducerGUI:
1232
1255
 
1233
1256
  self.root.after(0, apply)
1234
1257
 
1235
- def _start_status_animation(self) -> None:
1236
- self._status_animation_phase = 0
1237
- self._schedule_status_animation()
1238
-
1239
- def _schedule_status_animation(self) -> None:
1240
- if self._status_state.lower() != "processing":
1241
- return
1242
-
1243
- dots = self._status_animation_phase % 4
1244
- suffix = "." * dots
1245
- text = "Processing" + suffix
1246
- self.status_var.set(text)
1247
- self._status_animation_phase = (self._status_animation_phase + 1) % 4
1248
- self._status_animation_job = self.root.after(
1249
- 400, self._schedule_status_animation
1250
- )
1251
-
1252
- def _stop_status_animation(self) -> None:
1253
- if self._status_animation_job is not None:
1254
- self.root.after_cancel(self._status_animation_job)
1255
- self._status_animation_job = None
1256
- if self._status_state.lower() != "processing":
1257
- self.status_var.set(self._status_state)
1258
-
1259
1258
  def _calculate_gradient_color(self, percentage: int, darken: float = 1.0) -> str:
1260
1259
  """Calculate color gradient from red (0%) to green (100%).
1261
1260
 
@@ -1334,7 +1333,7 @@ class TalksReducerGUI:
1334
1333
  def updater() -> None:
1335
1334
  # Map status to progress bar style
1336
1335
  status_lower = status.lower()
1337
- if status_lower == "success":
1336
+ if status_lower == "success" or ("time:" in status_lower and "size:" in status_lower):
1338
1337
  style = "Success.Horizontal.TProgressbar"
1339
1338
  elif status_lower == "error":
1340
1339
  style = "Error.Horizontal.TProgressbar"
talks_reducer/models.py CHANGED
@@ -36,6 +36,9 @@ class ProcessingResult:
36
36
  output_file: Path
37
37
  frame_rate: float
38
38
  original_duration: float
39
+ output_duration: float
39
40
  chunk_count: int
40
41
  used_cuda: bool
41
42
  max_audio_volume: float
43
+ time_ratio: Optional[float]
44
+ size_ratio: Optional[float]
talks_reducer/pipeline.py CHANGED
@@ -158,7 +158,7 @@ def speed_up_video(
158
158
  )
159
159
 
160
160
  reporter.log("Extracting audio...")
161
- process_callback = getattr(reporter, 'process_callback', None)
161
+ process_callback = getattr(reporter, "process_callback", None)
162
162
  run_timed_ffmpeg_command(
163
163
  extract_command,
164
164
  reporter=reporter,
@@ -270,12 +270,23 @@ def speed_up_video(
270
270
  finally:
271
271
  _delete_path(temp_path)
272
272
 
273
+ output_metadata = _extract_video_metadata(output_path, frame_rate)
274
+ output_duration = output_metadata.get("duration", 0.0)
275
+ time_ratio = output_duration / original_duration if original_duration > 0 else None
276
+
277
+ input_size = input_path.stat().st_size if input_path.exists() else 0
278
+ output_size = output_path.stat().st_size if output_path.exists() else 0
279
+ size_ratio = (output_size / input_size) if input_size > 0 else None
280
+
273
281
  return ProcessingResult(
274
282
  input_file=input_path,
275
283
  output_file=output_path,
276
284
  frame_rate=frame_rate,
277
285
  original_duration=original_duration,
286
+ output_duration=output_duration,
278
287
  chunk_count=len(chunks),
279
288
  used_cuda=use_cuda_encoder,
280
289
  max_audio_volume=max_audio_volume,
290
+ time_ratio=time_ratio,
291
+ size_ratio=size_ratio,
281
292
  )
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: talks-reducer
3
+ Version: 0.3.3
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: imageio-ffmpeg>=0.4.8
17
+ Provides-Extra: dev
18
+ Requires-Dist: build>=1.0.0; extra == "dev"
19
+ Requires-Dist: twine>=4.0.0; extra == "dev"
20
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
21
+ Requires-Dist: black>=23.0.0; extra == "dev"
22
+ Requires-Dist: isort>=5.12.0; extra == "dev"
23
+ Requires-Dist: pyinstaller>=6.0.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # Talks Reducer
27
+ Talks Reducer shortens long-form presentations by removing silent gaps and optionally re-encoding them to smaller files. The
28
+ project was renamed from **jumpcutter** to emphasize its focus on conference talks and screencasts.
29
+
30
+ ![Main demo](docs/assets/screencast-main.gif)
31
+
32
+ ## Example
33
+ - 1h 37m, 571 MB — Original OBS video recording
34
+ - 1h 19m, 751 MB — Talks Reducer
35
+ - 1h 19m, 171 MB — Talks Reducer `--small`
36
+
37
+ ## Changelog
38
+
39
+ See [CHANGELOG.md](CHANGELOG.md).
40
+
41
+ ## Install GUI (Windows, macOS)
42
+ Go to the [releases page](https://github.com/popstas/talks-reducer/releases) and download the appropriate artifact:
43
+
44
+ - **Windows** — `talks-reducer-windows.zip`
45
+ - **macOS** — `talks-reducer.app.zip` (but it doesn't work for me)
46
+
47
+ ## Install CLI (Linux, Windows, macOS)
48
+ ```
49
+ pip install talks-reducer
50
+ ```
51
+
52
+ **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.
53
+
54
+ The `--small` preset applies a 720p video scale and 128 kbps audio bitrate, making it useful for sharing talks over constrained
55
+ connections. Without `--small`, the script aims to preserve original quality while removing silence.
56
+
57
+ Example CLI usage:
58
+
59
+ ```sh
60
+ talks-reducer --small input.mp4
61
+ ```
62
+
63
+ When CUDA-capable hardware is available the pipeline leans on GPU encoders to keep export times low, but it still runs great on
64
+ CPUs.
65
+
66
+ ## Contributing
67
+ See `CONTRIBUTION.md` for development setup details and guidance on sharing improvements.
68
+
69
+ ## License
70
+ Talks Reducer is released under the MIT License. See `LICENSE` for the full text.
@@ -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=sjHMeY0H9ESG-Gn5BX0wFRBX7sXjWwsgS8u9Vb0bJ88,4396
4
+ talks_reducer/chunks.py,sha256=IpdZxRFPURSG5wP-OQ_p09CVP8wcKwIFysV29zOTSWI,2959
5
+ talks_reducer/cli.py,sha256=SKQznseHAGQ8yHPk22aO3XtM1wftzxNi26CmKRm-oZE,7000
6
+ talks_reducer/ffmpeg.py,sha256=CVrxwNcWHrzvxTzoALtx5UdNWXxxfOFYF3FES7lvaO4,11680
7
+ talks_reducer/gui.py,sha256=qqnZhGbBeOnunpXQG9Z9jIGEhbcglNCaHVHhUwMx9TY,54399
8
+ talks_reducer/models.py,sha256=vdQLliiHKUuYtNlZzS796kGK39cbtjkUfYcT95KwwKE,1197
9
+ talks_reducer/pipeline.py,sha256=nfAX8dooN3-009WqMyYTv4nINNMtVmbWtsmzQeBM9Wg,9415
10
+ talks_reducer/progress.py,sha256=Mh43M6VWhjjUv9CI22xfD2EJ_7Aq3PCueqefQ9Bd5-o,4565
11
+ talks_reducer-0.3.3.dist-info/licenses/LICENSE,sha256=jN17mHNR3e84awmH3AbpWBcBDBzPxEH0rcOFoj1s7sQ,1124
12
+ talks_reducer-0.3.3.dist-info/METADATA,sha256=z8XIj-8B0_4AYqkPFos7GnXfo3VRMygQVDAIGvjVgXk,2459
13
+ talks_reducer-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ talks_reducer-0.3.3.dist-info/entry_points.txt,sha256=LCzfSnh_7VXhvl9twoFSAj0C3sG7bayWs2LkxpH7hoI,100
15
+ talks_reducer-0.3.3.dist-info/top_level.txt,sha256=pJWGcy__LR9JIEKH3QJyFmk9XrIsiFtqvuMNxFdIzDU,14
16
+ talks_reducer-0.3.3.dist-info/RECORD,,
@@ -1,152 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: talks-reducer
3
- Version: 0.3.1
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: imageio-ffmpeg>=0.4.8
17
- Provides-Extra: dev
18
- Requires-Dist: build>=1.0.0; extra == "dev"
19
- Requires-Dist: twine>=4.0.0; extra == "dev"
20
- Requires-Dist: pytest>=7.0.0; extra == "dev"
21
- Requires-Dist: black>=23.0.0; extra == "dev"
22
- Requires-Dist: isort>=5.12.0; extra == "dev"
23
- Requires-Dist: pyinstaller>=6.0.0; extra == "dev"
24
- Dynamic: license-file
25
-
26
- # Talks Reducer
27
- Talks Reducer shortens long-form presentations by removing silent gaps and optionally re-encoding them to smaller files. The
28
- project was renamed from **jumpcutter** to emphasize its focus on conference talks and screencasts.
29
-
30
- ## Example
31
- - 1h 37m, 571 MB — Original OBS video recording
32
- - 1h 19m, 751 MB — Talks Reducer
33
- - 1h 19m, 171 MB — Talks Reducer `--small`
34
-
35
- ## Install GUI (Windows, macOS)
36
- Go to the [releases page](https://github.com/popstas/talks-reducer/releases) and download the appropriate artifact:
37
-
38
- - **Windows** — `talks-reducer-gui.exe`
39
- - **macOS** — `talks-reducer-gui-macos-universal` (requires macOS 10.13 High Sierra or
40
- newer). The bundle is built as a universal (`x86_64` + `arm64`) app so it runs
41
- natively on Apple Silicon without Rosetta.
42
-
43
- ## Install CLI (Linux, Windows, macOS)
44
- ```
45
- pip install talks-reducer
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
-
50
- The `--small` preset applies a 720p video scale and 128 kbps audio bitrate, making it useful for sharing talks over constrained
51
- connections. Without `--small`, the script aims to preserve original quality while removing silence.
52
-
53
- > **Tip:** Running `talks-reducer-gui` without arguments opens the Tkinter interface. Passing regular CLI options (for example,
54
- > `talks-reducer-gui --small input.mp4`) now executes the command-line pipeline, so you can keep a single shortcut for both
55
- > workflows.
56
-
57
- When CUDA-capable hardware is available the pipeline leans on GPU encoders to keep export times low, but it still runs great on
58
- CPUs.
59
-
60
- ### macOS codesigning and notarization
61
-
62
- Maintainers with Apple Developer credentials can optionally sign and notarize
63
- the GUI release to avoid Gatekeeper warnings on download:
64
-
65
- 1. Export or create a keychain profile for `notarytool` (see `man
66
- notarytool`) and note the profile name.
67
- 2. Set the following environment variables before running `scripts/build-gui.sh`:
68
- - `MACOS_CODESIGN_IDENTITY` — the signing identity, for example
69
- `Developer ID Application: Example Corp (TEAMID)`.
70
- - `MACOS_CODESIGN_ENTITLEMENTS` *(optional)* — path to an entitlements plist
71
- used during codesigning.
72
- - `MACOS_NOTARIZE_PROFILE` *(optional)* — the keychain profile name to submit
73
- the archive for notarization. When present, the script zips the `.app`,
74
- submits it with `notarytool --wait`, and staples the returned ticket.
75
-
76
- The codesigning step executes only when the variables are provided, so the build
77
- continues to work unchanged for local development.
78
-
79
- ### Graphical Interface
80
-
81
- - **Simple mode** — the default experience shrinks the window to a large drop
82
- zone, hides the manual run controls and log, and automatically processes new
83
- files as soon as you drop them. Uncheck the box to return to the full layout
84
- with file pickers, the Run button, and detailed logging.
85
- - **Input drop zone** — drag files or folders from your desktop, click to open
86
- the system file picker, or add them via the Explorer/Finder dialog; duplicates
87
- are ignored.
88
- - **Small video** — toggles the `--small` preset used by the CLI.
89
- - **Open after convert** — controls whether the exported file is revealed in
90
- your system file manager as soon as each job finishes.
91
- - **Advanced** — reveals optional controls for the output path, temp folder,
92
- timing/audio knobs mirrored from the command line, and an appearance picker
93
- that can force dark or light mode or follow your operating system.
94
-
95
- Progress updates stream into the 10-line log panel while the processing runs in
96
- a background thread. Once every queued job succeeds an **Open last output**
97
- button appears so you can jump straight to the exported file in your system
98
- file manager.
99
-
100
- The GUI stores your last-used Simple mode, Small video, Open after convert, and
101
- theme preferences in a cross-platform configuration file so they persist across
102
- launches.
103
-
104
- ## Repository Structure
105
- - `talks_reducer/` — Python package that exposes the CLI and reusable pipeline:
106
- - `cli.py` parses arguments and dispatches to the pipeline.
107
- - `pipeline.py` orchestrates FFmpeg, audio processing, and temporary assets.
108
- - `audio.py` handles audio validation, volume analysis, and phase vocoder processing.
109
- - `chunks.py` builds timing metadata and FFmpeg expressions for frame selection.
110
- - `ffmpeg.py` discovers the FFmpeg binary, checks CUDA availability, and assembles command strings.
111
- - `requirements.txt` — Python dependencies for local development.
112
- - `default.nix` — reproducible environment definition for Nix users.
113
- - `CONTRIBUTION.md` — development workflow, formatting expectations, and release checklist.
114
- - `AGENTS.md` — maintainer tips and coding conventions for this repository.
115
-
116
- ## Highlights
117
- - Builds on gegell's classic jumpcutter workflow with more efficient frame and audio processing
118
- - Generates FFmpeg filter graphs instead of writing temporary frames to disk
119
- - Streams audio transformations in memory to avoid slow intermediate files
120
- - Accepts multiple inputs or directories of recordings in a single run
121
- - Provides progress feedback via `tqdm`
122
- - Automatically detects NVENC availability, so you no longer need to pass `--cuda`
123
-
124
- ## Processing Pipeline
125
- 1. Validate that each input file contains an audio stream using `ffprobe`.
126
- 2. Extract audio and calculate loudness to identify silent regions.
127
- 3. Stretch the non-silent segments with `audiotsm` to maintain speech clarity.
128
- 4. Stitch the processed audio and video together with FFmpeg, using NVENC if the GPU encoders are detected.
129
-
130
- ## Recent Updates
131
- - **October 2025** — Project renamed to *Talks Reducer* across documentation and scripts.
132
- - **October 2025** — Added `--small` preset with 720p/128 kbps defaults for bandwidth-friendly exports.
133
- - **October 2025** — Removed the `--cuda` flag; CUDA/NVENC support is now auto-detected.
134
-
135
- ## Changelog
136
- Major and minor releases are tracked in `CHANGELOG.md`. The log is generated from
137
- Conventional Commits that start with either `feat:` or `fix:`. Only tags in the
138
- form `v<major>.<minor>.0` are included so patch releases (for example,
139
- `v1.1.1`) are omitted. Regenerate the file whenever you cut a release:
140
-
141
- ```bash
142
- python scripts/generate_changelog.py
143
- ```
144
-
145
- CI will fail if the generated changelog does not match the committed version, so
146
- run the script before opening a pull request that updates release tags.
147
-
148
- ## Contributing
149
- See `CONTRIBUTION.md` for development setup details and guidance on sharing improvements.
150
-
151
- ## License
152
- Talks Reducer is released under the MIT License. See `LICENSE` for the full text.
@@ -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=sjHMeY0H9ESG-Gn5BX0wFRBX7sXjWwsgS8u9Vb0bJ88,4396
4
- talks_reducer/chunks.py,sha256=IpdZxRFPURSG5wP-OQ_p09CVP8wcKwIFysV29zOTSWI,2959
5
- talks_reducer/cli.py,sha256=ZQcay6NF32g0PF7OGLKWPY1TbIR1Hx2xaRlzOXK1lto,6508
6
- talks_reducer/ffmpeg.py,sha256=tM_T2mV_Y7U91QzWmTBEid9cjEobLpPnxsiOUfz_yDA,11697
7
- talks_reducer/gui.py,sha256=42gWHLs1LlNvNYp-mEnUmpJd-XpempOF5tWytA5fTRs,53679
8
- talks_reducer/models.py,sha256=6cZRcJf0EBZIzNd-PWrh4Wdsoa4EBj5nSdB6BnFiOXM,1106
9
- talks_reducer/pipeline.py,sha256=deGvGMF3CSVd7lcpA7dke8dlLQw3mi_FEhSkFNte7Ro,8871
10
- talks_reducer/progress.py,sha256=Mh43M6VWhjjUv9CI22xfD2EJ_7Aq3PCueqefQ9Bd5-o,4565
11
- talks_reducer-0.3.1.dist-info/licenses/LICENSE,sha256=jN17mHNR3e84awmH3AbpWBcBDBzPxEH0rcOFoj1s7sQ,1124
12
- talks_reducer-0.3.1.dist-info/METADATA,sha256=NU0eHj8kK0bC4Lz-AMjqxlAUhxKuaSgODYVr5iRkT9Y,7660
13
- talks_reducer-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- talks_reducer-0.3.1.dist-info/entry_points.txt,sha256=LCzfSnh_7VXhvl9twoFSAj0C3sG7bayWs2LkxpH7hoI,100
15
- talks_reducer-0.3.1.dist-info/top_level.txt,sha256=pJWGcy__LR9JIEKH3QJyFmk9XrIsiFtqvuMNxFdIzDU,14
16
- talks_reducer-0.3.1.dist-info/RECORD,,