auto-editor 28.0.0__py3-none-any.whl → 28.0.2__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/cmds/cache.py +1 -1
- auto_editor/cmds/info.py +3 -3
- auto_editor/cmds/levels.py +17 -9
- auto_editor/cmds/palet.py +1 -1
- auto_editor/cmds/subdump.py +2 -4
- auto_editor/cmds/test.py +15 -12
- auto_editor/edit.py +31 -28
- auto_editor/exports/fcp11.py +42 -16
- auto_editor/exports/fcp7.py +2 -39
- auto_editor/exports/json.py +39 -8
- auto_editor/exports/shotcut.py +2 -7
- auto_editor/ffwrapper.py +12 -1
- auto_editor/imports/fcp7.py +5 -7
- auto_editor/imports/json.py +5 -6
- auto_editor/json.py +2 -2
- auto_editor/lang/palet.py +5 -42
- auto_editor/lang/stdenv.py +3 -18
- auto_editor/lib/contracts.py +3 -5
- auto_editor/lib/data_structs.py +0 -3
- auto_editor/make_layers.py +13 -13
- auto_editor/preview.py +12 -21
- auto_editor/render/audio.py +8 -5
- auto_editor/render/video.py +3 -2
- auto_editor/timeline.py +12 -59
- {auto_editor-28.0.0.dist-info → auto_editor-28.0.2.dist-info}/METADATA +1 -1
- auto_editor-28.0.2.dist-info/RECORD +56 -0
- auto_editor-28.0.0.dist-info/RECORD +0 -56
- {auto_editor-28.0.0.dist-info → auto_editor-28.0.2.dist-info}/WHEEL +0 -0
- {auto_editor-28.0.0.dist-info → auto_editor-28.0.2.dist-info}/entry_points.txt +0 -0
- {auto_editor-28.0.0.dist-info → auto_editor-28.0.2.dist-info}/licenses/LICENSE +0 -0
- {auto_editor-28.0.0.dist-info → auto_editor-28.0.2.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "28.0.
|
1
|
+
__version__ = "28.0.2"
|
auto_editor/cmds/cache.py
CHANGED
@@ -26,7 +26,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
26
26
|
return
|
27
27
|
|
28
28
|
def format_bytes(size: float) -> str:
|
29
|
-
for unit in
|
29
|
+
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
30
30
|
if size < 1024:
|
31
31
|
return f"{size:.2f} {unit}"
|
32
32
|
size /= 1024
|
auto_editor/cmds/info.py
CHANGED
@@ -113,7 +113,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
113
113
|
f"{recTb.numerator}/{recTb.denominator}"
|
114
114
|
)
|
115
115
|
|
116
|
-
for
|
116
|
+
for v in src.videos:
|
117
117
|
w, h = v.width, v.height
|
118
118
|
|
119
119
|
vid: VideoJson = {
|
@@ -134,7 +134,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
134
134
|
}
|
135
135
|
file_info[file]["video"].append(vid)
|
136
136
|
|
137
|
-
for
|
137
|
+
for a in src.audios:
|
138
138
|
aud: AudioJson = {
|
139
139
|
"codec": a.codec,
|
140
140
|
"layout": a.layout,
|
@@ -145,7 +145,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
145
145
|
}
|
146
146
|
file_info[file]["audio"].append(aud)
|
147
147
|
|
148
|
-
for
|
148
|
+
for s_stream in src.subtitles:
|
149
149
|
sub: SubtitleJson = {"codec": s_stream.codec, "lang": s_stream.lang}
|
150
150
|
file_info[file]["subtitle"].append(sub)
|
151
151
|
|
auto_editor/cmds/levels.py
CHANGED
@@ -36,6 +36,7 @@ class LevelArgs:
|
|
36
36
|
input: list[str] = field(default_factory=list)
|
37
37
|
edit: str = "audio"
|
38
38
|
timebase: Fraction | None = None
|
39
|
+
no_cache: bool = False
|
39
40
|
help: bool = False
|
40
41
|
|
41
42
|
|
@@ -53,16 +54,14 @@ def levels_options(parser: ArgumentParser) -> ArgumentParser:
|
|
53
54
|
type=frame_rate,
|
54
55
|
help="Set custom timebase",
|
55
56
|
)
|
57
|
+
parser.add_argument("--no-cache", flag=True)
|
56
58
|
return parser
|
57
59
|
|
58
60
|
|
59
61
|
def print_arr(arr: NDArray) -> None:
|
60
62
|
print("")
|
61
63
|
print("@start")
|
62
|
-
if arr.dtype
|
63
|
-
for a in arr:
|
64
|
-
sys.stdout.write(f"{a:.20f}\n")
|
65
|
-
elif arr.dtype == np.bool_:
|
64
|
+
if arr.dtype == np.bool_:
|
66
65
|
for a in arr:
|
67
66
|
sys.stdout.write(f"{1 if a else 0}\n")
|
68
67
|
else:
|
@@ -76,7 +75,7 @@ def print_arr_gen(arr: Iterator[float | np.float32]) -> None:
|
|
76
75
|
print("")
|
77
76
|
print("@start")
|
78
77
|
for a in arr:
|
79
|
-
print(f"{a
|
78
|
+
print(f"{a}")
|
80
79
|
print("")
|
81
80
|
|
82
81
|
|
@@ -131,7 +130,11 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
131
130
|
levels = initLevels(src, tb, bar, False, log)
|
132
131
|
try:
|
133
132
|
if method == "audio":
|
134
|
-
if (
|
133
|
+
if (
|
134
|
+
not args.no_cache
|
135
|
+
and (arr := levels.read_cache("audio", (obj["stream"],)))
|
136
|
+
is not None
|
137
|
+
):
|
135
138
|
print_arr(arr)
|
136
139
|
else:
|
137
140
|
container = bv.open(src.path, "r")
|
@@ -148,11 +151,15 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
148
151
|
container.close()
|
149
152
|
|
150
153
|
cache_array = np.array(values, dtype=np.float32)
|
151
|
-
|
154
|
+
if not args.no_cache:
|
155
|
+
levels.cache(cache_array, "audio", (obj["stream"],))
|
152
156
|
|
153
157
|
elif method == "motion":
|
154
158
|
mobj = (obj["stream"], obj["width"], obj["blur"])
|
155
|
-
if (
|
159
|
+
if (
|
160
|
+
not args.no_cache
|
161
|
+
and (arr := levels.read_cache("motion", mobj)) is not None
|
162
|
+
):
|
156
163
|
print_arr(arr)
|
157
164
|
else:
|
158
165
|
container = bv.open(src.path, "r")
|
@@ -171,7 +178,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
171
178
|
container.close()
|
172
179
|
|
173
180
|
cache_array = np.array(values, dtype=np.float32)
|
174
|
-
|
181
|
+
if not args.no_cache:
|
182
|
+
levels.cache(cache_array, "motion", mobj)
|
175
183
|
|
176
184
|
elif method == "subtitle":
|
177
185
|
print_arr(levels.subtitle(**obj))
|
auto_editor/cmds/palet.py
CHANGED
@@ -14,7 +14,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
14
14
|
|
15
15
|
env.update(make_standard_env())
|
16
16
|
try:
|
17
|
-
interpret(env, Parser(Lexer(sys_args[0], program_text
|
17
|
+
interpret(env, Parser(Lexer(sys_args[0], program_text)))
|
18
18
|
except (MyError, ZeroDivisionError) as e:
|
19
19
|
sys.stderr.write(f"error: {e}\n")
|
20
20
|
sys.exit(1)
|
auto_editor/cmds/subdump.py
CHANGED
@@ -21,8 +21,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
21
21
|
parser.add_argument("--json", flag=True)
|
22
22
|
args = parser.parse_args(SubdumpArgs, sys_args)
|
23
23
|
|
24
|
-
do_filter = True
|
25
|
-
|
26
24
|
if args.json:
|
27
25
|
data = {}
|
28
26
|
for input_file in args.input:
|
@@ -46,7 +44,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
46
44
|
startf = round(float(start), 3)
|
47
45
|
endf = round(float(end), 3)
|
48
46
|
|
49
|
-
if
|
47
|
+
if endf - startf <= 0.02:
|
50
48
|
continue
|
51
49
|
|
52
50
|
for sub in packet.decode():
|
@@ -60,7 +58,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
60
58
|
dump(data, sys.stdout, indent=4)
|
61
59
|
return
|
62
60
|
|
63
|
-
for
|
61
|
+
for input_file in args.input:
|
64
62
|
with bv.open(input_file) as container:
|
65
63
|
for s in range(len(container.streams.subtitles)):
|
66
64
|
print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
|
auto_editor/cmds/test.py
CHANGED
@@ -202,6 +202,19 @@ class Runner:
|
|
202
202
|
assert audio.language == "eng"
|
203
203
|
assert audio.layout.name == "stereo"
|
204
204
|
|
205
|
+
def test_video_to_mp3(self) -> None:
|
206
|
+
out = self.main(["example.mp4"], [], output="example_ALTERED.mp3")
|
207
|
+
with bv.open(out) as container:
|
208
|
+
assert container.duration is not None
|
209
|
+
assert container.duration > 17300000 and container.duration < 2 << 24
|
210
|
+
|
211
|
+
assert len(container.streams) == 1
|
212
|
+
audio = container.streams[0]
|
213
|
+
assert isinstance(audio, bv.AudioStream)
|
214
|
+
assert audio.codec.name in ("mp3", "mp3float")
|
215
|
+
assert audio.sample_rate == 48000
|
216
|
+
assert audio.layout.name == "stereo"
|
217
|
+
|
205
218
|
def test_to_mono(self) -> None:
|
206
219
|
out = self.main(["example.mp4"], ["-layout", "mono"], output="example_mono.mp4")
|
207
220
|
with bv.open(out) as container:
|
@@ -516,9 +529,6 @@ class Runner:
|
|
516
529
|
assert output.videos[0].pix_fmt == "yuv420p"
|
517
530
|
|
518
531
|
def test_encode_hevc(self):
|
519
|
-
if os.environ.get("GITHUB_ACTIONS") == "true":
|
520
|
-
raise SkipTest()
|
521
|
-
|
522
532
|
out = self.main(["resources/testsrc.mp4"], ["-c:v", "hevc"], "out.mkv")
|
523
533
|
output = fileinfo(out)
|
524
534
|
assert output.videos[0].codec == "hevc"
|
@@ -563,15 +573,12 @@ class Runner:
|
|
563
573
|
("238.5", 238.5),
|
564
574
|
("-34", -34),
|
565
575
|
("-98.3", -98.3),
|
566
|
-
("+3i", 3j),
|
567
576
|
("3sec", 90),
|
568
577
|
("-3sec", -90),
|
569
578
|
("0.2sec", 6),
|
570
579
|
("(+ 4 3)", 7),
|
571
580
|
("(+ 4 3 2)", 9),
|
572
581
|
("(+ 10.5 3)", 13.5),
|
573
|
-
("(+ 3+4i -2-2i)", 1 + 2j),
|
574
|
-
("(+ 3+4i -2-2i 5)", 6 + 2j),
|
575
582
|
("(- 4 3)", 1),
|
576
583
|
("(- 3)", -3),
|
577
584
|
("(- 10.5 3)", 7.5),
|
@@ -580,7 +587,6 @@ class Runner:
|
|
580
587
|
("(/ 5)", 0.2),
|
581
588
|
("(/ 6 1)", 6.0),
|
582
589
|
("30/1", Fraction(30)),
|
583
|
-
("(sqrt -4)", 2j),
|
584
590
|
("(pow 2 3)", 8),
|
585
591
|
("(pow 4 0.5)", 2.0),
|
586
592
|
("(abs 1.0)", 1.0),
|
@@ -595,7 +601,6 @@ class Runner:
|
|
595
601
|
("(int? #t)", False),
|
596
602
|
("(int? #f)", False),
|
597
603
|
("(int? 4/5)", False),
|
598
|
-
("(int? 0+2i)", False),
|
599
604
|
('(int? "hello")', False),
|
600
605
|
('(int? "3")', False),
|
601
606
|
("(float? -23.4)", True),
|
@@ -609,7 +614,6 @@ class Runner:
|
|
609
614
|
('(define apple "Red Wood") apple', "Red Wood"),
|
610
615
|
("(= 1 1.0)", True),
|
611
616
|
("(= 1 2)", False),
|
612
|
-
("(= 2+3i 2+3i 2+3i)", True),
|
613
617
|
("(= 1)", True),
|
614
618
|
("(+)", 0),
|
615
619
|
("(*)", 1),
|
@@ -618,7 +622,6 @@ class Runner:
|
|
618
622
|
('(if #f mango "Hi")', "Hi"),
|
619
623
|
('{if (= [+ 3 4] 7) "yes" "no"}', "yes"),
|
620
624
|
("((if #t + -) 3 4)", 7),
|
621
|
-
("((if #t + oops) 3+3i 4-2i)", 7 + 1j),
|
622
625
|
("((if #f + -) 3 4)", -1),
|
623
626
|
("(when (positive? 3) 17)", 17),
|
624
627
|
("(string)", ""),
|
@@ -698,7 +701,7 @@ class Runner:
|
|
698
701
|
self.raw(["palet", "resources/scripts/testmath.pal"])
|
699
702
|
|
700
703
|
|
701
|
-
def run_tests(
|
704
|
+
def run_tests(tests: list[Callable], args: TestArgs) -> None:
|
702
705
|
if args.only != []:
|
703
706
|
tests = list(filter(lambda t: t.__name__ in args.only, tests))
|
704
707
|
|
@@ -797,7 +800,7 @@ def main(sys_args: list[str] | None = None) -> None:
|
|
797
800
|
]
|
798
801
|
)
|
799
802
|
try:
|
800
|
-
run_tests(
|
803
|
+
run_tests(tests, args)
|
801
804
|
except KeyboardInterrupt:
|
802
805
|
print("Testing Interrupted by User.")
|
803
806
|
shutil.rmtree(run.temp_dir)
|
auto_editor/edit.py
CHANGED
@@ -29,8 +29,8 @@ if TYPE_CHECKING:
|
|
29
29
|
|
30
30
|
|
31
31
|
def set_output(
|
32
|
-
out: str | None,
|
33
|
-
) -> tuple[str,
|
32
|
+
out: str | None, export: str | None, path: Path | None, log: Log
|
33
|
+
) -> tuple[str, str]:
|
34
34
|
if out is None or out == "-":
|
35
35
|
if path is None:
|
36
36
|
log.error("`--output` must be set.") # When a timeline file is the input.
|
@@ -42,33 +42,33 @@ def set_output(
|
|
42
42
|
# Use `mp4` as the default, because it is most compatible.
|
43
43
|
ext = ".mp4" if path is None else path.suffix
|
44
44
|
|
45
|
-
if
|
45
|
+
if export is None:
|
46
46
|
match ext:
|
47
47
|
case ".xml":
|
48
|
-
export
|
48
|
+
export = "premiere"
|
49
49
|
case ".fcpxml":
|
50
|
-
export =
|
50
|
+
export = "final-cut-pro"
|
51
51
|
case ".mlt":
|
52
|
-
export =
|
52
|
+
export = "shotcut"
|
53
53
|
case ".json" | ".v1":
|
54
|
-
export =
|
54
|
+
export = "v1"
|
55
55
|
case ".v3":
|
56
|
-
export =
|
56
|
+
export = "v3"
|
57
57
|
case _:
|
58
|
-
export =
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
"
|
64
|
-
|
65
|
-
"
|
66
|
-
|
67
|
-
"
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
58
|
+
export = "default"
|
59
|
+
|
60
|
+
match export:
|
61
|
+
case "premiere" | "resolve-fcp7":
|
62
|
+
ext = ".xml"
|
63
|
+
case "final-cut-pro" | "resolve":
|
64
|
+
ext = ".fcpxml"
|
65
|
+
case "shotcut":
|
66
|
+
ext = ".mlt"
|
67
|
+
case "v1":
|
68
|
+
if ext != ".json":
|
69
|
+
ext = ".v1"
|
70
|
+
case "v3":
|
71
|
+
ext = ".v3"
|
72
72
|
|
73
73
|
if out == "-":
|
74
74
|
return "-", export
|
@@ -184,9 +184,13 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
184
184
|
src = sources[0]
|
185
185
|
use_path = src.path
|
186
186
|
|
187
|
-
|
188
|
-
|
189
|
-
|
187
|
+
if args.export is None:
|
188
|
+
output, export = set_output(args.output, args.export, use_path, log)
|
189
|
+
export_ops: dict[str, Any] = {"export": export}
|
190
|
+
else:
|
191
|
+
export_ops = parse_export(args.export, log)
|
192
|
+
export = export_ops["export"]
|
193
|
+
output, _ = set_output(args.output, export, use_path, log)
|
190
194
|
|
191
195
|
if output == "-":
|
192
196
|
# When printing to stdout, silence all logs.
|
@@ -247,8 +251,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
247
251
|
if export == "resolve":
|
248
252
|
from auto_editor.exports.fcp11 import fcp11_write_xml
|
249
253
|
|
250
|
-
|
251
|
-
set_stream_to_0(src, tl, log)
|
254
|
+
set_stream_to_0(tl, log)
|
252
255
|
fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
|
253
256
|
return
|
254
257
|
|
@@ -287,7 +290,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
287
290
|
output = bv.open(output_path, "w", container_options=options)
|
288
291
|
|
289
292
|
# Setup video
|
290
|
-
if ctr.default_vid
|
293
|
+
if ctr.default_vid not in ("none", "png") and tl.v:
|
291
294
|
vframes = render_av(output, tl, args, log)
|
292
295
|
output_stream: bv.VideoStream | None
|
293
296
|
output_stream = next(vframes) # type: ignore
|
auto_editor/exports/fcp11.py
CHANGED
@@ -1,17 +1,10 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
import xml.etree.ElementTree as ET
|
4
|
-
from
|
2
|
+
from fractions import Fraction
|
5
3
|
from xml.etree.ElementTree import Element, ElementTree, SubElement, indent
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
from auto_editor.ffwrapper import FileInfo
|
12
|
-
from auto_editor.timeline import TlAudio, TlVideo, v3
|
13
|
-
from auto_editor.utils.log import Log
|
14
|
-
|
5
|
+
from auto_editor.ffwrapper import FileInfo
|
6
|
+
from auto_editor.timeline import Clip, v3
|
7
|
+
from auto_editor.utils.log import Log
|
15
8
|
|
16
9
|
"""
|
17
10
|
Export a FCPXML 11 file readable with Final Cut Pro 10.6.8 or later.
|
@@ -52,6 +45,35 @@ def make_name(src: FileInfo, tb: Fraction) -> str:
|
|
52
45
|
return "FFVideoFormatRateUndefined"
|
53
46
|
|
54
47
|
|
48
|
+
def parseSMPTE(val: str, fps: Fraction, log: Log) -> int:
|
49
|
+
if len(val) == 0:
|
50
|
+
return 0
|
51
|
+
try:
|
52
|
+
parts = val.split(":")
|
53
|
+
if len(parts) != 4:
|
54
|
+
raise ValueError(f"Invalid SMPTE format: {val}")
|
55
|
+
|
56
|
+
hours, minutes, seconds, frames = map(int, parts)
|
57
|
+
|
58
|
+
if (
|
59
|
+
hours < 0
|
60
|
+
or minutes < 0
|
61
|
+
or minutes >= 60
|
62
|
+
or seconds < 0
|
63
|
+
or seconds >= 60
|
64
|
+
or frames < 0
|
65
|
+
):
|
66
|
+
raise ValueError(f"Invalid SMPTE values: {val}")
|
67
|
+
|
68
|
+
if frames >= fps:
|
69
|
+
raise ValueError(f"Frame count {frames} exceeds fps {fps}")
|
70
|
+
|
71
|
+
total_frames = (hours * 3600 + minutes * 60 + seconds) * fps + frames
|
72
|
+
return int(round(total_frames))
|
73
|
+
except (ValueError, ZeroDivisionError) as e:
|
74
|
+
log.error(f"Cannot parse SMPTE timecode '{val}': {e}")
|
75
|
+
|
76
|
+
|
55
77
|
def fcp11_write_xml(
|
56
78
|
group_name: str, version: int, output: str, resolve: bool, tl: v3, log: Log
|
57
79
|
) -> None:
|
@@ -90,12 +112,14 @@ def fcp11_write_xml(
|
|
90
112
|
height=f"{tl.res[1]}",
|
91
113
|
colorSpace=get_colorspace(one_src),
|
92
114
|
)
|
115
|
+
|
116
|
+
startPoint = parseSMPTE(one_src.timecode, tl.tb, log)
|
93
117
|
r2 = SubElement(
|
94
118
|
resources,
|
95
119
|
"asset",
|
96
120
|
id=f"r{i * 2 + 2}",
|
97
121
|
name=one_src.path.stem,
|
98
|
-
start=
|
122
|
+
start=fraction(startPoint),
|
99
123
|
hasVideo="1" if one_src.videos else "0",
|
100
124
|
format=f"r{i * 2 + 1}",
|
101
125
|
hasAudio="1" if one_src.audios else "0",
|
@@ -121,13 +145,15 @@ def fcp11_write_xml(
|
|
121
145
|
)
|
122
146
|
spine = SubElement(sequence, "spine")
|
123
147
|
|
124
|
-
def make_clip(ref: str, clip:
|
148
|
+
def make_clip(ref: str, clip: Clip) -> None:
|
149
|
+
startPoint = parseSMPTE(clip.src.timecode, tl.tb, log)
|
150
|
+
|
125
151
|
clip_properties = {
|
126
152
|
"name": proj_name,
|
127
153
|
"ref": ref,
|
128
|
-
"offset": fraction(clip.start),
|
154
|
+
"offset": fraction(clip.start + startPoint),
|
129
155
|
"duration": fraction(clip.dur),
|
130
|
-
"start": fraction(clip.offset),
|
156
|
+
"start": fraction(clip.offset + startPoint),
|
131
157
|
"tcFormat": "NDF",
|
132
158
|
}
|
133
159
|
asset = SubElement(spine, "asset-clip", clip_properties)
|
@@ -146,7 +172,7 @@ def fcp11_write_xml(
|
|
146
172
|
)
|
147
173
|
|
148
174
|
if tl.v and tl.v[0]:
|
149
|
-
clips
|
175
|
+
clips = [clip for clip in tl.v[0] if isinstance(clip, Clip)]
|
150
176
|
elif tl.a and tl.a[0]:
|
151
177
|
clips = tl.a[0]
|
152
178
|
else:
|
auto_editor/exports/fcp7.py
CHANGED
@@ -3,14 +3,10 @@ from __future__ import annotations
|
|
3
3
|
import xml.etree.ElementTree as ET
|
4
4
|
from fractions import Fraction
|
5
5
|
from math import ceil
|
6
|
-
from typing import TYPE_CHECKING
|
7
6
|
from xml.etree.ElementTree import Element
|
8
7
|
|
9
8
|
from auto_editor.ffwrapper import FileInfo
|
10
|
-
from auto_editor.timeline import
|
11
|
-
|
12
|
-
if TYPE_CHECKING:
|
13
|
-
from auto_editor.utils.log import Log
|
9
|
+
from auto_editor.timeline import Clip, v3
|
14
10
|
|
15
11
|
"""
|
16
12
|
Premiere Pro uses the Final Cut Pro 7 XML Interchange Format
|
@@ -69,39 +65,6 @@ def speedup(speed: float) -> Element:
|
|
69
65
|
return fil
|
70
66
|
|
71
67
|
|
72
|
-
SUPPORTED_EFFECTS = ("timeremap",)
|
73
|
-
|
74
|
-
|
75
|
-
def read_filters(clipitem: Element, log: Log) -> float:
|
76
|
-
for effect_tag in clipitem:
|
77
|
-
if effect_tag.tag in {"enabled", "start", "end"}:
|
78
|
-
continue
|
79
|
-
if len(effect_tag) < 3:
|
80
|
-
log.error("<effect> requires: <effectid> <name> and one <parameter>")
|
81
|
-
for i, effects in enumerate(effect_tag):
|
82
|
-
if i == 0 and effects.tag != "name":
|
83
|
-
log.error("<effect>: <name> must be first tag")
|
84
|
-
if i == 1 and effects.tag != "effectid":
|
85
|
-
log.error("<effect>: <effectid> must be second tag")
|
86
|
-
if effects.text not in SUPPORTED_EFFECTS:
|
87
|
-
log.error(f"`{effects.text}` is not a supported effect.")
|
88
|
-
|
89
|
-
if i > 1:
|
90
|
-
for j, parms in enumerate(effects):
|
91
|
-
if j == 0:
|
92
|
-
if parms.tag != "parameterid":
|
93
|
-
log.error("<parameter>: <parameterid> must be first tag")
|
94
|
-
if parms.text != "speed":
|
95
|
-
break
|
96
|
-
|
97
|
-
if j > 0 and parms.tag == "value":
|
98
|
-
if parms.text is None:
|
99
|
-
log.error("<value>: number required")
|
100
|
-
return float(parms.text) / 100
|
101
|
-
|
102
|
-
return 1.0
|
103
|
-
|
104
|
-
|
105
68
|
def media_def(
|
106
69
|
filedef: Element, url: str, src: FileInfo, tl: v3, tb: int, ntsc: str
|
107
70
|
) -> None:
|
@@ -295,7 +258,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
|
295
258
|
track = ET.SubElement(video, "track")
|
296
259
|
|
297
260
|
for j, clip in enumerate(tl.v[0]):
|
298
|
-
assert isinstance(clip,
|
261
|
+
assert isinstance(clip, Clip)
|
299
262
|
|
300
263
|
_start = f"{clip.start}"
|
301
264
|
_end = f"{clip.start + clip.dur}"
|
auto_editor/exports/json.py
CHANGED
@@ -1,13 +1,44 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
import sys
|
4
|
-
from typing import TYPE_CHECKING
|
5
2
|
|
6
3
|
from auto_editor.json import dump
|
7
|
-
from auto_editor.timeline import v3
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
from auto_editor.timeline import Clip, v3
|
5
|
+
from auto_editor.utils.log import Log
|
6
|
+
|
7
|
+
|
8
|
+
def as_dict(self: v3) -> dict:
|
9
|
+
def aclip_to_dict(self: Clip) -> dict:
|
10
|
+
return {
|
11
|
+
"name": "audio",
|
12
|
+
"src": self.src,
|
13
|
+
"start": self.start,
|
14
|
+
"dur": self.dur,
|
15
|
+
"offset": self.offset,
|
16
|
+
"speed": self.speed,
|
17
|
+
"volume": self.volume,
|
18
|
+
"stream": self.stream,
|
19
|
+
}
|
20
|
+
|
21
|
+
v = []
|
22
|
+
a = []
|
23
|
+
for vlayer in self.v:
|
24
|
+
vb = [vobj.as_dict() for vobj in vlayer]
|
25
|
+
if vb:
|
26
|
+
v.append(vb)
|
27
|
+
for layer in self.a:
|
28
|
+
ab = [aclip_to_dict(clip) for clip in layer]
|
29
|
+
if ab:
|
30
|
+
a.append(ab)
|
31
|
+
|
32
|
+
return {
|
33
|
+
"version": "3",
|
34
|
+
"timebase": f"{self.tb.numerator}/{self.tb.denominator}",
|
35
|
+
"background": self.background,
|
36
|
+
"resolution": self.T.res,
|
37
|
+
"samplerate": self.T.sr,
|
38
|
+
"layout": self.T.layout,
|
39
|
+
"v": v,
|
40
|
+
"a": a,
|
41
|
+
}
|
11
42
|
|
12
43
|
|
13
44
|
def make_json_timeline(ver: str, out: str, tl: v3, log: Log) -> None:
|
@@ -20,7 +51,7 @@ def make_json_timeline(ver: str, out: str, tl: v3, log: Log) -> None:
|
|
20
51
|
outfile = open(out, "w")
|
21
52
|
|
22
53
|
if ver == "v3":
|
23
|
-
dump(
|
54
|
+
dump(as_dict(tl), outfile, indent=2)
|
24
55
|
else:
|
25
56
|
if tl.v1 is None:
|
26
57
|
log.error("Timeline can't be converted to v1 format")
|
auto_editor/exports/shotcut.py
CHANGED
@@ -1,13 +1,8 @@
|
|
1
1
|
import xml.etree.ElementTree as ET
|
2
|
-
from typing import TYPE_CHECKING, Any, cast
|
3
2
|
|
4
|
-
from auto_editor.timeline import
|
3
|
+
from auto_editor.timeline import Clip, v3
|
5
4
|
from auto_editor.utils.func import aspect_ratio, to_timecode
|
6
5
|
|
7
|
-
if TYPE_CHECKING:
|
8
|
-
from collections.abc import Sequence
|
9
|
-
|
10
|
-
|
11
6
|
"""
|
12
7
|
Shotcut uses the MLT timeline format
|
13
8
|
|
@@ -75,7 +70,7 @@ def shotcut_write_mlt(output: str, tl: v3) -> None:
|
|
75
70
|
producers = 0
|
76
71
|
|
77
72
|
if tl.v:
|
78
|
-
clips
|
73
|
+
clips = [clip for clip in tl.v[0] if isinstance(clip, Clip)]
|
79
74
|
elif tl.a:
|
80
75
|
clips = tl.a[0]
|
81
76
|
else:
|
auto_editor/ffwrapper.py
CHANGED
@@ -66,6 +66,7 @@ class FileInfo:
|
|
66
66
|
path: Path
|
67
67
|
bitrate: int
|
68
68
|
duration: float
|
69
|
+
timecode: str # in SMPTE
|
69
70
|
videos: tuple[VideoStream, ...]
|
70
71
|
audios: tuple[AudioStream, ...]
|
71
72
|
subtitles: tuple[SubtitleStream, ...]
|
@@ -165,12 +166,22 @@ class FileInfo:
|
|
165
166
|
ext = sub_exts.get(codec, "vtt")
|
166
167
|
subtitles += (SubtitleStream(codec, ext, s.language),)
|
167
168
|
|
169
|
+
def get_timecode() -> str:
|
170
|
+
for d in cont.streams.data:
|
171
|
+
if (result := d.metadata.get("timecode")) is not None:
|
172
|
+
return result
|
173
|
+
for v in cont.streams.video:
|
174
|
+
if (result := v.metadata.get("timecode")) is not None:
|
175
|
+
return result
|
176
|
+
return "00:00:00:00"
|
177
|
+
|
178
|
+
timecode = get_timecode()
|
168
179
|
bitrate = 0 if cont.bit_rate is None else cont.bit_rate
|
169
180
|
dur = 0 if cont.duration is None else cont.duration / bv.time_base
|
170
181
|
|
171
182
|
cont.close()
|
172
183
|
|
173
|
-
return FileInfo(Path(path), bitrate, dur, videos, audios, subtitles)
|
184
|
+
return FileInfo(Path(path), bitrate, dur, timecode, videos, audios, subtitles)
|
174
185
|
|
175
186
|
def __repr__(self) -> str:
|
176
187
|
return f"@{self.path.name}"
|