talks-reducer 0.4.1__tar.gz → 0.5.0__tar.gz
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-0.4.1/talks_reducer.egg-info → talks_reducer-0.5.0}/PKG-INFO +93 -71
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/README.md +21 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/pyproject.toml +2 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/setup.cfg +4 -4
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/__about__.py +1 -1
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/cli.py +23 -2
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/ffmpeg.py +28 -6
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/gui.py +159 -24
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/models.py +2 -2
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/pipeline.py +82 -8
- talks_reducer-0.5.0/talks_reducer/server.py +353 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0/talks_reducer.egg-info}/PKG-INFO +93 -71
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer.egg-info/SOURCES.txt +3 -1
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer.egg-info/entry_points.txt +1 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer.egg-info/requires.txt +1 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/tests/test_cli.py +27 -0
- talks_reducer-0.5.0/tests/test_server.py +68 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/LICENSE +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/__init__.py +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/__main__.py +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/audio.py +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/chunks.py +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer/progress.py +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer.egg-info/dependency_links.txt +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/talks_reducer.egg-info/top_level.txt +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/tests/test_audio.py +0 -0
- {talks_reducer-0.4.1 → talks_reducer-0.5.0}/tests/test_pipeline_service.py +0 -0
@@ -1,71 +1,93 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: talks-reducer
|
3
|
-
Version: 0.
|
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
|
-
|
18
|
-
|
19
|
-
Requires-Dist:
|
20
|
-
Requires-Dist:
|
21
|
-
Requires-Dist:
|
22
|
-
Requires-Dist:
|
23
|
-
Requires-Dist:
|
24
|
-
Requires-Dist:
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
Talks Reducer
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
- 1h
|
36
|
-
- 1h 19m,
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
- **
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: talks-reducer
|
3
|
+
Version: 0.5.0
|
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
|
+
Requires-Dist: gradio>=4.0.0
|
18
|
+
Provides-Extra: dev
|
19
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
20
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
21
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
22
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
23
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
24
|
+
Requires-Dist: bump-my-version>=0.5.0; extra == "dev"
|
25
|
+
Requires-Dist: pyinstaller>=6.4.0; extra == "dev"
|
26
|
+
Dynamic: license-file
|
27
|
+
|
28
|
+
# Talks Reducer
|
29
|
+
Talks Reducer shortens long-form presentations by removing silent gaps and optionally re-encoding them to smaller files. The
|
30
|
+
project was renamed from **jumpcutter** to emphasize its focus on conference talks and screencasts.
|
31
|
+
|
32
|
+

