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 +30 -17
- talks_reducer/ffmpeg.py +7 -6
- talks_reducer/gui.py +43 -44
- talks_reducer/models.py +3 -0
- talks_reducer/pipeline.py +12 -1
- talks_reducer-0.3.3.dist-info/METADATA +70 -0
- talks_reducer-0.3.3.dist-info/RECORD +16 -0
- talks_reducer-0.3.1.dist-info/METADATA +0 -152
- talks_reducer-0.3.1.dist-info/RECORD +0 -16
- {talks_reducer-0.3.1.dist-info → talks_reducer-0.3.3.dist-info}/WHEEL +0 -0
- {talks_reducer-0.3.1.dist-info → talks_reducer-0.3.3.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.3.1.dist-info → talks_reducer-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.3.1.dist-info → talks_reducer-0.3.3.dist-info}/top_level.txt +0 -0
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
|
-
|
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(
|
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(
|
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.
|
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
|
-
|
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
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
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
|
-
|
1198
|
-
|
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
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,
|
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
|
+

|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|