auto-editor 26.3.1__py3-none-any.whl → 26.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.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +181 -55
- auto_editor/analyze.py +47 -45
- auto_editor/cmds/levels.py +1 -1
- auto_editor/cmds/repl.py +2 -3
- auto_editor/cmds/test.py +334 -386
- auto_editor/edit.py +28 -9
- auto_editor/lang/palet.py +23 -27
- auto_editor/make_layers.py +30 -18
- auto_editor/output.py +2 -2
- auto_editor/preview.py +3 -2
- auto_editor/render/audio.py +4 -1
- auto_editor/render/video.py +1 -1
- auto_editor/timeline.py +8 -1
- auto_editor/utils/types.py +7 -116
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/METADATA +2 -2
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/RECORD +21 -21
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/WHEEL +1 -1
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/LICENSE +0 -0
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.3.1.dist-info → auto_editor-26.3.3.dist-info}/top_level.txt +0 -0
auto_editor/cmds/test.py
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
import concurrent.futures
|
2
|
+
import hashlib
|
3
3
|
import os
|
4
4
|
import shutil
|
5
5
|
import subprocess
|
6
6
|
import sys
|
7
|
+
from collections.abc import Callable
|
7
8
|
from dataclasses import dataclass, field
|
8
9
|
from fractions import Fraction
|
10
|
+
from hashlib import sha256
|
11
|
+
from tempfile import mkdtemp
|
9
12
|
from time import perf_counter
|
10
|
-
from typing import TYPE_CHECKING
|
11
13
|
|
12
14
|
import av
|
13
15
|
import numpy as np
|
@@ -20,12 +22,6 @@ from auto_editor.lib.err import MyError
|
|
20
22
|
from auto_editor.utils.log import Log
|
21
23
|
from auto_editor.vanparse import ArgumentParser
|
22
24
|
|
23
|
-
if TYPE_CHECKING:
|
24
|
-
from collections.abc import Callable
|
25
|
-
from typing import Any
|
26
|
-
|
27
|
-
from auto_editor.vanparse import ArgumentParser
|
28
|
-
|
29
25
|
|
30
26
|
@dataclass(slots=True)
|
31
27
|
class TestArgs:
|
@@ -53,38 +49,53 @@ def pipe_to_console(cmd: list[str]) -> tuple[int, str, str]:
|
|
53
49
|
return process.returncode, stdout.decode("utf-8"), stderr.decode("utf-8")
|
54
50
|
|
55
51
|
|
52
|
+
all_files = (
|
53
|
+
"aac.m4a",
|
54
|
+
"alac.m4a",
|
55
|
+
"wav/pcm-f32le.wav",
|
56
|
+
"wav/pcm-s32le.wav",
|
57
|
+
"multi-track.mov",
|
58
|
+
"mov_text.mp4",
|
59
|
+
"testsrc.mkv",
|
60
|
+
)
|
61
|
+
log = Log()
|
62
|
+
|
63
|
+
|
64
|
+
def fileinfo(path: str) -> FileInfo:
|
65
|
+
return initFileInfo(path, log)
|
66
|
+
|
67
|
+
|
68
|
+
def calculate_sha256(filename: str) -> str:
|
69
|
+
sha256_hash = hashlib.sha256()
|
70
|
+
with open(filename, "rb") as f:
|
71
|
+
for byte_block in iter(lambda: f.read(4096), b""):
|
72
|
+
sha256_hash.update(byte_block)
|
73
|
+
return sha256_hash.hexdigest()
|
74
|
+
|
75
|
+
|
56
76
|
class Runner:
|
57
77
|
def __init__(self) -> None:
|
58
78
|
self.program = [sys.executable, "-m", "auto_editor"]
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
cmd = self.program + inputs + cmd + ["--no-open"]
|
64
|
-
|
65
|
-
if
|
79
|
+
self.temp_dir = mkdtemp()
|
80
|
+
|
81
|
+
def main(self, inputs: list[str], cmd: list[str], output: str | None = None) -> str:
|
82
|
+
assert inputs
|
83
|
+
cmd = self.program + inputs + cmd + ["--no-open", "--progress", "none"]
|
84
|
+
temp_dir = self.temp_dir
|
85
|
+
if not os.path.exists(temp_dir):
|
86
|
+
raise ValueError("Where's the temp dir")
|
87
|
+
if output is None:
|
88
|
+
new_root = sha256("".join(cmd).encode()).hexdigest()[:16]
|
89
|
+
output = os.path.join(temp_dir, new_root)
|
90
|
+
else:
|
66
91
|
root, ext = os.path.splitext(output)
|
67
92
|
if inputs and ext == "":
|
68
93
|
output = root + os.path.splitext(inputs[0])[1]
|
69
|
-
|
94
|
+
output = os.path.join(temp_dir, output)
|
70
95
|
|
71
|
-
|
72
|
-
root, ext = os.path.splitext(inputs[0])
|
73
|
-
|
74
|
-
if "--export_as_json" in cmd:
|
75
|
-
ext = ".json"
|
76
|
-
elif "-exp" in cmd:
|
77
|
-
ext = ".xml"
|
78
|
-
elif "-exf" in cmd:
|
79
|
-
ext = ".fcpxml"
|
80
|
-
elif "-exs" in cmd:
|
81
|
-
ext = ".mlt"
|
82
|
-
|
83
|
-
output = f"{root}_ALTERED{ext}"
|
84
|
-
|
85
|
-
returncode, stdout, stderr = pipe_to_console(cmd)
|
96
|
+
returncode, stdout, stderr = pipe_to_console(cmd + ["--output", output])
|
86
97
|
if returncode > 0:
|
87
|
-
raise Exception(f"{stdout}\n{stderr}\n")
|
98
|
+
raise Exception(f"Test returned: {returncode}\n{stdout}\n{stderr}\n")
|
88
99
|
|
89
100
|
return output
|
90
101
|
|
@@ -106,182 +117,111 @@ class Runner:
|
|
106
117
|
else:
|
107
118
|
raise Exception("Program should not respond with code 0 but did!")
|
108
119
|
|
109
|
-
|
110
|
-
def run_tests(tests: list[Callable], args: TestArgs) -> None:
|
111
|
-
def clean_all() -> None:
|
112
|
-
def clean(the_dir: str) -> None:
|
113
|
-
for item in os.listdir(the_dir):
|
114
|
-
if "_ALTERED" in item:
|
115
|
-
os.remove(os.path.join(the_dir, item))
|
116
|
-
if item.endswith("_tracks"):
|
117
|
-
shutil.rmtree(os.path.join(the_dir, item))
|
118
|
-
|
119
|
-
clean("resources")
|
120
|
-
clean(os.getcwd())
|
121
|
-
|
122
|
-
if args.only != []:
|
123
|
-
tests = list(filter(lambda t: t.__name__ in args.only, tests))
|
124
|
-
|
125
|
-
total_time = 0.0
|
126
|
-
|
127
|
-
passed = 0
|
128
|
-
total = len(tests)
|
129
|
-
for index, test in enumerate(tests, start=1):
|
130
|
-
name = test.__name__
|
131
|
-
start = perf_counter()
|
132
|
-
outputs = None
|
133
|
-
|
134
|
-
try:
|
135
|
-
outputs = test()
|
136
|
-
dur = perf_counter() - start
|
137
|
-
total_time += dur
|
138
|
-
except KeyboardInterrupt:
|
139
|
-
print("Testing Interrupted by User.")
|
140
|
-
clean_all()
|
141
|
-
sys.exit(1)
|
142
|
-
except Exception as e:
|
143
|
-
dur = perf_counter() - start
|
144
|
-
total_time += dur
|
145
|
-
print(
|
146
|
-
f"{name:<24} ({index}/{total}) {round(dur, 2):<4} secs \033[1;31m[FAILED]\033[0m",
|
147
|
-
flush=True,
|
148
|
-
)
|
149
|
-
if args.no_fail_fast:
|
150
|
-
print(f"\n{e}")
|
151
|
-
else:
|
152
|
-
print("")
|
153
|
-
clean_all()
|
154
|
-
raise e
|
155
|
-
else:
|
156
|
-
passed += 1
|
157
|
-
print(
|
158
|
-
f"{name:<24} ({index}/{total}) {round(dur, 2):<4} secs [\033[1;32mPASSED\033[0m]",
|
159
|
-
flush=True,
|
160
|
-
)
|
161
|
-
if outputs is not None:
|
162
|
-
if isinstance(outputs, str):
|
163
|
-
outputs = [outputs]
|
164
|
-
|
165
|
-
for out in outputs:
|
166
|
-
try:
|
167
|
-
os.remove(out)
|
168
|
-
except FileNotFoundError:
|
169
|
-
pass
|
170
|
-
|
171
|
-
print(f"\nCompleted\n{passed}/{total}\n{round(total_time, 2)} secs")
|
172
|
-
clean_all()
|
173
|
-
|
174
|
-
|
175
|
-
def main(sys_args: list[str] | None = None):
|
176
|
-
if sys_args is None:
|
177
|
-
sys_args = sys.argv[1:]
|
178
|
-
|
179
|
-
args = test_options(ArgumentParser("test")).parse_args(TestArgs, sys_args)
|
180
|
-
|
181
|
-
run = Runner()
|
182
|
-
log = Log()
|
183
|
-
|
184
|
-
def fileinfo(path: str) -> FileInfo:
|
185
|
-
return initFileInfo(path, log)
|
186
|
-
|
187
|
-
### Tests ###
|
188
|
-
|
189
|
-
all_files = (
|
190
|
-
"aac.m4a",
|
191
|
-
"alac.m4a",
|
192
|
-
"wav/pcm-f32le.wav",
|
193
|
-
"wav/pcm-s32le.wav",
|
194
|
-
"multi-track.mov",
|
195
|
-
"mov_text.mp4",
|
196
|
-
"testsrc.mkv",
|
197
|
-
)
|
198
|
-
|
199
|
-
## API Tests ##
|
200
|
-
|
201
|
-
def help_tests():
|
120
|
+
def test_help(self):
|
202
121
|
"""check the help option, its short, and help on options and groups."""
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
def
|
122
|
+
self.raw(["--help"])
|
123
|
+
self.raw(["-h"])
|
124
|
+
self.raw(["--margin", "--help"])
|
125
|
+
self.raw(["--edit", "-h"])
|
126
|
+
self.raw(["--help", "--help"])
|
127
|
+
self.raw(["-h", "--help"])
|
128
|
+
self.raw(["--help", "-h"])
|
129
|
+
|
130
|
+
def test_version(self):
|
212
131
|
"""Test version flags and debug by itself."""
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
def
|
217
|
-
|
218
|
-
|
219
|
-
def info():
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
def levels():
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
def subdump():
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
def desc():
|
235
|
-
|
132
|
+
self.raw(["--version"])
|
133
|
+
self.raw(["-V"])
|
134
|
+
|
135
|
+
def test_parser(self):
|
136
|
+
self.check(["example.mp4", "--video-speed"], "needs argument")
|
137
|
+
|
138
|
+
def info(self):
|
139
|
+
self.raw(["info", "example.mp4"])
|
140
|
+
self.raw(["info", "resources/only-video/man-on-green-screen.mp4"])
|
141
|
+
self.raw(["info", "resources/multi-track.mov"])
|
142
|
+
self.raw(["info", "resources/new-commentary.mp3"])
|
143
|
+
self.raw(["info", "resources/testsrc.mkv"])
|
144
|
+
|
145
|
+
def levels(self):
|
146
|
+
self.raw(["levels", "resources/multi-track.mov"])
|
147
|
+
self.raw(["levels", "resources/new-commentary.mp3"])
|
148
|
+
|
149
|
+
def subdump(self):
|
150
|
+
self.raw(["subdump", "resources/mov_text.mp4"])
|
151
|
+
self.raw(["subdump", "resources/webvtt.mkv"])
|
152
|
+
|
153
|
+
def desc(self):
|
154
|
+
self.raw(["desc", "example.mp4"])
|
155
|
+
|
156
|
+
def test_movflags(self) -> None:
|
157
|
+
file = "resources/testsrc.mp4"
|
158
|
+
out = self.main([file], ["--faststart"]) + ".mp4"
|
159
|
+
fast = calculate_sha256(out)
|
160
|
+
with av.open(out) as container:
|
161
|
+
assert isinstance(container.streams[0], av.VideoStream)
|
162
|
+
assert isinstance(container.streams[1], av.AudioStream)
|
236
163
|
|
237
|
-
|
238
|
-
|
164
|
+
out = self.main([file], ["--no-faststart"]) + ".mp4"
|
165
|
+
nofast = calculate_sha256(out)
|
166
|
+
with av.open(out) as container:
|
167
|
+
assert isinstance(container.streams[0], av.VideoStream)
|
168
|
+
assert isinstance(container.streams[1], av.AudioStream)
|
239
169
|
|
170
|
+
out = self.main([file], ["--fragmented"]) + ".mp4"
|
171
|
+
frag = calculate_sha256(out)
|
240
172
|
with av.open(out) as container:
|
241
|
-
assert container.streams[0].
|
242
|
-
assert container.streams[1].
|
173
|
+
assert isinstance(container.streams[0], av.VideoStream)
|
174
|
+
assert isinstance(container.streams[1], av.AudioStream)
|
243
175
|
|
244
|
-
|
245
|
-
|
176
|
+
assert fast != nofast, "+faststart is not being applied"
|
177
|
+
assert frag not in (fast, nofast), "fragmented output should diff."
|
246
178
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
179
|
+
def test_example(self) -> None:
|
180
|
+
out = self.main(["example.mp4"], [], output="example_ALTERED.mp4")
|
181
|
+
with av.open(out) as container:
|
182
|
+
assert container.duration is not None
|
183
|
+
assert container.duration > 17300000 and container.duration < 2 << 24
|
184
|
+
|
185
|
+
video = container.streams[0]
|
186
|
+
audio = container.streams[1]
|
187
|
+
assert isinstance(video, av.VideoStream)
|
188
|
+
assert isinstance(audio, av.AudioStream)
|
189
|
+
assert video.base_rate == 30
|
190
|
+
assert video.average_rate is not None
|
191
|
+
assert video.average_rate == 30, video.average_rate
|
192
|
+
assert (video.width, video.height) == (1280, 720)
|
193
|
+
assert video.codec.name == "h264"
|
194
|
+
assert video.language == "eng"
|
195
|
+
assert audio.codec.name == "aac"
|
196
|
+
assert audio.sample_rate == 48000
|
197
|
+
assert audio.language == "eng"
|
258
198
|
|
259
199
|
# PR #260
|
260
|
-
def
|
261
|
-
|
200
|
+
def test_high_speed(self):
|
201
|
+
self.check(["example.mp4", "--video-speed", "99998"], "empty")
|
262
202
|
|
263
203
|
# Issue #184
|
264
|
-
def
|
265
|
-
|
266
|
-
|
204
|
+
def test_units(self):
|
205
|
+
self.main(["example.mp4"], ["--edit", "all/e", "--set-speed", "125%,-30,end"])
|
206
|
+
self.main(["example.mp4"], ["--edit", "audio:threshold=4%"])
|
267
207
|
|
268
|
-
def
|
269
|
-
|
270
|
-
|
208
|
+
def test_sr_units(self):
|
209
|
+
self.main(["example.mp4"], ["--sample_rate", "44100 Hz"])
|
210
|
+
self.main(["example.mp4"], ["--sample_rate", "44.1 kHz"])
|
271
211
|
|
272
|
-
def
|
273
|
-
|
212
|
+
def test_video_speed(self):
|
213
|
+
self.main(["example.mp4"], ["--video-speed", "1.5"])
|
274
214
|
|
275
|
-
def
|
215
|
+
def test_backwards_range(self):
|
276
216
|
"""
|
277
217
|
Cut out the last 5 seconds of a media file by using negative number in the
|
278
218
|
range.
|
279
219
|
"""
|
280
|
-
|
281
|
-
|
220
|
+
self.main(["example.mp4"], ["--edit", "none", "--cut_out", "-5secs,end"])
|
221
|
+
self.main(["example.mp4"], ["--edit", "all/e", "--add_in", "-5secs,end"])
|
282
222
|
|
283
|
-
def
|
284
|
-
|
223
|
+
def test_cut_out(self):
|
224
|
+
self.main(
|
285
225
|
["example.mp4"],
|
286
226
|
[
|
287
227
|
"--edit",
|
@@ -294,112 +234,93 @@ def main(sys_args: list[str] | None = None):
|
|
294
234
|
"2secs,10secs",
|
295
235
|
],
|
296
236
|
)
|
297
|
-
|
237
|
+
self.main(
|
298
238
|
["example.mp4"],
|
299
239
|
["--edit", "all/e", "--video_speed", "2", "--add_in", "2secs,10secs"],
|
300
240
|
)
|
301
241
|
|
302
|
-
def
|
242
|
+
def test_gif(self):
|
303
243
|
"""
|
304
244
|
Feed auto-editor a gif file and make sure it can spit out a correctly formatted
|
305
245
|
gif. No editing is requested.
|
306
246
|
"""
|
307
|
-
|
308
|
-
|
309
|
-
)
|
247
|
+
input = ["resources/only-video/man-on-green-screen.gif"]
|
248
|
+
out = self.main(input, ["--edit", "none", "--cut-out", "2sec,end"], "out.gif")
|
310
249
|
assert fileinfo(out).videos[0].codec == "gif"
|
311
250
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
run.main(["example.mp4"], ["-m", "0.3sec"])
|
318
|
-
run.main(["example.mp4"], ["-m", "6,-3secs"])
|
319
|
-
return run.main(["example.mp4"], ["-m", "0.4 seconds", "--stats"])
|
251
|
+
def test_margin(self):
|
252
|
+
self.main(["example.mp4"], ["-m", "3"])
|
253
|
+
self.main(["example.mp4"], ["-m", "0.3sec"])
|
254
|
+
self.main(["example.mp4"], ["-m", "0.1 seconds"])
|
255
|
+
self.main(["example.mp4"], ["-m", "6,-3secs"])
|
320
256
|
|
321
|
-
def
|
257
|
+
def test_input_extension(self):
|
322
258
|
"""Input file must have an extension. Throw error if none is given."""
|
259
|
+
path = os.path.join(self.temp_dir, "example")
|
260
|
+
shutil.copy("example.mp4", path)
|
261
|
+
self.check([path, "--no-open"], "must have an extension")
|
323
262
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
return "example"
|
328
|
-
|
329
|
-
def output_extension():
|
330
|
-
# Add input extension to output name if no output extension is given.
|
331
|
-
out = run.main(inputs=["example.mp4"], cmd=[], output="out")
|
332
|
-
|
333
|
-
assert out == "out.mp4"
|
334
|
-
assert fileinfo(out).videos[0].codec == "h264"
|
335
|
-
|
336
|
-
out = run.main(inputs=["resources/testsrc.mkv"], cmd=[], output="out")
|
337
|
-
assert out == "out.mkv"
|
338
|
-
assert fileinfo(out).videos[0].codec == "h264"
|
263
|
+
def test_silent_threshold(self):
|
264
|
+
with av.open("resources/new-commentary.mp3") as container:
|
265
|
+
assert container.duration / av.time_base == 6.732
|
339
266
|
|
340
|
-
|
341
|
-
|
342
|
-
def progress():
|
343
|
-
run.main(["example.mp4"], ["--progress", "machine"])
|
344
|
-
run.main(["example.mp4"], ["--progress", "none"])
|
345
|
-
return run.main(["example.mp4"], ["--progress", "ascii"])
|
346
|
-
|
347
|
-
def silent_threshold():
|
348
|
-
return run.main(
|
267
|
+
out = self.main(
|
349
268
|
["resources/new-commentary.mp3"], ["--edit", "audio:threshold=0.1"]
|
350
269
|
)
|
270
|
+
out += ".mp3"
|
351
271
|
|
352
|
-
|
353
|
-
|
354
|
-
assert len(fileinfo(out).audios) == 2
|
272
|
+
with av.open(out) as container:
|
273
|
+
assert container.duration / av.time_base == 6.552
|
355
274
|
|
356
|
-
|
275
|
+
def test_track(self):
|
276
|
+
out = self.main(["resources/multi-track.mov"], ["--keep_tracks_seperate"], "te")
|
277
|
+
assert len(fileinfo(out).audios) == 2
|
357
278
|
|
358
|
-
def
|
359
|
-
out =
|
360
|
-
|
361
|
-
return out, out2
|
279
|
+
def test_export_json(self):
|
280
|
+
out = self.main(["example.mp4"], ["--export_as_json"], "c77130d763d40e8.json")
|
281
|
+
self.main([out], [])
|
362
282
|
|
363
|
-
def
|
364
|
-
|
283
|
+
def test_import_v1(self):
|
284
|
+
path = os.path.join(self.temp_dir, "v1.json")
|
285
|
+
with open(path, "w") as file:
|
365
286
|
file.write(
|
366
287
|
"""{"version": "1", "source": "example.mp4", "chunks": [ [0, 26, 1.0], [26, 34, 0] ]}"""
|
367
288
|
)
|
368
289
|
|
369
|
-
|
290
|
+
self.main([path], [])
|
370
291
|
|
371
|
-
def
|
372
|
-
|
292
|
+
def test_premiere_named_export(self):
|
293
|
+
self.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
|
373
294
|
|
374
|
-
def
|
375
|
-
# cn = fileinfo(
|
295
|
+
def test_export_subtitles(self):
|
296
|
+
# cn = fileinfo(self.main(["resources/mov_text.mp4"], [], "movtext_out.mp4"))
|
376
297
|
|
377
298
|
# assert len(cn.videos) == 1
|
378
299
|
# assert len(cn.audios) == 1
|
379
300
|
# assert len(cn.subtitles) == 1
|
380
301
|
|
381
|
-
cn = fileinfo(
|
382
|
-
|
302
|
+
cn = fileinfo(self.main(["resources/webvtt.mkv"], [], "webvtt_out.mkv"))
|
383
303
|
assert len(cn.videos) == 1
|
384
304
|
assert len(cn.audios) == 1
|
385
305
|
assert len(cn.subtitles) == 1
|
386
306
|
|
387
|
-
def
|
388
|
-
cn = fileinfo(
|
389
|
-
|
307
|
+
def test_scale(self):
|
308
|
+
cn = fileinfo(self.main(["example.mp4"], ["--scale", "1.5"], "scale.mp4"))
|
390
309
|
assert cn.videos[0].fps == 30
|
391
310
|
assert cn.videos[0].width == 1920
|
392
311
|
assert cn.videos[0].height == 1080
|
393
312
|
assert cn.audios[0].samplerate == 48000
|
394
313
|
|
395
|
-
cn = fileinfo(
|
396
|
-
|
314
|
+
cn = fileinfo(self.main(["example.mp4"], ["--scale", "0.2"], "scale.mp4"))
|
397
315
|
assert cn.videos[0].fps == 30
|
398
316
|
assert cn.videos[0].width == 256
|
399
317
|
assert cn.videos[0].height == 144
|
400
318
|
assert cn.audios[0].samplerate == 48000
|
401
319
|
|
402
|
-
|
320
|
+
def test_resolution(self):
|
321
|
+
out = self.main(
|
322
|
+
["example.mp4"], ["-res", "700,380", "-b", "darkgreen"], "green"
|
323
|
+
)
|
403
324
|
cn = fileinfo(out)
|
404
325
|
|
405
326
|
assert cn.videos[0].fps == 30
|
@@ -407,182 +328,159 @@ def main(sys_args: list[str] | None = None):
|
|
407
328
|
assert cn.videos[0].height == 380
|
408
329
|
assert cn.audios[0].samplerate == 48000
|
409
330
|
|
410
|
-
|
331
|
+
def test_premiere(self):
|
332
|
+
for test_name in all_files:
|
333
|
+
p_xml = self.main([f"resources/{test_name}"], ["-exp"], "out.xml")
|
334
|
+
self.main([p_xml], [])
|
411
335
|
|
412
|
-
def
|
413
|
-
results = set()
|
336
|
+
def test_export(self):
|
414
337
|
for test_name in all_files:
|
415
338
|
test_file = f"resources/{test_name}"
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
return tuple(results)
|
421
|
-
|
422
|
-
def export():
|
423
|
-
results = set()
|
339
|
+
self.main([test_file], ["--export", "final-cut-pro:version=10"])
|
340
|
+
self.main([test_file], ["--export", "final-cut-pro:version=11"])
|
341
|
+
self.main([test_file], ["-exs"])
|
342
|
+
self.main([test_file], ["--stats"])
|
424
343
|
|
344
|
+
def test_clip_sequence(self):
|
425
345
|
for test_name in all_files:
|
426
346
|
test_file = f"resources/{test_name}"
|
427
|
-
|
428
|
-
run.main([test_file], ["--edit", "none"])
|
429
|
-
results.add(run.main([test_file], ["--export", "final-cut-pro:version=10"]))
|
430
|
-
results.add(run.main([test_file], ["--export", "final-cut-pro:version=11"]))
|
431
|
-
results.add(run.main([test_file], ["-exs"]))
|
432
|
-
results.add(run.main([test_file], ["--export_as_clip_sequence"]))
|
433
|
-
run.main([test_file], ["--stats"])
|
347
|
+
self.main([test_file], ["--export_as_clip_sequence"])
|
434
348
|
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
run.main(["example.mp4"], ["--video_codec", "h264"])
|
439
|
-
return run.main(["example.mp4"], ["--audio_codec", "ac3"])
|
349
|
+
def test_codecs(self):
|
350
|
+
self.main(["example.mp4"], ["--video_codec", "h264"])
|
351
|
+
self.main(["example.mp4"], ["--audio_codec", "ac3"])
|
440
352
|
|
441
353
|
# Issue #241
|
442
|
-
def
|
443
|
-
out =
|
354
|
+
def test_multi_track_edit(self):
|
355
|
+
out = self.main(
|
444
356
|
["example.mp4", "resources/multi-track.mov"],
|
445
357
|
["--edit", "audio:stream=1"],
|
446
|
-
"
|
358
|
+
"multi-track_ALTERED.mov",
|
447
359
|
)
|
448
360
|
assert len(fileinfo(out).audios) == 1
|
449
361
|
|
450
|
-
|
362
|
+
def test_concat(self):
|
363
|
+
out = self.main(["example.mp4"], ["--cut-out", "0,171"], "hmm.mp4")
|
364
|
+
self.main(["example.mp4", out], ["--debug"])
|
451
365
|
|
452
|
-
def
|
453
|
-
out =
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
def concat_mux_tracks():
|
458
|
-
out = run.main(["example.mp4", "resources/multi-track.mov"], [], "out.mov")
|
366
|
+
def test_concat_mux_tracks(self):
|
367
|
+
out = self.main(
|
368
|
+
["example.mp4", "resources/multi-track.mov"], [], "concat_mux.mov"
|
369
|
+
)
|
459
370
|
assert len(fileinfo(out).audios) == 1
|
460
371
|
|
461
|
-
|
462
|
-
|
463
|
-
def concat_multiple_tracks():
|
464
|
-
out = run.main(
|
372
|
+
def test_concat_multi_tracks(self):
|
373
|
+
out = self.main(
|
465
374
|
["resources/multi-track.mov", "resources/multi-track.mov"],
|
466
375
|
["--keep-tracks-separate"],
|
467
376
|
"out.mov",
|
468
377
|
)
|
469
378
|
assert len(fileinfo(out).audios) == 2
|
470
|
-
out =
|
379
|
+
out = self.main(
|
471
380
|
["example.mp4", "resources/multi-track.mov"],
|
472
381
|
["--keep-tracks-separate"],
|
473
382
|
"out.mov",
|
474
383
|
)
|
475
384
|
assert len(fileinfo(out).audios) == 2
|
476
385
|
|
477
|
-
|
478
|
-
|
479
|
-
def frame_rate():
|
480
|
-
cn = fileinfo(run.main(["example.mp4"], ["-r", "15", "--no-seek"]))
|
386
|
+
def test_frame_rate(self):
|
387
|
+
cn = fileinfo(self.main(["example.mp4"], ["-r", "15", "--no-seek"], "fr.mp4"))
|
481
388
|
video = cn.videos[0]
|
482
389
|
assert video.fps == 15, video.fps
|
483
390
|
assert video.duration - 17.33333333333333333333333 < 3, video.duration
|
484
391
|
|
485
|
-
cn = fileinfo(
|
392
|
+
cn = fileinfo(self.main(["example.mp4"], ["-r", "20"], "fr.mp4"))
|
486
393
|
video = cn.videos[0]
|
487
394
|
assert video.fps == 20, video.fps
|
488
395
|
assert video.duration - 17.33333333333333333333333 < 2
|
489
396
|
|
490
|
-
|
397
|
+
def test_frame_rate_60(self):
|
398
|
+
cn = fileinfo(self.main(["example.mp4"], ["-r", "60"], "fr60.mp4"))
|
491
399
|
video = cn.videos[0]
|
492
400
|
|
493
401
|
assert video.fps == 60, video.fps
|
494
402
|
assert video.duration - 17.33333333333333333333333 < 0.3
|
495
403
|
|
496
|
-
|
497
|
-
|
498
|
-
# def embedded_image():
|
499
|
-
# out1 = run.main(["resources/embedded-image/h264-png.mp4"], [])
|
404
|
+
# def embedded_image(self):
|
405
|
+
# out1 = self.main(["resources/embedded-image/h264-png.mp4"], [])
|
500
406
|
# cn = fileinfo(out1)
|
501
407
|
# assert cn.videos[0].codec == "h264"
|
502
408
|
# assert cn.videos[1].codec == "png"
|
503
409
|
|
504
|
-
# out2 =
|
410
|
+
# out2 = self.main(["resources/embedded-image/h264-mjpeg.mp4"], [])
|
505
411
|
# cn = fileinfo(out2)
|
506
412
|
# assert cn.videos[0].codec == "h264"
|
507
413
|
# assert cn.videos[1].codec == "mjpeg"
|
508
414
|
|
509
|
-
# out3 =
|
415
|
+
# out3 = self.main(["resources/embedded-image/h264-png.mkv"], [])
|
510
416
|
# cn = fileinfo(out3)
|
511
417
|
# assert cn.videos[0].codec == "h264"
|
512
418
|
# assert cn.videos[1].codec == "png"
|
513
419
|
|
514
|
-
# out4 =
|
420
|
+
# out4 = self.main(["resources/embedded-image/h264-mjpeg.mkv"], [])
|
515
421
|
# cn = fileinfo(out4)
|
516
422
|
# assert cn.videos[0].codec == "h264"
|
517
423
|
# assert cn.videos[1].codec == "mjpeg"
|
518
424
|
|
519
|
-
|
520
|
-
|
521
|
-
def motion():
|
522
|
-
out = run.main(
|
425
|
+
def test_motion(self):
|
426
|
+
self.main(
|
523
427
|
["resources/only-video/man-on-green-screen.mp4"],
|
524
428
|
["--edit", "motion", "--margin", "0"],
|
525
429
|
)
|
526
|
-
|
430
|
+
self.main(
|
527
431
|
["resources/only-video/man-on-green-screen.mp4"],
|
528
432
|
["--edit", "motion:threshold=0,width=200"],
|
529
433
|
)
|
530
|
-
return out, out2
|
531
434
|
|
532
|
-
def
|
533
|
-
|
534
|
-
|
535
|
-
|
435
|
+
def test_edit_positive(self):
|
436
|
+
self.main(["resources/multi-track.mov"], ["--edit", "audio:stream=all"])
|
437
|
+
self.main(["resources/multi-track.mov"], ["--edit", "not audio:stream=all"])
|
438
|
+
self.main(
|
536
439
|
["resources/multi-track.mov"],
|
537
440
|
["--edit", "(or (not audio:threshold=4%) audio:stream=1)"],
|
538
441
|
)
|
539
|
-
|
442
|
+
self.main(
|
540
443
|
["resources/multi-track.mov"],
|
541
444
|
["--edit", "(or (not audio:threshold=4%) (not audio:stream=1))"],
|
542
445
|
)
|
543
|
-
return out
|
544
446
|
|
545
|
-
def
|
546
|
-
|
447
|
+
def test_edit_negative(self):
|
448
|
+
self.check(
|
547
449
|
["resources/wav/example-cut-s16le.wav", "--edit", "motion"],
|
548
|
-
"video stream
|
450
|
+
"video stream",
|
549
451
|
)
|
550
|
-
|
452
|
+
self.check(
|
551
453
|
["resources/only-video/man-on-green-screen.gif", "--edit", "audio"],
|
552
|
-
"audio stream
|
454
|
+
"audio stream",
|
553
455
|
)
|
554
456
|
|
555
|
-
def
|
556
|
-
|
457
|
+
def test_yuv442p(self):
|
458
|
+
self.main(["resources/test_yuv422p.mp4"], [])
|
557
459
|
|
558
|
-
def
|
559
|
-
|
560
|
-
assert fileinfo(
|
460
|
+
def test_prores(self):
|
461
|
+
out = self.main(["resources/testsrc.mp4"], ["-c:v", "prores"], "prores.mkv")
|
462
|
+
assert fileinfo(out).videos[0].pix_fmt == "yuv422p10le"
|
561
463
|
|
562
|
-
|
563
|
-
assert fileinfo(
|
564
|
-
|
565
|
-
return "out.mkv", "out2.mkv"
|
464
|
+
out2 = self.main([out], ["-c:v", "prores"], "prores2.mkv")
|
465
|
+
assert fileinfo(out2).videos[0].pix_fmt == "yuv422p10le"
|
566
466
|
|
567
467
|
# Issue 280
|
568
|
-
def
|
569
|
-
out =
|
468
|
+
def test_SAR(self):
|
469
|
+
out = self.main(["resources/SAR-2by3.mp4"], [], "2by3_out.mp4")
|
570
470
|
assert fileinfo(out).videos[0].sar == Fraction(2, 3)
|
571
471
|
|
572
|
-
|
573
|
-
|
574
|
-
def audio_norm_f():
|
575
|
-
return run.main(["example.mp4"], ["--audio-normalize", "#f"])
|
472
|
+
def test_audio_norm_f(self):
|
473
|
+
return self.main(["example.mp4"], ["--audio-normalize", "#f"])
|
576
474
|
|
577
|
-
def
|
578
|
-
return
|
475
|
+
def test_audio_norm_ebu(self):
|
476
|
+
return self.main(
|
579
477
|
["example.mp4"], ["--audio-normalize", "ebu:i=-5,lra=20,gain=5,tp=-1"]
|
580
478
|
)
|
581
479
|
|
582
|
-
def palet_python_bridge():
|
480
|
+
def palet_python_bridge(self):
|
583
481
|
env.update(make_standard_env())
|
584
482
|
|
585
|
-
def cases(*cases: tuple[str,
|
483
|
+
def cases(*cases: tuple[str, object]) -> None:
|
586
484
|
for text, expected in cases:
|
587
485
|
try:
|
588
486
|
parser = Parser(Lexer("repl", text))
|
@@ -730,65 +628,115 @@ def main(sys_args: list[str] | None = None):
|
|
730
628
|
('#(#("sym" "symbol?") "bool?")', [["sym", "symbol?"], "bool?"]),
|
731
629
|
)
|
732
630
|
|
733
|
-
def palet_scripts():
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
631
|
+
def palet_scripts(self):
|
632
|
+
self.raw(["palet", "resources/scripts/scope.pal"])
|
633
|
+
self.raw(["palet", "resources/scripts/maxcut.pal"])
|
634
|
+
self.raw(["palet", "resources/scripts/case.pal"])
|
635
|
+
self.raw(["palet", "resources/scripts/testmath.pal"])
|
636
|
+
|
738
637
|
|
638
|
+
def run_tests(runner: Runner, tests: list[Callable], args: TestArgs) -> None:
|
639
|
+
if args.only != []:
|
640
|
+
tests = list(filter(lambda t: t.__name__ in args.only, tests))
|
641
|
+
|
642
|
+
total_time = 0.0
|
643
|
+
real_time = perf_counter()
|
644
|
+
passed = 0
|
645
|
+
total = len(tests)
|
646
|
+
|
647
|
+
def timed_test(test_func):
|
648
|
+
start_time = perf_counter()
|
649
|
+
try:
|
650
|
+
test_func()
|
651
|
+
success = True
|
652
|
+
except Exception as e:
|
653
|
+
success = False
|
654
|
+
exception = e
|
655
|
+
end_time = perf_counter()
|
656
|
+
duration = end_time - start_time
|
657
|
+
|
658
|
+
if success:
|
659
|
+
return (True, duration, None)
|
660
|
+
else:
|
661
|
+
return (False, duration, exception)
|
662
|
+
|
663
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
664
|
+
future_to_data = {}
|
665
|
+
for test in tests:
|
666
|
+
future = executor.submit(timed_test, test)
|
667
|
+
future_to_data[future] = test
|
668
|
+
|
669
|
+
index = 0
|
670
|
+
for future in concurrent.futures.as_completed(future_to_data):
|
671
|
+
test = future_to_data[future]
|
672
|
+
name = test.__name__
|
673
|
+
success, dur, exception = future.result()
|
674
|
+
total_time += dur
|
675
|
+
index += 1
|
676
|
+
|
677
|
+
if success:
|
678
|
+
passed += 1
|
679
|
+
print(
|
680
|
+
f"{name:<26} ({index}/{total}) {round(dur, 2):<4} secs [\033[1;32mPASSED\033[0m]",
|
681
|
+
flush=True,
|
682
|
+
)
|
683
|
+
else:
|
684
|
+
print(
|
685
|
+
f"{name:<26} ({index}/{total}) {round(dur, 2):<4} secs \033[1;31m[FAILED]\033[0m",
|
686
|
+
flush=True,
|
687
|
+
)
|
688
|
+
if args.no_fail_fast:
|
689
|
+
print(f"\n{exception}")
|
690
|
+
else:
|
691
|
+
print("")
|
692
|
+
raise exception
|
693
|
+
|
694
|
+
real_time = round(perf_counter() - real_time, 2)
|
695
|
+
total_time = round(total_time, 2)
|
696
|
+
print(
|
697
|
+
f"\nCompleted {passed}/{total}\nreal time: {real_time} secs total: {total_time} secs"
|
698
|
+
)
|
699
|
+
|
700
|
+
|
701
|
+
def main(sys_args: list[str] | None = None) -> None:
|
702
|
+
if sys_args is None:
|
703
|
+
sys_args = sys.argv[1:]
|
704
|
+
|
705
|
+
args = test_options(ArgumentParser("test")).parse_args(TestArgs, sys_args)
|
706
|
+
run = Runner()
|
739
707
|
tests = []
|
740
708
|
|
709
|
+
test_methods = {
|
710
|
+
name: getattr(run, name)
|
711
|
+
for name in dir(Runner)
|
712
|
+
if callable(getattr(Runner, name)) and name not in ["main", "raw", "check"]
|
713
|
+
}
|
714
|
+
|
741
715
|
if args.category in {"palet", "all"}:
|
742
|
-
tests.extend(
|
716
|
+
tests.extend(
|
717
|
+
[test_methods[name] for name in ["palet_python_bridge", "palet_scripts"]]
|
718
|
+
)
|
743
719
|
|
744
720
|
if args.category in {"sub", "all"}:
|
745
|
-
tests.extend(
|
721
|
+
tests.extend(
|
722
|
+
[test_methods[name] for name in ["info", "levels", "subdump", "desc"]]
|
723
|
+
)
|
746
724
|
|
747
725
|
if args.category in {"cli", "all"}:
|
748
726
|
tests.extend(
|
749
727
|
[
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
prores,
|
754
|
-
edit_negative_tests,
|
755
|
-
edit_positive_tests,
|
756
|
-
audio_norm_f,
|
757
|
-
audio_norm_ebu,
|
758
|
-
export_json_tests,
|
759
|
-
import_v1_tests,
|
760
|
-
high_speed_test,
|
761
|
-
video_speed,
|
762
|
-
multi_track_edit,
|
763
|
-
concat_mux_tracks,
|
764
|
-
concat_multiple_tracks,
|
765
|
-
frame_rate,
|
766
|
-
help_tests,
|
767
|
-
version_test,
|
768
|
-
parser_test,
|
769
|
-
concat,
|
770
|
-
example,
|
771
|
-
units,
|
772
|
-
sr_units,
|
773
|
-
backwards_range,
|
774
|
-
cut_out,
|
775
|
-
gif,
|
776
|
-
margin_tests,
|
777
|
-
input_extension,
|
778
|
-
output_extension,
|
779
|
-
progress,
|
780
|
-
silent_threshold,
|
781
|
-
track_tests,
|
782
|
-
codec_tests,
|
783
|
-
premiere_named_export,
|
784
|
-
export_subtitles,
|
785
|
-
export,
|
786
|
-
motion,
|
787
|
-
resolution_and_scale,
|
728
|
+
getattr(run, name)
|
729
|
+
for name in dir(Runner)
|
730
|
+
if callable(getattr(Runner, name)) and name.startswith("test_")
|
788
731
|
]
|
789
732
|
)
|
790
|
-
|
791
|
-
|
733
|
+
try:
|
734
|
+
run_tests(run, tests, args)
|
735
|
+
except KeyboardInterrupt:
|
736
|
+
print("Testing Interrupted by User.")
|
737
|
+
shutil.rmtree(run.temp_dir)
|
738
|
+
sys.exit(1)
|
739
|
+
shutil.rmtree(run.temp_dir)
|
792
740
|
|
793
741
|
|
794
742
|
if __name__ == "__main__":
|