talks-reducer 0.6.1__py3-none-any.whl → 0.7.0__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 +9 -3
- talks_reducer/{gui.py → gui/__init__.py} +384 -1216
- talks_reducer/gui/__main__.py +8 -0
- talks_reducer/gui/discovery.py +126 -0
- talks_reducer/gui/layout.py +526 -0
- talks_reducer/gui/preferences.py +113 -0
- talks_reducer/gui/remote.py +356 -0
- talks_reducer/gui/theme.py +269 -0
- talks_reducer/models.py +1 -1
- talks_reducer/pipeline.py +142 -92
- talks_reducer/resources/__init__.py +0 -0
- talks_reducer/server.py +60 -6
- talks_reducer/server_tray.py +4 -6
- talks_reducer/service_client.py +56 -4
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/METADATA +23 -13
- talks_reducer-0.7.0.dist-info/RECORD +29 -0
- talks_reducer-0.6.1.dist-info/RECORD +0 -22
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/WHEEL +0 -0
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,269 @@
|
|
1
|
+
"""Theme utilities shared by the Tkinter GUI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import subprocess
|
6
|
+
from typing import Any, Callable, Mapping, Sequence
|
7
|
+
|
8
|
+
STATUS_COLORS = {
|
9
|
+
"idle": "#9ca3af",
|
10
|
+
"waiting": "#9ca3af",
|
11
|
+
"processing": "#af8e0e",
|
12
|
+
"success": "#178941",
|
13
|
+
"error": "#ad4f4f",
|
14
|
+
"aborted": "#6d727a",
|
15
|
+
}
|
16
|
+
|
17
|
+
LIGHT_THEME = {
|
18
|
+
"background": "#f5f5f5",
|
19
|
+
"foreground": "#1f2933",
|
20
|
+
"accent": "#2563eb",
|
21
|
+
"surface": "#ffffff",
|
22
|
+
"border": "#cbd5e1",
|
23
|
+
"hover": "#efefef",
|
24
|
+
"hover_text": "#000000",
|
25
|
+
"selection_background": "#2563eb",
|
26
|
+
"selection_foreground": "#ffffff",
|
27
|
+
}
|
28
|
+
|
29
|
+
DARK_THEME = {
|
30
|
+
"background": "#1e1e28",
|
31
|
+
"foreground": "#f3f4f6",
|
32
|
+
"accent": "#60a5fa",
|
33
|
+
"surface": "#2b2b3c",
|
34
|
+
"border": "#4b5563",
|
35
|
+
"hover": "#333333",
|
36
|
+
"hover_text": "#ffffff",
|
37
|
+
"selection_background": "#333333",
|
38
|
+
"selection_foreground": "#f3f4f6",
|
39
|
+
}
|
40
|
+
|
41
|
+
|
42
|
+
RegistryReader = Callable[[str, str], int]
|
43
|
+
DefaultsRunner = Callable[[Sequence[str]], subprocess.CompletedProcess[str]]
|
44
|
+
|
45
|
+
|
46
|
+
def read_windows_theme_registry(key_path: str, value_name: str) -> int:
|
47
|
+
"""Read *value_name* from the registry key at *key_path*."""
|
48
|
+
|
49
|
+
import winreg # type: ignore
|
50
|
+
|
51
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key:
|
52
|
+
value, _ = winreg.QueryValueEx(key, value_name)
|
53
|
+
return int(value)
|
54
|
+
|
55
|
+
|
56
|
+
def run_defaults_command(args: Sequence[str]) -> subprocess.CompletedProcess[str]:
|
57
|
+
"""Execute the macOS ``defaults`` command used to detect theme."""
|
58
|
+
|
59
|
+
return subprocess.run(args, capture_output=True, text=True, check=False)
|
60
|
+
|
61
|
+
|
62
|
+
def detect_system_theme(
|
63
|
+
env: Mapping[str, str],
|
64
|
+
platform: str,
|
65
|
+
registry_reader: RegistryReader,
|
66
|
+
defaults_runner: DefaultsRunner,
|
67
|
+
) -> str:
|
68
|
+
"""Detect the system theme for the provided *platform* and environment."""
|
69
|
+
|
70
|
+
if platform.startswith("win"):
|
71
|
+
try:
|
72
|
+
value = registry_reader(
|
73
|
+
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
74
|
+
"AppsUseLightTheme",
|
75
|
+
)
|
76
|
+
return "light" if int(value) else "dark"
|
77
|
+
except OSError:
|
78
|
+
return "light"
|
79
|
+
|
80
|
+
if platform == "darwin":
|
81
|
+
try:
|
82
|
+
result = defaults_runner(["defaults", "read", "-g", "AppleInterfaceStyle"])
|
83
|
+
except Exception:
|
84
|
+
return "light"
|
85
|
+
if result.returncode == 0 and result.stdout.strip().lower() == "dark":
|
86
|
+
return "dark"
|
87
|
+
return "light"
|
88
|
+
|
89
|
+
theme = env.get("GTK_THEME", "").lower()
|
90
|
+
if "dark" in theme:
|
91
|
+
return "dark"
|
92
|
+
return "light"
|
93
|
+
|
94
|
+
|
95
|
+
def apply_theme(
|
96
|
+
style: Any,
|
97
|
+
palette: Mapping[str, str],
|
98
|
+
widgets: Mapping[str, Any],
|
99
|
+
) -> Mapping[str, str]:
|
100
|
+
"""Apply *palette* to *style* and update GUI *widgets*."""
|
101
|
+
|
102
|
+
root = widgets.get("root")
|
103
|
+
if root is not None:
|
104
|
+
root.configure(bg=palette["background"])
|
105
|
+
|
106
|
+
style.theme_use("clam")
|
107
|
+
style.configure(
|
108
|
+
".", background=palette["background"], foreground=palette["foreground"]
|
109
|
+
)
|
110
|
+
style.configure("TFrame", background=palette["background"])
|
111
|
+
style.configure(
|
112
|
+
"TLabelframe",
|
113
|
+
background=palette["background"],
|
114
|
+
foreground=palette["foreground"],
|
115
|
+
borderwidth=0,
|
116
|
+
relief="flat",
|
117
|
+
)
|
118
|
+
style.configure(
|
119
|
+
"TLabelframe.Label",
|
120
|
+
background=palette["background"],
|
121
|
+
foreground=palette["foreground"],
|
122
|
+
)
|
123
|
+
style.configure(
|
124
|
+
"TLabel", background=palette["background"], foreground=palette["foreground"]
|
125
|
+
)
|
126
|
+
style.configure(
|
127
|
+
"TCheckbutton",
|
128
|
+
background=palette["background"],
|
129
|
+
foreground=palette["foreground"],
|
130
|
+
)
|
131
|
+
style.map(
|
132
|
+
"TCheckbutton",
|
133
|
+
background=[("active", palette.get("hover", palette["background"]))],
|
134
|
+
)
|
135
|
+
style.configure(
|
136
|
+
"TRadiobutton",
|
137
|
+
background=palette["background"],
|
138
|
+
foreground=palette["foreground"],
|
139
|
+
)
|
140
|
+
style.map(
|
141
|
+
"TRadiobutton",
|
142
|
+
background=[("active", palette.get("hover", palette["background"]))],
|
143
|
+
)
|
144
|
+
style.configure(
|
145
|
+
"Link.TButton",
|
146
|
+
background=palette["background"],
|
147
|
+
foreground=palette["accent"],
|
148
|
+
borderwidth=0,
|
149
|
+
relief="flat",
|
150
|
+
highlightthickness=0,
|
151
|
+
padding=2,
|
152
|
+
font=("TkDefaultFont", 8, "underline"),
|
153
|
+
)
|
154
|
+
style.map(
|
155
|
+
"Link.TButton",
|
156
|
+
background=[
|
157
|
+
("active", palette.get("hover", palette["background"])),
|
158
|
+
("disabled", palette["background"]),
|
159
|
+
],
|
160
|
+
foreground=[
|
161
|
+
("active", palette.get("accent", palette["foreground"])),
|
162
|
+
("disabled", palette["foreground"]),
|
163
|
+
],
|
164
|
+
)
|
165
|
+
style.configure(
|
166
|
+
"TButton",
|
167
|
+
background=palette["surface"],
|
168
|
+
foreground=palette["foreground"],
|
169
|
+
padding=4,
|
170
|
+
font=("TkDefaultFont", 8),
|
171
|
+
)
|
172
|
+
style.map(
|
173
|
+
"TButton",
|
174
|
+
background=[
|
175
|
+
("active", palette.get("hover", palette["accent"])),
|
176
|
+
("disabled", palette["surface"]),
|
177
|
+
],
|
178
|
+
foreground=[
|
179
|
+
("active", palette.get("hover_text", "#000000")),
|
180
|
+
("disabled", palette["foreground"]),
|
181
|
+
],
|
182
|
+
)
|
183
|
+
style.configure(
|
184
|
+
"TEntry",
|
185
|
+
fieldbackground=palette["surface"],
|
186
|
+
foreground=palette["foreground"],
|
187
|
+
)
|
188
|
+
style.configure(
|
189
|
+
"TCombobox",
|
190
|
+
fieldbackground=palette["surface"],
|
191
|
+
foreground=palette["foreground"],
|
192
|
+
)
|
193
|
+
|
194
|
+
style.configure(
|
195
|
+
"Idle.Horizontal.TProgressbar",
|
196
|
+
background=STATUS_COLORS["idle"],
|
197
|
+
troughcolor=palette["surface"],
|
198
|
+
borderwidth=0,
|
199
|
+
thickness=20,
|
200
|
+
)
|
201
|
+
style.configure(
|
202
|
+
"Processing.Horizontal.TProgressbar",
|
203
|
+
background=STATUS_COLORS["processing"],
|
204
|
+
troughcolor=palette["surface"],
|
205
|
+
borderwidth=0,
|
206
|
+
thickness=20,
|
207
|
+
)
|
208
|
+
style.configure(
|
209
|
+
"Success.Horizontal.TProgressbar",
|
210
|
+
background=STATUS_COLORS["success"],
|
211
|
+
troughcolor=palette["surface"],
|
212
|
+
borderwidth=0,
|
213
|
+
thickness=20,
|
214
|
+
)
|
215
|
+
style.configure(
|
216
|
+
"Error.Horizontal.TProgressbar",
|
217
|
+
background=STATUS_COLORS["error"],
|
218
|
+
troughcolor=palette["surface"],
|
219
|
+
borderwidth=0,
|
220
|
+
thickness=20,
|
221
|
+
)
|
222
|
+
style.configure(
|
223
|
+
"Aborted.Horizontal.TProgressbar",
|
224
|
+
background=STATUS_COLORS["aborted"],
|
225
|
+
troughcolor=palette["surface"],
|
226
|
+
borderwidth=0,
|
227
|
+
thickness=20,
|
228
|
+
)
|
229
|
+
|
230
|
+
drop_zone = widgets.get("drop_zone")
|
231
|
+
if drop_zone is not None:
|
232
|
+
drop_zone.configure(
|
233
|
+
bg=palette["surface"],
|
234
|
+
fg=palette["foreground"],
|
235
|
+
highlightthickness=0,
|
236
|
+
)
|
237
|
+
|
238
|
+
tk_module = widgets.get("tk")
|
239
|
+
slider_relief = getattr(tk_module, "FLAT", "flat") if tk_module else "flat"
|
240
|
+
sliders = widgets.get("sliders") or []
|
241
|
+
for slider in sliders:
|
242
|
+
slider.configure(
|
243
|
+
background=palette["border"],
|
244
|
+
troughcolor=palette["surface"],
|
245
|
+
activebackground=palette["border"],
|
246
|
+
sliderrelief=slider_relief,
|
247
|
+
bd=0,
|
248
|
+
)
|
249
|
+
|
250
|
+
log_text = widgets.get("log_text")
|
251
|
+
if log_text is not None:
|
252
|
+
log_text.configure(
|
253
|
+
bg=palette["surface"],
|
254
|
+
fg=palette["foreground"],
|
255
|
+
insertbackground=palette["foreground"],
|
256
|
+
highlightbackground=palette["border"],
|
257
|
+
highlightcolor=palette["border"],
|
258
|
+
)
|
259
|
+
|
260
|
+
status_label = widgets.get("status_label")
|
261
|
+
if status_label is not None:
|
262
|
+
status_label.configure(bg=palette["background"])
|
263
|
+
|
264
|
+
apply_status_style = widgets.get("apply_status_style")
|
265
|
+
status_state = widgets.get("status_state")
|
266
|
+
if callable(apply_status_style) and status_state is not None:
|
267
|
+
apply_status_style(status_state)
|
268
|
+
|
269
|
+
return palette
|
talks_reducer/models.py
CHANGED
talks_reducer/pipeline.py
CHANGED
@@ -23,103 +23,39 @@ from .ffmpeg import (
|
|
23
23
|
)
|
24
24
|
from .models import ProcessingOptions, ProcessingResult
|
25
25
|
from .progress import NullProgressReporter, ProgressReporter
|
26
|
+
from talks_reducer.version_utils import resolve_version
|
26
27
|
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
suffix_parts = []
|
31
|
-
|
32
|
-
if small:
|
33
|
-
suffix_parts.append("_small")
|
34
|
-
|
35
|
-
if not suffix_parts:
|
36
|
-
suffix_parts.append("") # Default case
|
37
|
-
|
38
|
-
suffix = "_speedup" + "".join(suffix_parts)
|
39
|
-
new_name = (
|
40
|
-
filename.name[:dot_index] + suffix + filename.name[dot_index:]
|
41
|
-
if dot_index != -1
|
42
|
-
else filename.name + suffix
|
43
|
-
)
|
44
|
-
return filename.with_name(new_name)
|
45
|
-
|
46
|
-
|
47
|
-
def _create_path(path: Path) -> None:
|
48
|
-
try:
|
49
|
-
path.mkdir(parents=True, exist_ok=True)
|
50
|
-
except OSError as exc: # pragma: no cover - defensive logging
|
51
|
-
raise AssertionError(
|
52
|
-
"Creation of the directory failed. (The TEMP folder may already exist. Delete or rename it, and try again.)"
|
53
|
-
) from exc
|
54
|
-
|
55
|
-
|
56
|
-
def _delete_path(path: Path) -> None:
|
57
|
-
import time
|
58
|
-
from shutil import rmtree
|
59
|
-
|
60
|
-
try:
|
61
|
-
rmtree(path, ignore_errors=False)
|
62
|
-
for i in range(5):
|
63
|
-
if not path.exists():
|
64
|
-
return
|
65
|
-
time.sleep(0.01 * i)
|
66
|
-
except OSError as exc: # pragma: no cover - defensive logging
|
67
|
-
print(f"Deletion of the directory {path} failed")
|
68
|
-
print(exc)
|
69
|
-
|
70
|
-
|
71
|
-
def _extract_video_metadata(input_file: Path, frame_rate: float) -> Dict[str, float]:
|
72
|
-
from .ffmpeg import get_ffprobe_path
|
73
|
-
|
74
|
-
ffprobe_path = get_ffprobe_path()
|
75
|
-
command = [
|
76
|
-
ffprobe_path,
|
77
|
-
"-i",
|
78
|
-
os.fspath(input_file),
|
79
|
-
"-hide_banner",
|
80
|
-
"-loglevel",
|
81
|
-
"error",
|
82
|
-
"-select_streams",
|
83
|
-
"v",
|
84
|
-
"-show_entries",
|
85
|
-
"format=duration:stream=avg_frame_rate,nb_frames",
|
86
|
-
]
|
87
|
-
process = subprocess.Popen(
|
88
|
-
command,
|
89
|
-
stdout=subprocess.PIPE,
|
90
|
-
stderr=subprocess.PIPE,
|
91
|
-
bufsize=1,
|
92
|
-
universal_newlines=True,
|
93
|
-
)
|
94
|
-
stdout, _ = process.communicate()
|
29
|
+
class ProcessingAborted(RuntimeError):
|
30
|
+
"""Raised when processing is cancelled by the caller."""
|
95
31
|
|
96
|
-
match_frame_rate = re.search(r"frame_rate=(\d*)/(\d*)", str(stdout))
|
97
|
-
if match_frame_rate is not None:
|
98
|
-
frame_rate = float(match_frame_rate.group(1)) / float(match_frame_rate.group(2))
|
99
32
|
|
100
|
-
|
101
|
-
|
33
|
+
def _stop_requested(reporter: ProgressReporter | None) -> bool:
|
34
|
+
"""Return ``True`` when *reporter* indicates that processing should stop."""
|
102
35
|
|
103
|
-
|
104
|
-
|
36
|
+
if reporter is None:
|
37
|
+
return False
|
105
38
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
39
|
+
flag = getattr(reporter, "stop_requested", None)
|
40
|
+
if callable(flag):
|
41
|
+
try:
|
42
|
+
flag = flag()
|
43
|
+
except Exception: # pragma: no cover - defensive
|
44
|
+
flag = False
|
45
|
+
return bool(flag)
|
111
46
|
|
112
47
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
116
|
-
|
48
|
+
def _raise_if_stopped(
|
49
|
+
reporter: ProgressReporter | None, *, temp_path: Path | None = None
|
50
|
+
) -> None:
|
51
|
+
"""Abort processing when the user has requested a stop."""
|
117
52
|
|
53
|
+
if not _stop_requested(reporter):
|
54
|
+
return
|
118
55
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
return output_audio_data
|
56
|
+
if temp_path is not None and temp_path.exists():
|
57
|
+
_delete_path(temp_path)
|
58
|
+
raise ProcessingAborted("Processing aborted by user request.")
|
123
59
|
|
124
60
|
|
125
61
|
def speed_up_video(
|
@@ -152,10 +88,14 @@ def speed_up_video(
|
|
152
88
|
original_duration = metadata["duration"]
|
153
89
|
frame_count = metadata.get("frame_count", 0)
|
154
90
|
|
91
|
+
app_version = resolve_version()
|
92
|
+
if app_version and app_version != "unknown":
|
93
|
+
reporter.log(f"talks-reducer v{app_version}")
|
94
|
+
|
155
95
|
reporter.log(
|
156
96
|
(
|
157
|
-
"Source metadata
|
158
|
-
"
|
97
|
+
"Source metadata: duration: {duration:.2f}s, frame rate: {fps:.3f} fps,"
|
98
|
+
" frames: {frames}"
|
159
99
|
).format(
|
160
100
|
duration=original_duration,
|
161
101
|
fps=frame_rate,
|
@@ -186,6 +126,7 @@ def speed_up_video(
|
|
186
126
|
ffmpeg_path=ffmpeg_path,
|
187
127
|
)
|
188
128
|
|
129
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
189
130
|
reporter.log("Extracting audio...")
|
190
131
|
process_callback = getattr(reporter, "process_callback", None)
|
191
132
|
estimated_total_frames = frame_count
|
@@ -211,12 +152,13 @@ def speed_up_video(
|
|
211
152
|
audio_sample_count = audio_data.shape[0]
|
212
153
|
max_audio_volume = audio_utils.get_max_volume(audio_data)
|
213
154
|
|
214
|
-
reporter.log("
|
215
|
-
reporter.log(f"- Max Audio Volume: {max_audio_volume}")
|
155
|
+
reporter.log(f"Max Audio Volume: {max_audio_volume}")
|
216
156
|
|
217
157
|
samples_per_frame = wav_sample_rate / frame_rate
|
218
158
|
audio_frame_count = int(math.ceil(audio_sample_count / samples_per_frame))
|
219
159
|
|
160
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
161
|
+
|
220
162
|
has_loud_audio = chunk_utils.detect_loud_frames(
|
221
163
|
audio_data,
|
222
164
|
audio_frame_count,
|
@@ -227,7 +169,9 @@ def speed_up_video(
|
|
227
169
|
|
228
170
|
chunks, _ = chunk_utils.build_chunks(has_loud_audio, options.frame_spreadage)
|
229
171
|
|
230
|
-
reporter.log(f"
|
172
|
+
reporter.log(f"Processing {len(chunks)} chunks...")
|
173
|
+
|
174
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
231
175
|
|
232
176
|
new_speeds = [options.silent_speed, options.sounded_speed]
|
233
177
|
output_audio_data, updated_chunks = audio_utils.process_audio_chunks(
|
@@ -248,6 +192,8 @@ def speed_up_video(
|
|
248
192
|
_prepare_output_audio(output_audio_data),
|
249
193
|
)
|
250
194
|
|
195
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
196
|
+
|
251
197
|
expression = chunk_utils.get_tree_expression(updated_chunks)
|
252
198
|
filter_graph_path = temp_path / "filterGraph.txt"
|
253
199
|
with open(filter_graph_path, "w", encoding="utf-8") as filter_graph_file:
|
@@ -285,6 +231,8 @@ def speed_up_video(
|
|
285
231
|
_delete_path(temp_path)
|
286
232
|
raise FileNotFoundError("Filter graph file was not generated")
|
287
233
|
|
234
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
235
|
+
|
288
236
|
try:
|
289
237
|
final_total_frames = updated_chunks[-1][3] if updated_chunks else 0
|
290
238
|
if final_total_frames > 0:
|
@@ -315,6 +263,8 @@ def speed_up_video(
|
|
315
263
|
)
|
316
264
|
except subprocess.CalledProcessError as exc:
|
317
265
|
if fallback_command_str and use_cuda_encoder:
|
266
|
+
_raise_if_stopped(reporter, temp_path=temp_path)
|
267
|
+
|
318
268
|
reporter.log("CUDA encoding failed, retrying with CPU encoder...")
|
319
269
|
if final_total_frames > 0:
|
320
270
|
reporter.log(
|
@@ -364,3 +314,103 @@ def speed_up_video(
|
|
364
314
|
time_ratio=time_ratio,
|
365
315
|
size_ratio=size_ratio,
|
366
316
|
)
|
317
|
+
|
318
|
+
|
319
|
+
def _input_to_output_filename(filename: Path, small: bool = False) -> Path:
|
320
|
+
dot_index = filename.name.rfind(".")
|
321
|
+
suffix_parts = []
|
322
|
+
|
323
|
+
if small:
|
324
|
+
suffix_parts.append("_small")
|
325
|
+
|
326
|
+
if not suffix_parts:
|
327
|
+
suffix_parts.append("") # Default case
|
328
|
+
|
329
|
+
suffix = "_speedup" + "".join(suffix_parts)
|
330
|
+
new_name = (
|
331
|
+
filename.name[:dot_index] + suffix + filename.name[dot_index:]
|
332
|
+
if dot_index != -1
|
333
|
+
else filename.name + suffix
|
334
|
+
)
|
335
|
+
return filename.with_name(new_name)
|
336
|
+
|
337
|
+
|
338
|
+
def _create_path(path: Path) -> None:
|
339
|
+
try:
|
340
|
+
path.mkdir(parents=True, exist_ok=True)
|
341
|
+
except OSError as exc: # pragma: no cover - defensive logging
|
342
|
+
raise AssertionError(
|
343
|
+
"Creation of the directory failed. (The TEMP folder may already exist. Delete or rename it, and try again.)"
|
344
|
+
) from exc
|
345
|
+
|
346
|
+
|
347
|
+
def _delete_path(path: Path) -> None:
|
348
|
+
import time
|
349
|
+
from shutil import rmtree
|
350
|
+
|
351
|
+
if not path.exists():
|
352
|
+
return
|
353
|
+
|
354
|
+
try:
|
355
|
+
rmtree(path, ignore_errors=False)
|
356
|
+
for i in range(5):
|
357
|
+
if not path.exists():
|
358
|
+
return
|
359
|
+
time.sleep(0.01 * i)
|
360
|
+
except OSError as exc: # pragma: no cover - defensive logging
|
361
|
+
print(f"Deletion of the directory {path} failed")
|
362
|
+
print(exc)
|
363
|
+
|
364
|
+
|
365
|
+
def _extract_video_metadata(input_file: Path, frame_rate: float) -> Dict[str, float]:
|
366
|
+
from .ffmpeg import get_ffprobe_path
|
367
|
+
|
368
|
+
ffprobe_path = get_ffprobe_path()
|
369
|
+
command = [
|
370
|
+
ffprobe_path,
|
371
|
+
"-i",
|
372
|
+
os.fspath(input_file),
|
373
|
+
"-hide_banner",
|
374
|
+
"-loglevel",
|
375
|
+
"error",
|
376
|
+
"-select_streams",
|
377
|
+
"v",
|
378
|
+
"-show_entries",
|
379
|
+
"format=duration:stream=avg_frame_rate,nb_frames",
|
380
|
+
]
|
381
|
+
process = subprocess.Popen(
|
382
|
+
command,
|
383
|
+
stdout=subprocess.PIPE,
|
384
|
+
stderr=subprocess.PIPE,
|
385
|
+
bufsize=1,
|
386
|
+
universal_newlines=True,
|
387
|
+
)
|
388
|
+
stdout, _ = process.communicate()
|
389
|
+
|
390
|
+
match_frame_rate = re.search(r"frame_rate=(\d*)/(\d*)", str(stdout))
|
391
|
+
if match_frame_rate is not None:
|
392
|
+
frame_rate = float(match_frame_rate.group(1)) / float(match_frame_rate.group(2))
|
393
|
+
|
394
|
+
match_duration = re.search(r"duration=([\d.]*)", str(stdout))
|
395
|
+
original_duration = float(match_duration.group(1)) if match_duration else 0.0
|
396
|
+
|
397
|
+
match_frames = re.search(r"nb_frames=(\d+)", str(stdout))
|
398
|
+
frame_count = int(match_frames.group(1)) if match_frames else 0
|
399
|
+
|
400
|
+
return {
|
401
|
+
"frame_rate": frame_rate,
|
402
|
+
"duration": original_duration,
|
403
|
+
"frame_count": frame_count,
|
404
|
+
}
|
405
|
+
|
406
|
+
|
407
|
+
def _ensure_two_dimensional(audio_data: np.ndarray) -> np.ndarray:
|
408
|
+
if audio_data.ndim == 1:
|
409
|
+
return audio_data[:, np.newaxis]
|
410
|
+
return audio_data
|
411
|
+
|
412
|
+
|
413
|
+
def _prepare_output_audio(output_audio_data: np.ndarray) -> np.ndarray:
|
414
|
+
if output_audio_data.ndim == 2 and output_audio_data.shape[1] == 1:
|
415
|
+
return output_audio_data[:, 0]
|
416
|
+
return output_audio_data
|
File without changes
|