auto-editor 27.1.1__py3-none-any.whl → 28.0.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.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +0 -8
- auto_editor/cmds/cache.py +1 -1
- auto_editor/cmds/desc.py +8 -9
- auto_editor/cmds/info.py +4 -17
- auto_editor/cmds/palet.py +1 -1
- auto_editor/cmds/subdump.py +2 -4
- auto_editor/cmds/test.py +50 -31
- auto_editor/edit.py +52 -52
- auto_editor/{formats → exports}/fcp11.py +11 -7
- auto_editor/{formats → exports}/fcp7.py +7 -239
- auto_editor/exports/json.py +32 -0
- auto_editor/{formats → exports}/shotcut.py +8 -17
- auto_editor/ffwrapper.py +1 -3
- auto_editor/help.py +6 -17
- auto_editor/imports/__init__.py +0 -0
- auto_editor/imports/fcp7.py +275 -0
- auto_editor/{formats → imports}/json.py +16 -43
- auto_editor/json.py +2 -2
- auto_editor/lang/palet.py +6 -43
- auto_editor/lang/stdenv.py +8 -20
- auto_editor/lib/contracts.py +4 -6
- auto_editor/lib/data_structs.py +0 -3
- auto_editor/make_layers.py +13 -13
- auto_editor/preview.py +1 -2
- auto_editor/render/audio.py +9 -6
- auto_editor/render/video.py +3 -2
- auto_editor/timeline.py +35 -52
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/METADATA +2 -2
- auto_editor-28.0.1.dist-info/RECORD +56 -0
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/WHEEL +1 -1
- auto_editor/formats/utils.py +0 -56
- auto_editor-27.1.1.dist-info/RECORD +0 -54
- /auto_editor/{formats → exports}/__init__.py +0 -0
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/licenses/LICENSE +0 -0
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/top_level.txt +0 -0
@@ -3,16 +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
|
-
from .utils import Validator, show
|
13
|
-
|
14
|
-
if TYPE_CHECKING:
|
15
|
-
from auto_editor.utils.log import Log
|
9
|
+
from auto_editor.timeline import Clip, v3
|
16
10
|
|
17
11
|
"""
|
18
12
|
Premiere Pro uses the Final Cut Pro 7 XML Interchange Format
|
@@ -27,28 +21,6 @@ come back the way they started.
|
|
27
21
|
DEPTH = "16"
|
28
22
|
|
29
23
|
|
30
|
-
def uri_to_path(uri: str) -> str:
|
31
|
-
urllib = __import__("urllib.parse", fromlist=["parse"])
|
32
|
-
|
33
|
-
if uri.startswith("file://localhost/"):
|
34
|
-
uri = uri[16:]
|
35
|
-
elif uri.startswith("file://"):
|
36
|
-
# Windows-style paths
|
37
|
-
if len(uri) > 8 and uri[9] == ":":
|
38
|
-
uri = uri[8:]
|
39
|
-
else:
|
40
|
-
uri = uri[7:]
|
41
|
-
else:
|
42
|
-
return uri
|
43
|
-
|
44
|
-
return urllib.parse.unquote(uri)
|
45
|
-
|
46
|
-
# /Users/wyattblue/projects/auto-editor/example.mp4
|
47
|
-
# file:///Users/wyattblue/projects/auto-editor/example.mp4
|
48
|
-
# file:///C:/Users/WyattBlue/projects/auto-editor/example.mp4
|
49
|
-
# file://localhost/Users/wyattblue/projects/auto-editor/example.mp4
|
50
|
-
|
51
|
-
|
52
24
|
def set_tb_ntsc(tb: Fraction) -> tuple[int, str]:
|
53
25
|
# See chart: https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/FrameRate/FrameRate.html#//apple_ref/doc/uid/TP30001158-TPXREF103
|
54
26
|
if tb == Fraction(24000, 1001):
|
@@ -65,19 +37,6 @@ def set_tb_ntsc(tb: Fraction) -> tuple[int, str]:
|
|
65
37
|
return int(tb), "FALSE"
|
66
38
|
|
67
39
|
|
68
|
-
def read_tb_ntsc(tb: int, ntsc: bool) -> Fraction:
|
69
|
-
if ntsc:
|
70
|
-
if tb == 24:
|
71
|
-
return Fraction(24000, 1001)
|
72
|
-
if tb == 30:
|
73
|
-
return Fraction(30000, 1001)
|
74
|
-
if tb == 60:
|
75
|
-
return Fraction(60000, 1001)
|
76
|
-
return tb * Fraction(999, 1000)
|
77
|
-
|
78
|
-
return Fraction(tb)
|
79
|
-
|
80
|
-
|
81
40
|
def speedup(speed: float) -> Element:
|
82
41
|
fil = Element("filter")
|
83
42
|
effect = ET.SubElement(fil, "effect")
|
@@ -106,200 +65,6 @@ def speedup(speed: float) -> Element:
|
|
106
65
|
return fil
|
107
66
|
|
108
67
|
|
109
|
-
SUPPORTED_EFFECTS = ("timeremap",)
|
110
|
-
|
111
|
-
|
112
|
-
def read_filters(clipitem: Element, log: Log) -> float:
|
113
|
-
for effect_tag in clipitem:
|
114
|
-
if effect_tag.tag in {"enabled", "start", "end"}:
|
115
|
-
continue
|
116
|
-
if len(effect_tag) < 3:
|
117
|
-
log.error("<effect> requires: <effectid> <name> and one <parameter>")
|
118
|
-
for i, effects in enumerate(effect_tag):
|
119
|
-
if i == 0 and effects.tag != "name":
|
120
|
-
log.error("<effect>: <name> must be first tag")
|
121
|
-
if i == 1 and effects.tag != "effectid":
|
122
|
-
log.error("<effect>: <effectid> must be second tag")
|
123
|
-
if effects.text not in SUPPORTED_EFFECTS:
|
124
|
-
log.error(f"`{effects.text}` is not a supported effect.")
|
125
|
-
|
126
|
-
if i > 1:
|
127
|
-
for j, parms in enumerate(effects):
|
128
|
-
if j == 0:
|
129
|
-
if parms.tag != "parameterid":
|
130
|
-
log.error("<parameter>: <parameterid> must be first tag")
|
131
|
-
if parms.text != "speed":
|
132
|
-
break
|
133
|
-
|
134
|
-
if j > 0 and parms.tag == "value":
|
135
|
-
if parms.text is None:
|
136
|
-
log.error("<value>: number required")
|
137
|
-
return float(parms.text) / 100
|
138
|
-
|
139
|
-
return 1.0
|
140
|
-
|
141
|
-
|
142
|
-
def fcp7_read_xml(path: str, log: Log) -> v3:
|
143
|
-
def xml_bool(val: str) -> bool:
|
144
|
-
if val == "TRUE":
|
145
|
-
return True
|
146
|
-
if val == "FALSE":
|
147
|
-
return False
|
148
|
-
raise TypeError("Value must be 'TRUE' or 'FALSE'")
|
149
|
-
|
150
|
-
try:
|
151
|
-
tree = ET.parse(path)
|
152
|
-
except FileNotFoundError:
|
153
|
-
log.error(f"Could not find '{path}'")
|
154
|
-
|
155
|
-
root = tree.getroot()
|
156
|
-
|
157
|
-
valid = Validator(log)
|
158
|
-
|
159
|
-
valid.check(root, "xmeml")
|
160
|
-
valid.check(root[0], "sequence")
|
161
|
-
result = valid.parse(
|
162
|
-
root[0],
|
163
|
-
{
|
164
|
-
"name": str,
|
165
|
-
"duration": int,
|
166
|
-
"rate": {
|
167
|
-
"timebase": Fraction,
|
168
|
-
"ntsc": xml_bool,
|
169
|
-
},
|
170
|
-
"media": None,
|
171
|
-
},
|
172
|
-
)
|
173
|
-
|
174
|
-
tb = read_tb_ntsc(result["rate"]["timebase"], result["rate"]["ntsc"])
|
175
|
-
|
176
|
-
av = valid.parse(
|
177
|
-
result["media"],
|
178
|
-
{
|
179
|
-
"video": None,
|
180
|
-
"audio": None,
|
181
|
-
},
|
182
|
-
)
|
183
|
-
|
184
|
-
sources: dict[str, FileInfo] = {}
|
185
|
-
vobjs: VSpace = []
|
186
|
-
aobjs: ASpace = []
|
187
|
-
|
188
|
-
vclip_schema = {
|
189
|
-
"format": {
|
190
|
-
"samplecharacteristics": {
|
191
|
-
"width": int,
|
192
|
-
"height": int,
|
193
|
-
},
|
194
|
-
},
|
195
|
-
"track": {
|
196
|
-
"__arr": "",
|
197
|
-
"clipitem": {
|
198
|
-
"__arr": "",
|
199
|
-
"start": int,
|
200
|
-
"end": int,
|
201
|
-
"in": int,
|
202
|
-
"out": int,
|
203
|
-
"file": None,
|
204
|
-
"filter": None,
|
205
|
-
},
|
206
|
-
},
|
207
|
-
}
|
208
|
-
|
209
|
-
aclip_schema = {
|
210
|
-
"format": {"samplecharacteristics": {"samplerate": int}},
|
211
|
-
"track": {
|
212
|
-
"__arr": "",
|
213
|
-
"clipitem": {
|
214
|
-
"__arr": "",
|
215
|
-
"start": int,
|
216
|
-
"end": int,
|
217
|
-
"in": int,
|
218
|
-
"out": int,
|
219
|
-
"file": None,
|
220
|
-
"filter": None,
|
221
|
-
},
|
222
|
-
},
|
223
|
-
}
|
224
|
-
|
225
|
-
sr = 48000
|
226
|
-
res = (1920, 1080)
|
227
|
-
|
228
|
-
if "video" in av:
|
229
|
-
tracks = valid.parse(av["video"], vclip_schema)
|
230
|
-
|
231
|
-
if "format" in tracks:
|
232
|
-
width = tracks["format"]["samplecharacteristics"]["width"]
|
233
|
-
height = tracks["format"]["samplecharacteristics"]["height"]
|
234
|
-
res = width, height
|
235
|
-
|
236
|
-
for t, track in enumerate(tracks["track"]):
|
237
|
-
if len(track["clipitem"]) > 0:
|
238
|
-
vobjs.append([])
|
239
|
-
for i, clipitem in enumerate(track["clipitem"]):
|
240
|
-
file_id = clipitem["file"].attrib["id"]
|
241
|
-
if file_id not in sources:
|
242
|
-
fileobj = valid.parse(clipitem["file"], {"pathurl": str})
|
243
|
-
|
244
|
-
if "pathurl" in fileobj:
|
245
|
-
sources[file_id] = FileInfo.init(
|
246
|
-
uri_to_path(fileobj["pathurl"]),
|
247
|
-
log,
|
248
|
-
)
|
249
|
-
else:
|
250
|
-
show(clipitem["file"], 3)
|
251
|
-
log.error(
|
252
|
-
f"'pathurl' child element not found in {clipitem['file'].tag}"
|
253
|
-
)
|
254
|
-
|
255
|
-
if "filter" in clipitem:
|
256
|
-
speed = read_filters(clipitem["filter"], log)
|
257
|
-
else:
|
258
|
-
speed = 1.0
|
259
|
-
|
260
|
-
start = clipitem["start"]
|
261
|
-
dur = clipitem["end"] - start
|
262
|
-
offset = clipitem["in"]
|
263
|
-
|
264
|
-
vobjs[t].append(
|
265
|
-
TlVideo(start, dur, sources[file_id], offset, speed, stream=0)
|
266
|
-
)
|
267
|
-
|
268
|
-
if "audio" in av:
|
269
|
-
tracks = valid.parse(av["audio"], aclip_schema)
|
270
|
-
if "format" in tracks:
|
271
|
-
sr = tracks["format"]["samplecharacteristics"]["samplerate"]
|
272
|
-
|
273
|
-
for t, track in enumerate(tracks["track"]):
|
274
|
-
if len(track["clipitem"]) > 0:
|
275
|
-
aobjs.append([])
|
276
|
-
for i, clipitem in enumerate(track["clipitem"]):
|
277
|
-
file_id = clipitem["file"].attrib["id"]
|
278
|
-
if file_id not in sources:
|
279
|
-
fileobj = valid.parse(clipitem["file"], {"pathurl": str})
|
280
|
-
sources[file_id] = FileInfo.init(
|
281
|
-
uri_to_path(fileobj["pathurl"]), log
|
282
|
-
)
|
283
|
-
|
284
|
-
if "filter" in clipitem:
|
285
|
-
speed = read_filters(clipitem["filter"], log)
|
286
|
-
else:
|
287
|
-
speed = 1.0
|
288
|
-
|
289
|
-
start = clipitem["start"]
|
290
|
-
dur = clipitem["end"] - start
|
291
|
-
offset = clipitem["in"]
|
292
|
-
|
293
|
-
aobjs[t].append(
|
294
|
-
TlAudio(
|
295
|
-
start, dur, sources[file_id], offset, speed, volume=1, stream=0
|
296
|
-
)
|
297
|
-
)
|
298
|
-
|
299
|
-
T = Template.init(sources[next(iter(sources))], sr, res=res)
|
300
|
-
return v3(tb, "#000", T, vobjs, aobjs, v1=None)
|
301
|
-
|
302
|
-
|
303
68
|
def media_def(
|
304
69
|
filedef: Element, url: str, src: FileInfo, tl: v3, tb: int, ntsc: str
|
305
70
|
) -> None:
|
@@ -472,7 +237,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
|
472
237
|
sequence = ET.SubElement(xmeml, "sequence", explodedTracks="true")
|
473
238
|
|
474
239
|
ET.SubElement(sequence, "name").text = name
|
475
|
-
ET.SubElement(sequence, "duration").text = f"{
|
240
|
+
ET.SubElement(sequence, "duration").text = f"{len(tl)}"
|
476
241
|
rate = ET.SubElement(sequence, "rate")
|
477
242
|
ET.SubElement(rate, "timebase").text = f"{timebase}"
|
478
243
|
ET.SubElement(rate, "ntsc").text = ntsc
|
@@ -493,7 +258,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
|
493
258
|
track = ET.SubElement(video, "track")
|
494
259
|
|
495
260
|
for j, clip in enumerate(tl.v[0]):
|
496
|
-
assert isinstance(clip,
|
261
|
+
assert isinstance(clip, Clip)
|
497
262
|
|
498
263
|
_start = f"{clip.start}"
|
499
264
|
_end = f"{clip.start + clip.dur}"
|
@@ -542,4 +307,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
|
542
307
|
|
543
308
|
tree = ET.ElementTree(xmeml)
|
544
309
|
ET.indent(tree, space=" ", level=0)
|
545
|
-
|
310
|
+
if output == "-":
|
311
|
+
print(ET.tostring(xmeml, encoding="unicode"))
|
312
|
+
else:
|
313
|
+
tree.write(output, xml_declaration=True, encoding="utf-8")
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import sys
|
4
|
+
from typing import TYPE_CHECKING
|
5
|
+
|
6
|
+
from auto_editor.json import dump
|
7
|
+
from auto_editor.timeline import v3
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from auto_editor.utils.log import Log
|
11
|
+
|
12
|
+
|
13
|
+
def make_json_timeline(ver: str, out: str, tl: v3, log: Log) -> None:
|
14
|
+
if ver not in {"v1", "v3"}:
|
15
|
+
log.error(f"Unknown timeline version: {ver}")
|
16
|
+
|
17
|
+
if out == "-":
|
18
|
+
outfile = sys.stdout
|
19
|
+
else:
|
20
|
+
outfile = open(out, "w")
|
21
|
+
|
22
|
+
if ver == "v3":
|
23
|
+
dump(tl.as_dict(), outfile, indent=2)
|
24
|
+
else:
|
25
|
+
if tl.v1 is None:
|
26
|
+
log.error("Timeline can't be converted to v1 format")
|
27
|
+
dump(tl.v1.as_dict(), outfile, indent=2)
|
28
|
+
|
29
|
+
if out == "-":
|
30
|
+
print("") # Flush stdout
|
31
|
+
else:
|
32
|
+
outfile.close()
|
@@ -1,17 +1,9 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
import xml.etree.ElementTree as ET
|
4
|
-
from typing import
|
2
|
+
from typing import cast
|
5
3
|
|
6
|
-
from auto_editor.timeline import v3
|
4
|
+
from auto_editor.timeline import Clip, v3
|
7
5
|
from auto_editor.utils.func import aspect_ratio, to_timecode
|
8
6
|
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
from collections.abc import Sequence
|
11
|
-
|
12
|
-
from auto_editor.timeline import TlAudio, TlVideo
|
13
|
-
from auto_editor.utils.log import Log
|
14
|
-
|
15
7
|
"""
|
16
8
|
Shotcut uses the MLT timeline format
|
17
9
|
|
@@ -21,10 +13,6 @@ https://mltframework.org/docs/mltxml/
|
|
21
13
|
"""
|
22
14
|
|
23
15
|
|
24
|
-
def shotcut_read_mlt(path: str, log: Log) -> v3:
|
25
|
-
raise NotImplementedError
|
26
|
-
|
27
|
-
|
28
16
|
def shotcut_write_mlt(output: str, tl: v3) -> None:
|
29
17
|
mlt = ET.Element(
|
30
18
|
"mlt",
|
@@ -61,7 +49,7 @@ def shotcut_write_mlt(output: str, tl: v3) -> None:
|
|
61
49
|
playlist_bin = ET.SubElement(mlt, "playlist", id="main_bin")
|
62
50
|
ET.SubElement(playlist_bin, "property", name="xml_retain").text = "1"
|
63
51
|
|
64
|
-
global_out = to_timecode(tl
|
52
|
+
global_out = to_timecode(len(tl) / tb, "standard")
|
65
53
|
|
66
54
|
producer = ET.SubElement(mlt, "producer", id="bg")
|
67
55
|
|
@@ -83,7 +71,7 @@ def shotcut_write_mlt(output: str, tl: v3) -> None:
|
|
83
71
|
producers = 0
|
84
72
|
|
85
73
|
if tl.v:
|
86
|
-
clips
|
74
|
+
clips = cast(list[Clip], tl.v[0])
|
87
75
|
elif tl.a:
|
88
76
|
clips = tl.a[0]
|
89
77
|
else:
|
@@ -154,4 +142,7 @@ def shotcut_write_mlt(output: str, tl: v3) -> None:
|
|
154
142
|
|
155
143
|
ET.indent(tree, space="\t", level=0)
|
156
144
|
|
157
|
-
|
145
|
+
if output == "-":
|
146
|
+
print(ET.tostring(mlt, encoding="unicode"))
|
147
|
+
else:
|
148
|
+
tree.write(output, xml_declaration=True, encoding="utf-8")
|
auto_editor/ffwrapper.py
CHANGED
@@ -66,7 +66,6 @@ class FileInfo:
|
|
66
66
|
path: Path
|
67
67
|
bitrate: int
|
68
68
|
duration: float
|
69
|
-
description: str | None
|
70
69
|
videos: tuple[VideoStream, ...]
|
71
70
|
audios: tuple[AudioStream, ...]
|
72
71
|
subtitles: tuple[SubtitleStream, ...]
|
@@ -166,13 +165,12 @@ class FileInfo:
|
|
166
165
|
ext = sub_exts.get(codec, "vtt")
|
167
166
|
subtitles += (SubtitleStream(codec, ext, s.language),)
|
168
167
|
|
169
|
-
desc = cont.metadata.get("description", None)
|
170
168
|
bitrate = 0 if cont.bit_rate is None else cont.bit_rate
|
171
169
|
dur = 0 if cont.duration is None else cont.duration / bv.time_base
|
172
170
|
|
173
171
|
cont.close()
|
174
172
|
|
175
|
-
return FileInfo(Path(path), bitrate, dur,
|
173
|
+
return FileInfo(Path(path), bitrate, dur, videos, audios, subtitles)
|
176
174
|
|
177
175
|
def __repr__(self) -> str:
|
178
176
|
return f"@{self.path.name}"
|
auto_editor/help.py
CHANGED
@@ -71,26 +71,15 @@ This option controls how timelines are exported.
|
|
71
71
|
|
72
72
|
Export Methods:
|
73
73
|
- default ; Export as a regular media file
|
74
|
-
|
75
74
|
- premiere ; Export as an XML timeline file for Adobe Premiere Pro
|
76
|
-
- name
|
77
|
-
|
75
|
+
- name : "Auto-Editor Media Group"
|
78
76
|
- resolve ; Export as an XML timeline file for DaVinci Resolve
|
79
|
-
- name
|
80
|
-
|
77
|
+
- name : "Auto-Editor Media Group"
|
81
78
|
- final-cut-pro ; Export as an XML timeline file for Final Cut Pro
|
82
|
-
- name
|
83
|
-
|
84
|
-
-
|
85
|
-
|
86
|
-
- json ; Export as an auto-editor JSON timeline file
|
87
|
-
- api string? : "3"
|
88
|
-
|
89
|
-
- timeline ; Print the auto-editor timeline to stdout
|
90
|
-
- api string? : "3"
|
91
|
-
|
92
|
-
- audio ; Export as a WAV audio file
|
93
|
-
|
79
|
+
- name : "Auto-Editor Media Group"
|
80
|
+
- shotcut ; Export as an XML timeline file for Shotcut
|
81
|
+
- v3 ; Export as an auto-editor v3 timeline file
|
82
|
+
- v1 ; Export as an auto-editor v1 timeline file
|
94
83
|
- clip-sequence ; Export as multiple numbered media files
|
95
84
|
|
96
85
|
""".strip(),
|
File without changes
|