|
33
|
+
|
34
|
+
## Example
|
35
|
+
- 1h 37m, 571 MB — Original OBS video recording
|
36
|
+
- 1h 19m, 751 MB — Talks Reducer
|
37
|
+
- 1h 19m, 171 MB — Talks Reducer `--small`
|
38
|
+
|
39
|
+
## Changelog
|
40
|
+
|
41
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
42
|
+
|
43
|
+
## Install GUI (Windows, macOS)
|
44
|
+
Go to the [releases page](https://github.com/popstas/talks-reducer/releases) and download the appropriate artifact:
|
45
|
+
|
46
|
+
- **Windows** — `talks-reducer-windows-0.4.0.zip`
|
47
|
+
- **macOS** — `talks-reducer.app.zip` (but it doesn't work for me)
|
48
|
+
|
49
|
+
When extracted on Windows the bundled `talks-reducer.exe` behaves like the
|
50
|
+
`python talks_reducer/gui.py` entry point: double-clicking it launches the GUI
|
51
|
+
and passing a video file path (for example via *Open with…* or drag-and-drop
|
52
|
+
onto the executable) automatically queues that recording for processing.
|
53
|
+
|
54
|
+
## Install CLI (Linux, Windows, macOS)
|
55
|
+
```
|
56
|
+
pip install talks-reducer
|
57
|
+
```
|
58
|
+
|
59
|
+
**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.
|
60
|
+
|
61
|
+
The `--small` preset applies a 720p video scale and 128 kbps audio bitrate, making it useful for sharing talks over constrained
|
62
|
+
connections. Without `--small`, the script aims to preserve original quality while removing silence.
|
63
|
+
|
64
|
+
Example CLI usage:
|
65
|
+
|
66
|
+
```sh
|
67
|
+
talks-reducer --small input.mp4
|
68
|
+
```
|
69
|
+
|
70
|
+
### Speech detection
|
71
|
+
|
72
|
+
Talks Reducer now relies on its built-in volume thresholding to detect speech. Adjust `--silent_threshold` if you need to fine-tune when segments count as silence. Dropping the optional Silero VAD integration keeps the install lightweight and avoids pulling in PyTorch.
|
73
|
+
|
74
|
+
When CUDA-capable hardware is available the pipeline leans on GPU encoders to keep export times low, but it still runs great on
|
75
|
+
CPUs.
|
76
|
+
|
77
|
+
## Simple web server
|
78
|
+
|
79
|
+
Prefer a lightweight browser interface? Launch the Gradio-powered simple mode with:
|
80
|
+
|
81
|
+
```sh
|
82
|
+
talks-reducer server
|
83
|
+
```
|
84
|
+
|
85
|
+
This opens a local web page featuring a drag-and-drop upload zone, a **Small video** checkbox that mirrors the CLI preset, a live
|
86
|
+
progress indicator, and automatic previews of the processed output. Once the job completes you can inspect the resulting compression
|
87
|
+
ratio and download the rendered video directly from the page.
|
88
|
+
|
89
|
+
## Contributing
|
90
|
+
See `CONTRIBUTION.md` for development setup details and guidance on sharing improvements.
|
91
|
+
|
92
|
+
## License
|
93
|
+
Talks Reducer is released under the MIT License. See `LICENSE` for the full text.
|
@@ -19,6 +19,11 @@ Go to the [releases page](https://github.com/popstas/talks-reducer/releases) and
|
|
19
19
|
- **Windows** — `talks-reducer-windows-0.4.0.zip`
|
20
20
|
- **macOS** — `talks-reducer.app.zip` (but it doesn't work for me)
|
21
21
|
|
22
|
+
When extracted on Windows the bundled `talks-reducer.exe` behaves like the
|
23
|
+
`python talks_reducer/gui.py` entry point: double-clicking it launches the GUI
|
24
|
+
and passing a video file path (for example via *Open with…* or drag-and-drop
|
25
|
+
onto the executable) automatically queues that recording for processing.
|
26
|
+
|
22
27
|
## Install CLI (Linux, Windows, macOS)
|
23
28
|
```
|
24
29
|
pip install talks-reducer
|
@@ -35,9 +40,25 @@ Example CLI usage:
|
|
35
40
|
talks-reducer --small input.mp4
|
36
41
|
```
|
37
42
|
|
43
|
+
### Speech detection
|
44
|
+
|
45
|
+
Talks Reducer now relies on its built-in volume thresholding to detect speech. Adjust `--silent_threshold` if you need to fine-tune when segments count as silence. Dropping the optional Silero VAD integration keeps the install lightweight and avoids pulling in PyTorch.
|
46
|
+
|
38
47
|
When CUDA-capable hardware is available the pipeline leans on GPU encoders to keep export times low, but it still runs great on
|
39
48
|
CPUs.
|
40
49
|
|
50
|
+
## Simple web server
|
51
|
+
|
52
|
+
Prefer a lightweight browser interface? Launch the Gradio-powered simple mode with:
|
53
|
+
|
54
|
+
```sh
|
55
|
+
talks-reducer server
|
56
|
+
```
|
57
|
+
|
58
|
+
This opens a local web page featuring a drag-and-drop upload zone, a **Small video** checkbox that mirrors the CLI preset, a live
|
59
|
+
progress indicator, and automatic previews of the processed output. Once the job completes you can inspect the resulting compression
|
60
|
+
ratio and download the rendered video directly from the page.
|
61
|
+
|
41
62
|
## Contributing
|
42
63
|
See `CONTRIBUTION.md` for development setup details and guidance on sharing improvements.
|
43
64
|
|
@@ -20,6 +20,7 @@ dependencies = [
|
|
20
20
|
"tkinterdnd2>=0.3.0",
|
21
21
|
"Pillow>=9.0.0",
|
22
22
|
"imageio-ffmpeg>=0.4.8",
|
23
|
+
"gradio>=4.0.0",
|
23
24
|
]
|
24
25
|
|
25
26
|
[project.optional-dependencies]
|
@@ -36,6 +37,7 @@ dev = [
|
|
36
37
|
[project.scripts]
|
37
38
|
talks-reducer = "talks_reducer.cli:main"
|
38
39
|
talks-reducer-gui = "talks_reducer.gui:main"
|
40
|
+
talks-reducer-server = "talks_reducer.server:main"
|
39
41
|
|
40
42
|
[tool.setuptools.dynamic]
|
41
43
|
version = {attr = "talks_reducer.__about__.__version__"}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
[egg_info]
|
2
|
-
tag_build =
|
3
|
-
tag_date = 0
|
4
|
-
|
1
|
+
[egg_info]
|
2
|
+
tag_build =
|
3
|
+
tag_date = 0
|
4
|
+
|
@@ -63,7 +63,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
63
63
|
"--silent_threshold",
|
64
64
|
type=float,
|
65
65
|
dest="silent_threshold",
|
66
|
-
help="The volume amount that frames' audio needs to surpass to be considered sounded. Defaults to 0.
|
66
|
+
help="The volume amount that frames' audio needs to surpass to be considered sounded. Defaults to 0.05.",
|
67
67
|
)
|
68
68
|
parser.add_argument(
|
69
69
|
"-S",
|
@@ -143,6 +143,22 @@ def _launch_gui(argv: Sequence[str]) -> bool:
|
|
143
143
|
return bool(gui_main(list(argv)))
|
144
144
|
|
145
145
|
|
146
|
+
def _launch_server(argv: Sequence[str]) -> bool:
|
147
|
+
"""Attempt to launch the Gradio web server with the provided arguments."""
|
148
|
+
|
149
|
+
try:
|
150
|
+
server_module = import_module(".server", __package__)
|
151
|
+
except ImportError:
|
152
|
+
return False
|
153
|
+
|
154
|
+
server_main = getattr(server_module, "main", None)
|
155
|
+
if server_main is None:
|
156
|
+
return False
|
157
|
+
|
158
|
+
server_main(list(argv))
|
159
|
+
return True
|
160
|
+
|
161
|
+
|
146
162
|
def main(argv: Optional[Sequence[str]] = None) -> None:
|
147
163
|
"""Entry point for the command line interface.
|
148
164
|
|
@@ -154,6 +170,12 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
154
170
|
else:
|
155
171
|
argv_list = list(argv)
|
156
172
|
|
173
|
+
if argv_list and argv_list[0] in {"server", "serve"}:
|
174
|
+
if not _launch_server(argv_list[1:]):
|
175
|
+
print("Gradio server mode is unavailable.", file=sys.stderr)
|
176
|
+
sys.exit(1)
|
177
|
+
return
|
178
|
+
|
157
179
|
if not argv_list:
|
158
180
|
if _launch_gui(argv_list):
|
159
181
|
return
|
@@ -200,7 +222,6 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
200
222
|
option_kwargs["sample_rate"] = int(local_options["sample_rate"])
|
201
223
|
if "small" in local_options:
|
202
224
|
option_kwargs["small"] = bool(local_options["small"])
|
203
|
-
|
204
225
|
options = ProcessingOptions(**option_kwargs)
|
205
226
|
|
206
227
|
try:
|
@@ -242,11 +242,29 @@ def run_timed_ffmpeg_command(
|
|
242
242
|
if not line:
|
243
243
|
continue
|
244
244
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
245
|
+
# Filter out excessive progress output, only show important lines
|
246
|
+
if any(
|
247
|
+
keyword in line.lower()
|
248
|
+
for keyword in [
|
249
|
+
"error",
|
250
|
+
"warning",
|
251
|
+
"encoded successfully",
|
252
|
+
"frame=",
|
253
|
+
"time=",
|
254
|
+
"size=",
|
255
|
+
"bitrate=",
|
256
|
+
"speed=",
|
257
|
+
]
|
258
|
+
):
|
259
|
+
sys.stderr.write(line)
|
260
|
+
sys.stderr.flush()
|
261
|
+
|
262
|
+
# Send FFmpeg output to reporter for GUI display (filtered)
|
263
|
+
if any(
|
264
|
+
keyword in line.lower()
|
265
|
+
for keyword in ["error", "warning", "encoded successfully", "frame="]
|
266
|
+
):
|
267
|
+
progress_reporter.log(line.strip())
|
250
268
|
|
251
269
|
match = re.search(r"frame=\s*(\d+)", line)
|
252
270
|
if match:
|
@@ -365,7 +383,11 @@ def build_video_commands(
|
|
365
383
|
# Use a fast software encoder instead
|
366
384
|
video_encoder_args = ["-c:v libx264", "-preset veryfast", "-crf 23"]
|
367
385
|
|
368
|
-
audio_parts = [
|
386
|
+
audio_parts = [
|
387
|
+
"-c:a aac",
|
388
|
+
f'"{output_file}"',
|
389
|
+
"-loglevel warning -stats -hide_banner",
|
390
|
+
]
|
369
391
|
|
370
392
|
full_command_parts = (
|
371
393
|
global_parts + input_parts + output_parts + video_encoder_args + audio_parts
|
@@ -200,6 +200,7 @@ class _TkProgressReporter(SignalProgressReporter):
|
|
200
200
|
|
201
201
|
def log(self, message: str) -> None:
|
202
202
|
self._log_callback(message)
|
203
|
+
print(message, flush=True)
|
203
204
|
|
204
205
|
def task(
|
205
206
|
self, *, desc: str = "", total: Optional[int] = None, unit: str = ""
|
@@ -256,7 +257,12 @@ class TalksReducerGUI:
|
|
256
257
|
self._settings[key] = value
|
257
258
|
self._save_settings()
|
258
259
|
|
259
|
-
def __init__(
|
260
|
+
def __init__(
|
261
|
+
self,
|
262
|
+
initial_inputs: Optional[Sequence[str]] = None,
|
263
|
+
*,
|
264
|
+
auto_run: bool = False,
|
265
|
+
) -> None:
|
260
266
|
self._config_path = self._determine_config_path()
|
261
267
|
self._settings = self._load_settings()
|
262
268
|
|
@@ -284,8 +290,8 @@ class TalksReducerGUI:
|
|
284
290
|
|
285
291
|
self._apply_window_icon()
|
286
292
|
|
287
|
-
self._full_size = (
|
288
|
-
self._simple_size = (
|
293
|
+
self._full_size = (1000, 800)
|
294
|
+
self._simple_size = (300, 270)
|
289
295
|
self.root.geometry(f"{self._full_size[0]}x{self._full_size[1]}")
|
290
296
|
self.style = self.ttk.Style(self.root)
|
291
297
|
|
@@ -298,6 +304,9 @@ class TalksReducerGUI:
|
|
298
304
|
self._status_animation_job: Optional[str] = None
|
299
305
|
self._status_animation_phase = 0
|
300
306
|
self._video_duration_seconds: Optional[float] = None
|
307
|
+
self._encode_target_duration_seconds: Optional[float] = None
|
308
|
+
self._encode_total_frames: Optional[int] = None
|
309
|
+
self._encode_current_frame: Optional[int] = None
|
301
310
|
self.progress_var = tk.IntVar(value=0)
|
302
311
|
self._ffmpeg_process: Optional[subprocess.Popen] = None
|
303
312
|
self._stop_requested = False
|
@@ -333,6 +342,9 @@ class TalksReducerGUI:
|
|
333
342
|
"Drag and drop requires the tkinterdnd2 package. Install it to enable the drop zone."
|
334
343
|
)
|
335
344
|
|
345
|
+
if initial_inputs:
|
346
|
+
self._populate_initial_inputs(initial_inputs, auto_run=auto_run)
|
347
|
+
|
336
348
|
# ------------------------------------------------------------------ UI --
|
337
349
|
def _apply_window_icon(self) -> None:
|
338
350
|
"""Configure the application icon when the asset is available."""
|
@@ -423,7 +435,7 @@ class TalksReducerGUI:
|
|
423
435
|
|
424
436
|
# Options frame
|
425
437
|
options = self.ttk.Frame(main, padding=self.PADDING)
|
426
|
-
options.grid(row=2, column=0, pady=(
|
438
|
+
options.grid(row=2, column=0, pady=(0, 0), sticky="ew")
|
427
439
|
options.columnconfigure(0, weight=1)
|
428
440
|
|
429
441
|
checkbox_frame = self.ttk.Frame(options)
|
@@ -496,7 +508,7 @@ class TalksReducerGUI:
|
|
496
508
|
self.advanced_frame, "Frame margin", self.frame_margin_var, row=5
|
497
509
|
)
|
498
510
|
|
499
|
-
self.sample_rate_var = self.tk.StringVar()
|
511
|
+
self.sample_rate_var = self.tk.StringVar(value="48000")
|
500
512
|
self._add_entry(self.advanced_frame, "Sample rate", self.sample_rate_var, row=6)
|
501
513
|
|
502
514
|
self.ttk.Label(self.advanced_frame, text="Theme").grid(
|
@@ -862,6 +874,26 @@ class TalksReducerGUI:
|
|
862
874
|
widget.drop_target_register(DND_FILES) # type: ignore[arg-type]
|
863
875
|
widget.dnd_bind("<<Drop>>", self._on_drop) # type: ignore[attr-defined]
|
864
876
|
|
877
|
+
def _populate_initial_inputs(
|
878
|
+
self, inputs: Sequence[str], *, auto_run: bool = False
|
879
|
+
) -> None:
|
880
|
+
"""Seed the GUI with preselected inputs and optionally start processing."""
|
881
|
+
|
882
|
+
normalized: list[str] = []
|
883
|
+
for path in inputs:
|
884
|
+
if not path:
|
885
|
+
continue
|
886
|
+
resolved = os.fspath(Path(path))
|
887
|
+
if resolved not in self.input_files:
|
888
|
+
self.input_files.append(resolved)
|
889
|
+
self.input_list.insert(self.tk.END, resolved)
|
890
|
+
normalized.append(resolved)
|
891
|
+
|
892
|
+
if auto_run and normalized:
|
893
|
+
# Kick off processing once the event loop becomes idle so the
|
894
|
+
# interface has a chance to render before the work starts.
|
895
|
+
self.root.after_idle(self._start_run)
|
896
|
+
|
865
897
|
# -------------------------------------------------------------- actions --
|
866
898
|
def _ask_for_input_files(self) -> tuple[str, ...]:
|
867
899
|
"""Prompt the user to select input files for processing."""
|
@@ -1014,11 +1046,10 @@ class TalksReducerGUI:
|
|
1014
1046
|
self._append_log("Processing aborted by user.")
|
1015
1047
|
self._set_status("Aborted")
|
1016
1048
|
else:
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
)
|
1049
|
+
error_msg = f"Processing failed: {exc}"
|
1050
|
+
self._append_log(error_msg)
|
1051
|
+
print(error_msg, file=sys.stderr) # Also output to console
|
1052
|
+
self._notify(lambda: self.messagebox.showerror("Error", error_msg))
|
1022
1053
|
self._set_status("Error")
|
1023
1054
|
finally:
|
1024
1055
|
self._notify(self._hide_stop_button)
|
@@ -1093,7 +1124,6 @@ class TalksReducerGUI:
|
|
1093
1124
|
)
|
1094
1125
|
if self.small_var.get():
|
1095
1126
|
args["small"] = True
|
1096
|
-
|
1097
1127
|
return args
|
1098
1128
|
|
1099
1129
|
def _parse_float(self, value: str, label: str) -> float:
|
@@ -1155,16 +1185,73 @@ class TalksReducerGUI:
|
|
1155
1185
|
self._set_status("success", status_msg)
|
1156
1186
|
self._set_progress(100) # 100% on success
|
1157
1187
|
self._video_duration_seconds = None # Reset for next video
|
1188
|
+
self._encode_target_duration_seconds = None
|
1189
|
+
self._encode_total_frames = None
|
1190
|
+
self._encode_current_frame = None
|
1158
1191
|
elif normalized.startswith("extracting audio"):
|
1159
1192
|
self._set_status("processing", "Extracting audio...")
|
1160
1193
|
self._set_progress(0) # 0% on start
|
1161
1194
|
self._video_duration_seconds = None # Reset for new processing
|
1195
|
+
self._encode_target_duration_seconds = None
|
1196
|
+
self._encode_total_frames = None
|
1197
|
+
self._encode_current_frame = None
|
1162
1198
|
elif normalized.startswith("starting processing") or normalized.startswith(
|
1163
1199
|
"processing"
|
1164
1200
|
):
|
1165
1201
|
self._set_status("processing", "Processing")
|
1166
1202
|
self._set_progress(0) # 0% on start
|
1167
1203
|
self._video_duration_seconds = None # Reset for new processing
|
1204
|
+
self._encode_target_duration_seconds = None
|
1205
|
+
self._encode_total_frames = None
|
1206
|
+
self._encode_current_frame = None
|
1207
|
+
|
1208
|
+
frame_total_match = re.search(
|
1209
|
+
r"Final encode target frames(?: \(fallback\))?:\s*(\d+)", message
|
1210
|
+
)
|
1211
|
+
if frame_total_match:
|
1212
|
+
self._encode_total_frames = int(frame_total_match.group(1))
|
1213
|
+
return
|
1214
|
+
|
1215
|
+
if "final encode target frames" in normalized and "unknown" in normalized:
|
1216
|
+
self._encode_total_frames = None
|
1217
|
+
return
|
1218
|
+
|
1219
|
+
frame_match = re.search(r"frame=\s*(\d+)", message)
|
1220
|
+
if frame_match:
|
1221
|
+
try:
|
1222
|
+
current_frame = int(frame_match.group(1))
|
1223
|
+
except ValueError:
|
1224
|
+
current_frame = None
|
1225
|
+
|
1226
|
+
if current_frame is not None:
|
1227
|
+
if self._encode_current_frame == current_frame:
|
1228
|
+
return
|
1229
|
+
|
1230
|
+
self._encode_current_frame = current_frame
|
1231
|
+
if self._encode_total_frames and self._encode_total_frames > 0:
|
1232
|
+
percentage = min(
|
1233
|
+
100,
|
1234
|
+
int((current_frame / self._encode_total_frames) * 100),
|
1235
|
+
)
|
1236
|
+
self._set_progress(percentage)
|
1237
|
+
else:
|
1238
|
+
self._set_status("processing", f"{current_frame} frames encoded")
|
1239
|
+
|
1240
|
+
# Parse encode target duration reported by the pipeline
|
1241
|
+
encode_duration_match = re.search(
|
1242
|
+
r"Final encode target duration(?: \(fallback\))?:\s*([\d.]+)s",
|
1243
|
+
message,
|
1244
|
+
)
|
1245
|
+
if encode_duration_match:
|
1246
|
+
try:
|
1247
|
+
self._encode_target_duration_seconds = float(
|
1248
|
+
encode_duration_match.group(1)
|
1249
|
+
)
|
1250
|
+
except ValueError:
|
1251
|
+
self._encode_target_duration_seconds = None
|
1252
|
+
|
1253
|
+
if "final encode target duration" in normalized and "unknown" in normalized:
|
1254
|
+
self._encode_target_duration_seconds = None
|
1168
1255
|
|
1169
1256
|
# Parse video duration from FFmpeg output
|
1170
1257
|
duration_match = re.search(r"Duration:\s*(\d{2}):(\d{2}):(\d{2}\.\d+)", message)
|
@@ -1182,21 +1269,34 @@ class TalksReducerGUI:
|
|
1182
1269
|
hours = int(time_match.group(1))
|
1183
1270
|
minutes = int(time_match.group(2))
|
1184
1271
|
seconds = int(time_match.group(3))
|
1185
|
-
|
1272
|
+
current_seconds = hours * 3600 + minutes * 60 + seconds
|
1273
|
+
time_str = self._format_progress_time(current_seconds)
|
1186
1274
|
speed_str = speed_match.group(1)
|
1187
1275
|
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
self._set_status(
|
1195
|
-
"processing", f"{time_str}, {speed_str}x ({percentage}%)"
|
1196
|
-
)
|
1197
|
-
self._set_progress(percentage) # Update progress bar
|
1276
|
+
total_seconds = (
|
1277
|
+
self._encode_target_duration_seconds or self._video_duration_seconds
|
1278
|
+
)
|
1279
|
+
if total_seconds:
|
1280
|
+
total_str = self._format_progress_time(total_seconds)
|
1281
|
+
time_display = f"{time_str} / {total_str}"
|
1198
1282
|
else:
|
1199
|
-
|
1283
|
+
time_display = time_str
|
1284
|
+
|
1285
|
+
status_msg = f"{time_display}, {speed_str}x"
|
1286
|
+
|
1287
|
+
if (
|
1288
|
+
(
|
1289
|
+
not self._encode_total_frames
|
1290
|
+
or self._encode_total_frames <= 0
|
1291
|
+
or self._encode_current_frame is None
|
1292
|
+
)
|
1293
|
+
and total_seconds
|
1294
|
+
and total_seconds > 0
|
1295
|
+
):
|
1296
|
+
percentage = min(100, int((current_seconds / total_seconds) * 100))
|
1297
|
+
self._set_progress(percentage)
|
1298
|
+
|
1299
|
+
self._set_status("processing", status_msg)
|
1200
1300
|
|
1201
1301
|
def _apply_status_style(self, status: str) -> None:
|
1202
1302
|
color = STATUS_COLORS.get(status.lower())
|
@@ -1208,7 +1308,7 @@ class TalksReducerGUI:
|
|
1208
1308
|
status_lower = status.lower()
|
1209
1309
|
if (
|
1210
1310
|
"extracting audio" in status_lower
|
1211
|
-
or re.search(r"\d
|
1311
|
+
or re.search(r"\d+:\d{2}(?: / \d+:\d{2})?.*\d+\.?\d*x", status)
|
1212
1312
|
or ("time:" in status_lower and "size:" in status_lower)
|
1213
1313
|
):
|
1214
1314
|
if "time:" in status_lower and "size:" in status_lower:
|
@@ -1261,6 +1361,23 @@ class TalksReducerGUI:
|
|
1261
1361
|
|
1262
1362
|
self.root.after(0, apply)
|
1263
1363
|
|
1364
|
+
def _format_progress_time(self, total_seconds: float) -> str:
|
1365
|
+
"""Format a duration in seconds as h:mm or m:ss for status display."""
|
1366
|
+
|
1367
|
+
try:
|
1368
|
+
rounded_seconds = max(0, int(round(total_seconds)))
|
1369
|
+
except (TypeError, ValueError):
|
1370
|
+
return "0:00"
|
1371
|
+
|
1372
|
+
hours, remainder = divmod(rounded_seconds, 3600)
|
1373
|
+
minutes, seconds = divmod(remainder, 60)
|
1374
|
+
|
1375
|
+
if hours > 0:
|
1376
|
+
return f"{hours}:{minutes:02d}"
|
1377
|
+
|
1378
|
+
total_minutes = rounded_seconds // 60
|
1379
|
+
return f"{total_minutes}:{seconds:02d}"
|
1380
|
+
|
1264
1381
|
def _calculate_gradient_color(self, percentage: int, darken: float = 1.0) -> str:
|
1265
1382
|
"""Calculate color gradient from red (0%) to green (100%).
|
1266
1383
|
|
@@ -1377,6 +1494,24 @@ def main(argv: Optional[Sequence[str]] = None) -> bool:
|
|
1377
1494
|
argv = sys.argv[1:]
|
1378
1495
|
|
1379
1496
|
if argv:
|
1497
|
+
launch_gui = False
|
1498
|
+
if sys.platform == "win32" and not any(arg.startswith("-") for arg in argv):
|
1499
|
+
# Only attempt to launch the GUI automatically when the arguments
|
1500
|
+
# look like file or directory paths. This matches the behaviour of
|
1501
|
+
# file association launches on Windows while still allowing the CLI
|
1502
|
+
# to be used explicitly with option flags.
|
1503
|
+
if any(Path(arg).exists() for arg in argv if arg):
|
1504
|
+
launch_gui = True
|
1505
|
+
|
1506
|
+
if launch_gui:
|
1507
|
+
try:
|
1508
|
+
app = TalksReducerGUI(argv, auto_run=True)
|
1509
|
+
app.run()
|
1510
|
+
return True
|
1511
|
+
except Exception:
|
1512
|
+
# Fall back to the CLI if the GUI cannot be started.
|
1513
|
+
pass
|
1514
|
+
|
1380
1515
|
cli_main(argv)
|
1381
1516
|
return False
|
1382
1517
|
|
@@ -18,8 +18,8 @@ class ProcessingOptions:
|
|
18
18
|
input_file: Path
|
19
19
|
output_file: Optional[Path] = None
|
20
20
|
frame_rate: float = 30.0
|
21
|
-
sample_rate: int =
|
22
|
-
silent_threshold: float = 0.
|
21
|
+
sample_rate: int = 48000
|
22
|
+
silent_threshold: float = 0.05
|
23
23
|
silent_speed: float = 4.0
|
24
24
|
sounded_speed: float = 1.0
|
25
25
|
frame_spreadage: int = 2
|