talks-reducer 0.7.2__py3-none-any.whl → 0.8.1__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/__about__.py +1 -1
- talks_reducer/cli.py +225 -181
- talks_reducer/discovery.py +78 -22
- talks_reducer/gui/__init__.py +17 -1546
- talks_reducer/gui/__main__.py +1 -1
- talks_reducer/gui/app.py +1385 -0
- talks_reducer/gui/discovery.py +1 -1
- talks_reducer/gui/layout.py +1 -1
- talks_reducer/gui/progress.py +80 -0
- talks_reducer/gui/remote.py +11 -3
- talks_reducer/gui/startup.py +202 -0
- talks_reducer/icons.py +2 -0
- talks_reducer/pipeline.py +65 -31
- talks_reducer/resources/icons/app-256.png +0 -0
- talks_reducer/resources/icons/app.icns +0 -0
- talks_reducer/resources/icons/app.ico +0 -0
- talks_reducer/server.py +106 -41
- talks_reducer/server_tray.py +116 -39
- talks_reducer/service_client.py +77 -14
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/METADATA +2 -2
- talks_reducer-0.8.1.dist-info/RECORD +36 -0
- talks_reducer-0.7.2.dist-info/RECORD +0 -30
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/WHEEL +0 -0
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/top_level.txt +0 -0
talks_reducer/gui/discovery.py
CHANGED
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, List
|
|
8
8
|
from ..discovery import discover_servers as core_discover_servers
|
9
9
|
|
10
10
|
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
|
11
|
-
from . import TalksReducerGUI
|
11
|
+
from .app import TalksReducerGUI
|
12
12
|
|
13
13
|
|
14
14
|
def start_discovery(gui: "TalksReducerGUI") -> None:
|
talks_reducer/gui/layout.py
CHANGED
@@ -11,7 +11,7 @@ from ..models import default_temp_folder
|
|
11
11
|
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
|
12
12
|
import tkinter as tk
|
13
13
|
|
14
|
-
from . import TalksReducerGUI
|
14
|
+
from .app import TalksReducerGUI
|
15
15
|
|
16
16
|
|
17
17
|
def build_layout(gui: "TalksReducerGUI") -> None:
|
@@ -0,0 +1,80 @@
|
|
1
|
+
"""Progress helpers that bridge the pipeline with the Tkinter GUI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Callable, Optional
|
6
|
+
|
7
|
+
from ..progress import ProgressHandle, SignalProgressReporter
|
8
|
+
|
9
|
+
|
10
|
+
class _GuiProgressHandle(ProgressHandle):
|
11
|
+
"""Simple progress handle that records totals but only logs milestones."""
|
12
|
+
|
13
|
+
def __init__(self, log_callback: Callable[[str], None], desc: str) -> None:
|
14
|
+
self._log_callback = log_callback
|
15
|
+
self._desc = desc
|
16
|
+
self._current = 0
|
17
|
+
self._total: Optional[int] = None
|
18
|
+
if desc:
|
19
|
+
self._log_callback(f"{desc} started")
|
20
|
+
|
21
|
+
@property
|
22
|
+
def current(self) -> int:
|
23
|
+
return self._current
|
24
|
+
|
25
|
+
def ensure_total(self, total: int) -> None:
|
26
|
+
if self._total is None or total > self._total:
|
27
|
+
self._total = total
|
28
|
+
|
29
|
+
def advance(self, amount: int) -> None:
|
30
|
+
if amount > 0:
|
31
|
+
self._current += amount
|
32
|
+
|
33
|
+
def finish(self) -> None:
|
34
|
+
if self._total is not None:
|
35
|
+
self._current = self._total
|
36
|
+
if self._desc:
|
37
|
+
self._log_callback(f"{self._desc} completed")
|
38
|
+
|
39
|
+
def __enter__(self) -> "_GuiProgressHandle":
|
40
|
+
return self
|
41
|
+
|
42
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
43
|
+
if exc_type is None:
|
44
|
+
self.finish()
|
45
|
+
return False
|
46
|
+
|
47
|
+
|
48
|
+
class _TkProgressReporter(SignalProgressReporter):
|
49
|
+
"""Progress reporter that forwards updates to the GUI thread."""
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
log_callback: Callable[[str], None],
|
54
|
+
process_callback: Optional[Callable] = None,
|
55
|
+
*,
|
56
|
+
stop_callback: Optional[Callable[[], bool]] = None,
|
57
|
+
) -> None:
|
58
|
+
self._log_callback = log_callback
|
59
|
+
self.process_callback = process_callback
|
60
|
+
self._stop_callback = stop_callback
|
61
|
+
|
62
|
+
def log(self, message: str) -> None:
|
63
|
+
self._log_callback(message)
|
64
|
+
print(message, flush=True)
|
65
|
+
|
66
|
+
def task(
|
67
|
+
self, *, desc: str = "", total: Optional[int] = None, unit: str = ""
|
68
|
+
) -> _GuiProgressHandle:
|
69
|
+
del total, unit
|
70
|
+
return _GuiProgressHandle(self._log_callback, desc)
|
71
|
+
|
72
|
+
def stop_requested(self) -> bool:
|
73
|
+
"""Return ``True`` when the GUI has asked to cancel processing."""
|
74
|
+
|
75
|
+
if self._stop_callback is None:
|
76
|
+
return False
|
77
|
+
return bool(self._stop_callback())
|
78
|
+
|
79
|
+
|
80
|
+
__all__ = ["_GuiProgressHandle", "_TkProgressReporter"]
|
talks_reducer/gui/remote.py
CHANGED
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional
|
|
14
14
|
from ..pipeline import ProcessingAborted
|
15
15
|
|
16
16
|
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
|
17
|
-
from . import TalksReducerGUI
|
17
|
+
from .app import TalksReducerGUI
|
18
18
|
|
19
19
|
|
20
20
|
def normalize_server_url(server_url: str) -> str:
|
@@ -219,6 +219,12 @@ def check_remote_server_for_gui(
|
|
219
219
|
)
|
220
220
|
|
221
221
|
|
222
|
+
def _load_service_client() -> object:
|
223
|
+
"""Return the Talks Reducer service client module."""
|
224
|
+
|
225
|
+
return importlib.import_module("talks_reducer.service_client")
|
226
|
+
|
227
|
+
|
222
228
|
def process_files_via_server(
|
223
229
|
gui: "TalksReducerGUI",
|
224
230
|
files: List[str],
|
@@ -228,6 +234,8 @@ def process_files_via_server(
|
|
228
234
|
open_after_convert: bool,
|
229
235
|
default_remote_destination: Callable[[Path, bool], Path],
|
230
236
|
parse_summary: Callable[[str], tuple[Optional[float], Optional[float]]],
|
237
|
+
load_service_client: Callable[[], object] = _load_service_client,
|
238
|
+
check_server: Callable[..., bool] = check_remote_server_for_gui,
|
231
239
|
) -> bool:
|
232
240
|
"""Send *files* to the configured server for processing."""
|
233
241
|
|
@@ -236,7 +244,7 @@ def process_files_via_server(
|
|
236
244
|
raise ProcessingAborted("Remote processing cancelled by user.")
|
237
245
|
|
238
246
|
try:
|
239
|
-
service_module =
|
247
|
+
service_module = load_service_client()
|
240
248
|
except ModuleNotFoundError as exc:
|
241
249
|
gui._append_log(f"Server client unavailable: {exc}")
|
242
250
|
gui._schedule_on_ui_thread(
|
@@ -253,7 +261,7 @@ def process_files_via_server(
|
|
253
261
|
lambda: gui._set_status("waiting", f"Waiting server {host_label}...")
|
254
262
|
)
|
255
263
|
|
256
|
-
available =
|
264
|
+
available = check_server(
|
257
265
|
gui,
|
258
266
|
server_url,
|
259
267
|
success_status="waiting",
|
@@ -0,0 +1,202 @@
|
|
1
|
+
"""Startup utilities for launching the Talks Reducer GUI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import importlib
|
7
|
+
import json
|
8
|
+
import subprocess
|
9
|
+
import sys
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Optional, Sequence, Tuple
|
12
|
+
|
13
|
+
from ..cli import main as cli_main
|
14
|
+
from .app import TalksReducerGUI
|
15
|
+
|
16
|
+
|
17
|
+
def _check_tkinter_available() -> Tuple[bool, str]:
|
18
|
+
"""Check if tkinter can create windows without importing it globally."""
|
19
|
+
|
20
|
+
# Test in a subprocess to avoid crashing the main process
|
21
|
+
test_code = """
|
22
|
+
import json
|
23
|
+
|
24
|
+
def run_check():
|
25
|
+
try:
|
26
|
+
import tkinter as tk # noqa: F401 - imported for side effect
|
27
|
+
except Exception as exc: # pragma: no cover - runs in subprocess
|
28
|
+
return {
|
29
|
+
"status": "import_error",
|
30
|
+
"error": f"{exc.__class__.__name__}: {exc}",
|
31
|
+
}
|
32
|
+
|
33
|
+
try:
|
34
|
+
import tkinter as tk
|
35
|
+
|
36
|
+
root = tk.Tk()
|
37
|
+
root.destroy()
|
38
|
+
except Exception as exc: # pragma: no cover - runs in subprocess
|
39
|
+
return {
|
40
|
+
"status": "init_error",
|
41
|
+
"error": f"{exc.__class__.__name__}: {exc}",
|
42
|
+
}
|
43
|
+
|
44
|
+
return {"status": "ok"}
|
45
|
+
|
46
|
+
|
47
|
+
if __name__ == "__main__":
|
48
|
+
print(json.dumps(run_check()))
|
49
|
+
"""
|
50
|
+
|
51
|
+
try:
|
52
|
+
result = subprocess.run(
|
53
|
+
[sys.executable, "-c", test_code], capture_output=True, text=True, timeout=5
|
54
|
+
)
|
55
|
+
|
56
|
+
output = result.stdout.strip() or result.stderr.strip()
|
57
|
+
|
58
|
+
if not output:
|
59
|
+
return False, "Window creation failed"
|
60
|
+
|
61
|
+
try:
|
62
|
+
payload = json.loads(output)
|
63
|
+
except json.JSONDecodeError:
|
64
|
+
return False, output
|
65
|
+
|
66
|
+
status = payload.get("status")
|
67
|
+
|
68
|
+
if status == "ok":
|
69
|
+
return True, ""
|
70
|
+
|
71
|
+
if status == "import_error":
|
72
|
+
return (
|
73
|
+
False,
|
74
|
+
f"tkinter is not installed ({payload.get('error', 'unknown error')})",
|
75
|
+
)
|
76
|
+
|
77
|
+
if status == "init_error":
|
78
|
+
return (
|
79
|
+
False,
|
80
|
+
f"tkinter could not open a window ({payload.get('error', 'unknown error')})",
|
81
|
+
)
|
82
|
+
|
83
|
+
return False, output
|
84
|
+
except Exception as e: # pragma: no cover - defensive fallback
|
85
|
+
return False, f"Error testing tkinter: {e}"
|
86
|
+
|
87
|
+
|
88
|
+
def main(argv: Optional[Sequence[str]] = None) -> bool:
|
89
|
+
"""Launch the GUI when run without arguments, otherwise defer to the CLI."""
|
90
|
+
|
91
|
+
if argv is None:
|
92
|
+
argv = sys.argv[1:]
|
93
|
+
|
94
|
+
parser = argparse.ArgumentParser(add_help=False)
|
95
|
+
parser.add_argument(
|
96
|
+
"--server",
|
97
|
+
action="store_true",
|
98
|
+
help="Launch the Talks Reducer server tray instead of the desktop GUI.",
|
99
|
+
)
|
100
|
+
parser.add_argument(
|
101
|
+
"--no-tray",
|
102
|
+
action="store_true",
|
103
|
+
help="Deprecated: the GUI no longer starts the server tray automatically.",
|
104
|
+
)
|
105
|
+
|
106
|
+
parsed_args, remaining = parser.parse_known_args(argv)
|
107
|
+
if parsed_args.server:
|
108
|
+
package_name = __package__ or "talks_reducer"
|
109
|
+
module_name = f"{package_name}.server_tray"
|
110
|
+
try:
|
111
|
+
tray_module = importlib.import_module(module_name)
|
112
|
+
except ModuleNotFoundError as exc:
|
113
|
+
if exc.name != module_name:
|
114
|
+
raise
|
115
|
+
root_package = package_name.split(".")[0] or "talks_reducer"
|
116
|
+
tray_module = importlib.import_module(f"{root_package}.server_tray")
|
117
|
+
tray_main = getattr(tray_module, "main")
|
118
|
+
tray_main(remaining)
|
119
|
+
return False
|
120
|
+
if parsed_args.no_tray:
|
121
|
+
sys.stderr.write(
|
122
|
+
"Warning: --no-tray is deprecated; the GUI no longer starts the server tray automatically.\n"
|
123
|
+
)
|
124
|
+
argv = remaining
|
125
|
+
|
126
|
+
if argv:
|
127
|
+
launch_gui = False
|
128
|
+
if sys.platform == "win32" and not any(arg.startswith("-") for arg in argv):
|
129
|
+
if any(Path(arg).exists() for arg in argv if arg):
|
130
|
+
launch_gui = True
|
131
|
+
|
132
|
+
if launch_gui:
|
133
|
+
try:
|
134
|
+
app = TalksReducerGUI(argv, auto_run=True)
|
135
|
+
app.run()
|
136
|
+
return True
|
137
|
+
except Exception:
|
138
|
+
# Fall back to the CLI if the GUI cannot be started.
|
139
|
+
pass
|
140
|
+
|
141
|
+
cli_main(argv)
|
142
|
+
return False
|
143
|
+
|
144
|
+
is_frozen = getattr(sys, "frozen", False)
|
145
|
+
|
146
|
+
if not is_frozen:
|
147
|
+
tkinter_available, error_msg = _check_tkinter_available()
|
148
|
+
|
149
|
+
if not tkinter_available:
|
150
|
+
try:
|
151
|
+
print("Talks Reducer GUI")
|
152
|
+
print("=" * 50)
|
153
|
+
print("X GUI not available on this system")
|
154
|
+
print(f"Error: {error_msg}")
|
155
|
+
print()
|
156
|
+
print("! Alternative: Use the command-line interface")
|
157
|
+
print()
|
158
|
+
print("The CLI provides all the same functionality:")
|
159
|
+
print(" python3 -m talks_reducer <input_file> [options]")
|
160
|
+
print()
|
161
|
+
print("Examples:")
|
162
|
+
print(" python3 -m talks_reducer video.mp4")
|
163
|
+
print(" python3 -m talks_reducer video.mp4 --small")
|
164
|
+
print(" python3 -m talks_reducer video.mp4 -o output.mp4")
|
165
|
+
print()
|
166
|
+
print("Run 'python3 -m talks_reducer --help' for all options.")
|
167
|
+
print()
|
168
|
+
print("Troubleshooting tips:")
|
169
|
+
if sys.platform == "darwin":
|
170
|
+
print(
|
171
|
+
" - On macOS, install Python from python.org or ensure "
|
172
|
+
"Homebrew's python-tk package is present."
|
173
|
+
)
|
174
|
+
elif sys.platform.startswith("linux"):
|
175
|
+
print(
|
176
|
+
" - On Linux, install the Tk bindings for Python (for example, "
|
177
|
+
"python3-tk)."
|
178
|
+
)
|
179
|
+
else:
|
180
|
+
print(" - Ensure your Python installation includes Tk support.")
|
181
|
+
print(" - You can always fall back to the CLI workflow below.")
|
182
|
+
print()
|
183
|
+
print("The CLI interface works perfectly and is recommended.")
|
184
|
+
except UnicodeEncodeError:
|
185
|
+
sys.stderr.write("GUI not available. Use CLI mode instead.\n")
|
186
|
+
return False
|
187
|
+
|
188
|
+
try:
|
189
|
+
app = TalksReducerGUI()
|
190
|
+
app.run()
|
191
|
+
return True
|
192
|
+
except Exception as e:
|
193
|
+
import traceback
|
194
|
+
|
195
|
+
sys.stderr.write(f"Error starting GUI: {e}\n")
|
196
|
+
sys.stderr.write(traceback.format_exc())
|
197
|
+
sys.stderr.write("\nPlease use the CLI mode instead:\n")
|
198
|
+
sys.stderr.write(" python3 -m talks_reducer <input_file> [options]\n")
|
199
|
+
sys.exit(1)
|
200
|
+
|
201
|
+
|
202
|
+
__all__ = ["_check_tkinter_available", "main"]
|
talks_reducer/icons.py
CHANGED
@@ -11,6 +11,7 @@ from typing import Iterator, Optional, Sequence
|
|
11
11
|
LOGGER = logging.getLogger(__name__)
|
12
12
|
|
13
13
|
_ICON_RELATIVE_PATHS: Sequence[Path] = (
|
14
|
+
Path("resources") / "icons",
|
14
15
|
Path("talks_reducer") / "resources" / "icons",
|
15
16
|
Path("docs") / "assets",
|
16
17
|
)
|
@@ -43,6 +44,7 @@ def _iter_base_roots(module_file: Optional[Path | str] = None) -> Iterator[Path]
|
|
43
44
|
for root in (
|
44
45
|
package_root,
|
45
46
|
project_root,
|
47
|
+
Path.cwd(),
|
46
48
|
):
|
47
49
|
yield from _yield(root)
|
48
50
|
|
talks_reducer/pipeline.py
CHANGED
@@ -6,12 +6,15 @@ import math
|
|
6
6
|
import os
|
7
7
|
import re
|
8
8
|
import subprocess
|
9
|
+
from dataclasses import dataclass
|
9
10
|
from pathlib import Path
|
10
|
-
from typing import Dict
|
11
|
+
from typing import Callable, Dict
|
11
12
|
|
12
13
|
import numpy as np
|
13
14
|
from scipy.io import wavfile
|
14
15
|
|
16
|
+
from talks_reducer.version_utils import resolve_version
|
17
|
+
|
15
18
|
from . import audio as audio_utils
|
16
19
|
from . import chunks as chunk_utils
|
17
20
|
from .ffmpeg import (
|
@@ -23,13 +26,33 @@ from .ffmpeg import (
|
|
23
26
|
)
|
24
27
|
from .models import ProcessingOptions, ProcessingResult
|
25
28
|
from .progress import NullProgressReporter, ProgressReporter
|
26
|
-
from talks_reducer.version_utils import resolve_version
|
27
29
|
|
28
30
|
|
29
31
|
class ProcessingAborted(RuntimeError):
|
30
32
|
"""Raised when processing is cancelled by the caller."""
|
31
33
|
|
32
34
|
|
35
|
+
@dataclass
|
36
|
+
class PipelineDependencies:
|
37
|
+
"""Bundle of external dependencies used by :func:`speed_up_video`."""
|
38
|
+
|
39
|
+
get_ffmpeg_path: Callable[[], str] = get_ffmpeg_path
|
40
|
+
check_cuda_available: Callable[[str], bool] = check_cuda_available
|
41
|
+
build_extract_audio_command: Callable[..., str] = build_extract_audio_command
|
42
|
+
build_video_commands: Callable[..., tuple[str, str | None, bool]] = (
|
43
|
+
build_video_commands
|
44
|
+
)
|
45
|
+
run_timed_ffmpeg_command: Callable[..., None] = run_timed_ffmpeg_command
|
46
|
+
create_path: Callable[[Path], None] | None = None
|
47
|
+
delete_path: Callable[[Path], None] | None = None
|
48
|
+
|
49
|
+
def __post_init__(self) -> None:
|
50
|
+
if self.create_path is None:
|
51
|
+
self.create_path = _create_path
|
52
|
+
if self.delete_path is None:
|
53
|
+
self.delete_path = _delete_path
|
54
|
+
|
55
|
+
|
33
56
|
def _stop_requested(reporter: ProgressReporter | None) -> bool:
|
34
57
|
"""Return ``True`` when *reporter* indicates that processing should stop."""
|
35
58
|
|
@@ -46,7 +69,10 @@ def _stop_requested(reporter: ProgressReporter | None) -> bool:
|
|
46
69
|
|
47
70
|
|
48
71
|
def _raise_if_stopped(
|
49
|
-
reporter: ProgressReporter | None,
|
72
|
+
reporter: ProgressReporter | None,
|
73
|
+
*,
|
74
|
+
temp_path: Path | None = None,
|
75
|
+
dependencies: PipelineDependencies | None = None,
|
50
76
|
) -> None:
|
51
77
|
"""Abort processing when the user has requested a stop."""
|
52
78
|
|
@@ -54,34 +80,40 @@ def _raise_if_stopped(
|
|
54
80
|
return
|
55
81
|
|
56
82
|
if temp_path is not None and temp_path.exists():
|
57
|
-
|
83
|
+
if dependencies is not None:
|
84
|
+
dependencies.delete_path(temp_path)
|
85
|
+
else:
|
86
|
+
_delete_path(temp_path)
|
58
87
|
raise ProcessingAborted("Processing aborted by user request.")
|
59
88
|
|
60
89
|
|
61
90
|
def speed_up_video(
|
62
|
-
options: ProcessingOptions,
|
91
|
+
options: ProcessingOptions,
|
92
|
+
reporter: ProgressReporter | None = None,
|
93
|
+
dependencies: PipelineDependencies | None = None,
|
63
94
|
) -> ProcessingResult:
|
64
95
|
"""Speed up a video by shortening silent sections while keeping sounded sections intact."""
|
65
96
|
|
66
97
|
reporter = reporter or NullProgressReporter()
|
98
|
+
dependencies = dependencies or PipelineDependencies()
|
67
99
|
|
68
100
|
input_path = Path(options.input_file)
|
69
101
|
if not input_path.exists():
|
70
102
|
raise FileNotFoundError(f"Input file not found: {input_path}")
|
71
103
|
|
72
|
-
ffmpeg_path = get_ffmpeg_path()
|
104
|
+
ffmpeg_path = dependencies.get_ffmpeg_path()
|
73
105
|
|
74
106
|
output_path = options.output_file or _input_to_output_filename(
|
75
107
|
input_path, options.small
|
76
108
|
)
|
77
109
|
output_path = Path(output_path)
|
78
110
|
|
79
|
-
cuda_available = check_cuda_available(ffmpeg_path)
|
111
|
+
cuda_available = dependencies.check_cuda_available(ffmpeg_path)
|
80
112
|
|
81
113
|
temp_path = Path(options.temp_folder)
|
82
114
|
if temp_path.exists():
|
83
|
-
|
84
|
-
|
115
|
+
dependencies.delete_path(temp_path)
|
116
|
+
dependencies.create_path(temp_path)
|
85
117
|
|
86
118
|
metadata = _extract_video_metadata(input_path, options.frame_rate)
|
87
119
|
frame_rate = metadata["frame_rate"]
|
@@ -117,7 +149,7 @@ def speed_up_video(
|
|
117
149
|
|
118
150
|
extraction_sample_rate = options.sample_rate
|
119
151
|
|
120
|
-
extract_command = build_extract_audio_command(
|
152
|
+
extract_command = dependencies.build_extract_audio_command(
|
121
153
|
os.fspath(input_path),
|
122
154
|
os.fspath(audio_wav),
|
123
155
|
extraction_sample_rate,
|
@@ -126,7 +158,7 @@ def speed_up_video(
|
|
126
158
|
ffmpeg_path=ffmpeg_path,
|
127
159
|
)
|
128
160
|
|
129
|
-
_raise_if_stopped(reporter, temp_path=temp_path)
|
161
|
+
_raise_if_stopped(reporter, temp_path=temp_path, dependencies=dependencies)
|
130
162
|
reporter.log("Extracting audio...")
|
131
163
|
process_callback = getattr(reporter, "process_callback", None)
|
132
164
|
estimated_total_frames = frame_count
|
@@ -138,7 +170,7 @@ def speed_up_video(
|
|
138
170
|
else:
|
139
171
|
reporter.log("Extract audio target frames: unknown")
|
140
172
|
|
141
|
-
run_timed_ffmpeg_command(
|
173
|
+
dependencies.run_timed_ffmpeg_command(
|
142
174
|
extract_command,
|
143
175
|
reporter=reporter,
|
144
176
|
total=estimated_total_frames if estimated_total_frames > 0 else None,
|
@@ -157,7 +189,7 @@ def speed_up_video(
|
|
157
189
|
samples_per_frame = wav_sample_rate / frame_rate
|
158
190
|
audio_frame_count = int(math.ceil(audio_sample_count / samples_per_frame))
|
159
191
|
|
160
|
-
_raise_if_stopped(reporter, temp_path=temp_path)
|
192
|
+
_raise_if_stopped(reporter, temp_path=temp_path, dependencies=dependencies)
|
161
193
|
|
162
194
|
has_loud_audio = chunk_utils.detect_loud_frames(
|
163
195
|
audio_data,
|
@@ -171,7 +203,7 @@ def speed_up_video(
|
|
171
203
|
|
172
204
|
reporter.log(f"Processing {len(chunks)} chunks...")
|
173
205
|
|
174
|
-
_raise_if_stopped(reporter, temp_path=temp_path)
|
206
|
+
_raise_if_stopped(reporter, temp_path=temp_path, dependencies=dependencies)
|
175
207
|
|
176
208
|
new_speeds = [options.silent_speed, options.sounded_speed]
|
177
209
|
output_audio_data, updated_chunks = audio_utils.process_audio_chunks(
|
@@ -192,7 +224,7 @@ def speed_up_video(
|
|
192
224
|
_prepare_output_audio(output_audio_data),
|
193
225
|
)
|
194
226
|
|
195
|
-
_raise_if_stopped(reporter, temp_path=temp_path)
|
227
|
+
_raise_if_stopped(reporter, temp_path=temp_path, dependencies=dependencies)
|
196
228
|
|
197
229
|
expression = chunk_utils.get_tree_expression(updated_chunks)
|
198
230
|
filter_graph_path = temp_path / "filterGraph.txt"
|
@@ -205,14 +237,16 @@ def speed_up_video(
|
|
205
237
|
filter_parts.append(f"setpts={escaped_expression}")
|
206
238
|
filter_graph_file.write(",".join(filter_parts))
|
207
239
|
|
208
|
-
command_str, fallback_command_str, use_cuda_encoder =
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
240
|
+
command_str, fallback_command_str, use_cuda_encoder = (
|
241
|
+
dependencies.build_video_commands(
|
242
|
+
os.fspath(input_path),
|
243
|
+
os.fspath(audio_new_path),
|
244
|
+
os.fspath(filter_graph_path),
|
245
|
+
os.fspath(output_path),
|
246
|
+
ffmpeg_path=ffmpeg_path,
|
247
|
+
cuda_available=cuda_available,
|
248
|
+
small=options.small,
|
249
|
+
)
|
216
250
|
)
|
217
251
|
|
218
252
|
output_dir = output_path.parent.resolve()
|
@@ -224,14 +258,14 @@ def speed_up_video(
|
|
224
258
|
reporter.log(command_str)
|
225
259
|
|
226
260
|
if not audio_new_path.exists():
|
227
|
-
|
261
|
+
dependencies.delete_path(temp_path)
|
228
262
|
raise FileNotFoundError("Audio intermediate file was not generated")
|
229
263
|
|
230
264
|
if not filter_graph_path.exists():
|
231
|
-
|
265
|
+
dependencies.delete_path(temp_path)
|
232
266
|
raise FileNotFoundError("Filter graph file was not generated")
|
233
267
|
|
234
|
-
_raise_if_stopped(reporter, temp_path=temp_path)
|
268
|
+
_raise_if_stopped(reporter, temp_path=temp_path, dependencies=dependencies)
|
235
269
|
|
236
270
|
try:
|
237
271
|
final_total_frames = updated_chunks[-1][3] if updated_chunks else 0
|
@@ -253,7 +287,7 @@ def speed_up_video(
|
|
253
287
|
|
254
288
|
total_frames_arg = final_total_frames if final_total_frames > 0 else None
|
255
289
|
|
256
|
-
run_timed_ffmpeg_command(
|
290
|
+
dependencies.run_timed_ffmpeg_command(
|
257
291
|
command_str,
|
258
292
|
reporter=reporter,
|
259
293
|
total=total_frames_arg,
|
@@ -261,9 +295,9 @@ def speed_up_video(
|
|
261
295
|
desc="Generating final:",
|
262
296
|
process_callback=process_callback,
|
263
297
|
)
|
264
|
-
except subprocess.CalledProcessError
|
298
|
+
except subprocess.CalledProcessError:
|
265
299
|
if fallback_command_str and use_cuda_encoder:
|
266
|
-
_raise_if_stopped(reporter, temp_path=temp_path)
|
300
|
+
_raise_if_stopped(reporter, temp_path=temp_path, dependencies=dependencies)
|
267
301
|
|
268
302
|
reporter.log("CUDA encoding failed, retrying with CPU encoder...")
|
269
303
|
if final_total_frames > 0:
|
@@ -281,7 +315,7 @@ def speed_up_video(
|
|
281
315
|
fps=frame_rate,
|
282
316
|
)
|
283
317
|
)
|
284
|
-
run_timed_ffmpeg_command(
|
318
|
+
dependencies.run_timed_ffmpeg_command(
|
285
319
|
fallback_command_str,
|
286
320
|
reporter=reporter,
|
287
321
|
total=total_frames_arg,
|
@@ -292,7 +326,7 @@ def speed_up_video(
|
|
292
326
|
else:
|
293
327
|
raise
|
294
328
|
finally:
|
295
|
-
|
329
|
+
dependencies.delete_path(temp_path)
|
296
330
|
|
297
331
|
output_metadata = _extract_video_metadata(output_path, frame_rate)
|
298
332
|
output_duration = output_metadata.get("duration", 0.0)
|
Binary file
|
Binary file
|
Binary file
|