auto-editor 27.1.0__py3-none-any.whl → 28.0.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.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +0 -8
- auto_editor/cmds/desc.py +8 -9
- auto_editor/cmds/info.py +1 -14
- auto_editor/cmds/test.py +35 -19
- auto_editor/edit.py +64 -58
- auto_editor/{formats → exports}/fcp11.py +6 -2
- auto_editor/{formats → exports}/fcp7.py +6 -241
- auto_editor/exports/json.py +32 -0
- auto_editor/{formats → exports}/shotcut.py +6 -11
- auto_editor/ffwrapper.py +1 -3
- auto_editor/help.py +6 -17
- auto_editor/imports/__init__.py +0 -0
- auto_editor/imports/fcp7.py +277 -0
- auto_editor/{formats → imports}/json.py +11 -37
- auto_editor/lang/palet.py +1 -1
- auto_editor/lang/stdenv.py +5 -2
- auto_editor/lib/contracts.py +1 -1
- auto_editor/preview.py +1 -2
- auto_editor/render/audio.py +1 -1
- auto_editor/timeline.py +16 -18
- {auto_editor-27.1.0.dist-info → auto_editor-28.0.0.dist-info}/METADATA +2 -2
- {auto_editor-27.1.0.dist-info → auto_editor-28.0.0.dist-info}/RECORD +28 -26
- {auto_editor-27.1.0.dist-info → auto_editor-28.0.0.dist-info}/WHEEL +1 -1
- auto_editor/formats/utils.py +0 -56
- /auto_editor/{formats → exports}/__init__.py +0 -0
- {auto_editor-27.1.0.dist-info → auto_editor-28.0.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-27.1.0.dist-info → auto_editor-28.0.0.dist-info}/licenses/LICENSE +0 -0
- {auto_editor-27.1.0.dist-info → auto_editor-28.0.0.dist-info}/top_level.txt +0 -0
@@ -2,15 +2,12 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import xml.etree.ElementTree as ET
|
4
4
|
from fractions import Fraction
|
5
|
-
from io import StringIO
|
6
5
|
from math import ceil
|
7
6
|
from typing import TYPE_CHECKING
|
8
7
|
from xml.etree.ElementTree import Element
|
9
8
|
|
10
9
|
from auto_editor.ffwrapper import FileInfo
|
11
|
-
from auto_editor.timeline import
|
12
|
-
|
13
|
-
from .utils import Validator, show
|
10
|
+
from auto_editor.timeline import TlVideo, v3
|
14
11
|
|
15
12
|
if TYPE_CHECKING:
|
16
13
|
from auto_editor.utils.log import Log
|
@@ -28,67 +25,6 @@ come back the way they started.
|
|
28
25
|
DEPTH = "16"
|
29
26
|
|
30
27
|
|
31
|
-
def uri_to_path(uri: str) -> str:
|
32
|
-
def de_norm(s: str) -> str:
|
33
|
-
uri_escape = {
|
34
|
-
"3C": "<",
|
35
|
-
"3E": ">",
|
36
|
-
"23": "#",
|
37
|
-
"25": "%",
|
38
|
-
"2B": "+",
|
39
|
-
"7B": "{",
|
40
|
-
"7D": "}",
|
41
|
-
"7C": "|",
|
42
|
-
"5C": "\\",
|
43
|
-
"5E": "^",
|
44
|
-
"7E": "~",
|
45
|
-
"5B": "[",
|
46
|
-
"5D": "]",
|
47
|
-
"60": "`",
|
48
|
-
"3F": "?",
|
49
|
-
"3A": ":",
|
50
|
-
"40": "@",
|
51
|
-
"3D": "=",
|
52
|
-
"2A": "*",
|
53
|
-
"29": ")",
|
54
|
-
"28": "(",
|
55
|
-
"27": "'",
|
56
|
-
"26": "&",
|
57
|
-
"24": "$",
|
58
|
-
"22": '"',
|
59
|
-
"21": "!",
|
60
|
-
"20": " ",
|
61
|
-
}
|
62
|
-
buf = StringIO()
|
63
|
-
i = 0
|
64
|
-
while i < len(s):
|
65
|
-
if s[i] == "%" and len(s) > i + 3:
|
66
|
-
tag = s[i + 1 : i + 3]
|
67
|
-
if tag in uri_escape:
|
68
|
-
buf.write(uri_escape[tag])
|
69
|
-
i += 3
|
70
|
-
else:
|
71
|
-
buf.write(s[i])
|
72
|
-
i += 1
|
73
|
-
else:
|
74
|
-
buf.write(s[i])
|
75
|
-
i += 1
|
76
|
-
return buf.getvalue()
|
77
|
-
|
78
|
-
if uri.startswith("file://localhost/"):
|
79
|
-
return de_norm(uri[16:])
|
80
|
-
if uri.startswith("file://"):
|
81
|
-
if uri[9] == ":": # Handle Windows-style paths
|
82
|
-
return de_norm(uri[8:])
|
83
|
-
return de_norm(uri[7:])
|
84
|
-
return uri
|
85
|
-
|
86
|
-
# /Users/wyattblue/projects/auto-editor/example.mp4
|
87
|
-
# file:///Users/wyattblue/projects/auto-editor/example.mp4
|
88
|
-
# file:///C:/Users/WyattBlue/projects/auto-editor/example.mp4
|
89
|
-
# file://localhost/Users/wyattblue/projects/auto-editor/example.mp4
|
90
|
-
|
91
|
-
|
92
28
|
def set_tb_ntsc(tb: Fraction) -> tuple[int, str]:
|
93
29
|
# See chart: https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/FrameRate/FrameRate.html#//apple_ref/doc/uid/TP30001158-TPXREF103
|
94
30
|
if tb == Fraction(24000, 1001):
|
@@ -105,19 +41,6 @@ def set_tb_ntsc(tb: Fraction) -> tuple[int, str]:
|
|
105
41
|
return int(tb), "FALSE"
|
106
42
|
|
107
43
|
|
108
|
-
def read_tb_ntsc(tb: int, ntsc: bool) -> Fraction:
|
109
|
-
if ntsc:
|
110
|
-
if tb == 24:
|
111
|
-
return Fraction(24000, 1001)
|
112
|
-
if tb == 30:
|
113
|
-
return Fraction(30000, 1001)
|
114
|
-
if tb == 60:
|
115
|
-
return Fraction(60000, 1001)
|
116
|
-
return tb * Fraction(999, 1000)
|
117
|
-
|
118
|
-
return Fraction(tb)
|
119
|
-
|
120
|
-
|
121
44
|
def speedup(speed: float) -> Element:
|
122
45
|
fil = Element("filter")
|
123
46
|
effect = ET.SubElement(fil, "effect")
|
@@ -179,167 +102,6 @@ def read_filters(clipitem: Element, log: Log) -> float:
|
|
179
102
|
return 1.0
|
180
103
|
|
181
104
|
|
182
|
-
def fcp7_read_xml(path: str, log: Log) -> v3:
|
183
|
-
def xml_bool(val: str) -> bool:
|
184
|
-
if val == "TRUE":
|
185
|
-
return True
|
186
|
-
if val == "FALSE":
|
187
|
-
return False
|
188
|
-
raise TypeError("Value must be 'TRUE' or 'FALSE'")
|
189
|
-
|
190
|
-
try:
|
191
|
-
tree = ET.parse(path)
|
192
|
-
except FileNotFoundError:
|
193
|
-
log.error(f"Could not find '{path}'")
|
194
|
-
|
195
|
-
root = tree.getroot()
|
196
|
-
|
197
|
-
valid = Validator(log)
|
198
|
-
|
199
|
-
valid.check(root, "xmeml")
|
200
|
-
valid.check(root[0], "sequence")
|
201
|
-
result = valid.parse(
|
202
|
-
root[0],
|
203
|
-
{
|
204
|
-
"name": str,
|
205
|
-
"duration": int,
|
206
|
-
"rate": {
|
207
|
-
"timebase": Fraction,
|
208
|
-
"ntsc": xml_bool,
|
209
|
-
},
|
210
|
-
"media": None,
|
211
|
-
},
|
212
|
-
)
|
213
|
-
|
214
|
-
tb = read_tb_ntsc(result["rate"]["timebase"], result["rate"]["ntsc"])
|
215
|
-
|
216
|
-
av = valid.parse(
|
217
|
-
result["media"],
|
218
|
-
{
|
219
|
-
"video": None,
|
220
|
-
"audio": None,
|
221
|
-
},
|
222
|
-
)
|
223
|
-
|
224
|
-
sources: dict[str, FileInfo] = {}
|
225
|
-
vobjs: VSpace = []
|
226
|
-
aobjs: ASpace = []
|
227
|
-
|
228
|
-
vclip_schema = {
|
229
|
-
"format": {
|
230
|
-
"samplecharacteristics": {
|
231
|
-
"width": int,
|
232
|
-
"height": int,
|
233
|
-
},
|
234
|
-
},
|
235
|
-
"track": {
|
236
|
-
"__arr": "",
|
237
|
-
"clipitem": {
|
238
|
-
"__arr": "",
|
239
|
-
"start": int,
|
240
|
-
"end": int,
|
241
|
-
"in": int,
|
242
|
-
"out": int,
|
243
|
-
"file": None,
|
244
|
-
"filter": None,
|
245
|
-
},
|
246
|
-
},
|
247
|
-
}
|
248
|
-
|
249
|
-
aclip_schema = {
|
250
|
-
"format": {"samplecharacteristics": {"samplerate": int}},
|
251
|
-
"track": {
|
252
|
-
"__arr": "",
|
253
|
-
"clipitem": {
|
254
|
-
"__arr": "",
|
255
|
-
"start": int,
|
256
|
-
"end": int,
|
257
|
-
"in": int,
|
258
|
-
"out": int,
|
259
|
-
"file": None,
|
260
|
-
"filter": None,
|
261
|
-
},
|
262
|
-
},
|
263
|
-
}
|
264
|
-
|
265
|
-
sr = 48000
|
266
|
-
res = (1920, 1080)
|
267
|
-
|
268
|
-
if "video" in av:
|
269
|
-
tracks = valid.parse(av["video"], vclip_schema)
|
270
|
-
|
271
|
-
if "format" in tracks:
|
272
|
-
width = tracks["format"]["samplecharacteristics"]["width"]
|
273
|
-
height = tracks["format"]["samplecharacteristics"]["height"]
|
274
|
-
res = width, height
|
275
|
-
|
276
|
-
for t, track in enumerate(tracks["track"]):
|
277
|
-
if len(track["clipitem"]) > 0:
|
278
|
-
vobjs.append([])
|
279
|
-
for i, clipitem in enumerate(track["clipitem"]):
|
280
|
-
file_id = clipitem["file"].attrib["id"]
|
281
|
-
if file_id not in sources:
|
282
|
-
fileobj = valid.parse(clipitem["file"], {"pathurl": str})
|
283
|
-
|
284
|
-
if "pathurl" in fileobj:
|
285
|
-
sources[file_id] = FileInfo.init(
|
286
|
-
uri_to_path(fileobj["pathurl"]),
|
287
|
-
log,
|
288
|
-
)
|
289
|
-
else:
|
290
|
-
show(clipitem["file"], 3)
|
291
|
-
log.error(
|
292
|
-
f"'pathurl' child element not found in {clipitem['file'].tag}"
|
293
|
-
)
|
294
|
-
|
295
|
-
if "filter" in clipitem:
|
296
|
-
speed = read_filters(clipitem["filter"], log)
|
297
|
-
else:
|
298
|
-
speed = 1.0
|
299
|
-
|
300
|
-
start = clipitem["start"]
|
301
|
-
dur = clipitem["end"] - start
|
302
|
-
offset = clipitem["in"]
|
303
|
-
|
304
|
-
vobjs[t].append(
|
305
|
-
TlVideo(start, dur, sources[file_id], offset, speed, stream=0)
|
306
|
-
)
|
307
|
-
|
308
|
-
if "audio" in av:
|
309
|
-
tracks = valid.parse(av["audio"], aclip_schema)
|
310
|
-
if "format" in tracks:
|
311
|
-
sr = tracks["format"]["samplecharacteristics"]["samplerate"]
|
312
|
-
|
313
|
-
for t, track in enumerate(tracks["track"]):
|
314
|
-
if len(track["clipitem"]) > 0:
|
315
|
-
aobjs.append([])
|
316
|
-
for i, clipitem in enumerate(track["clipitem"]):
|
317
|
-
file_id = clipitem["file"].attrib["id"]
|
318
|
-
if file_id not in sources:
|
319
|
-
fileobj = valid.parse(clipitem["file"], {"pathurl": str})
|
320
|
-
sources[file_id] = FileInfo.init(
|
321
|
-
uri_to_path(fileobj["pathurl"]), log
|
322
|
-
)
|
323
|
-
|
324
|
-
if "filter" in clipitem:
|
325
|
-
speed = read_filters(clipitem["filter"], log)
|
326
|
-
else:
|
327
|
-
speed = 1.0
|
328
|
-
|
329
|
-
start = clipitem["start"]
|
330
|
-
dur = clipitem["end"] - start
|
331
|
-
offset = clipitem["in"]
|
332
|
-
|
333
|
-
aobjs[t].append(
|
334
|
-
TlAudio(
|
335
|
-
start, dur, sources[file_id], offset, speed, volume=1, stream=0
|
336
|
-
)
|
337
|
-
)
|
338
|
-
|
339
|
-
T = Template.init(sources[next(iter(sources))], sr, res=res)
|
340
|
-
return v3(tb, "#000", T, vobjs, aobjs, v1=None)
|
341
|
-
|
342
|
-
|
343
105
|
def media_def(
|
344
106
|
filedef: Element, url: str, src: FileInfo, tl: v3, tb: int, ntsc: str
|
345
107
|
) -> None:
|
@@ -512,7 +274,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
|
512
274
|
sequence = ET.SubElement(xmeml, "sequence", explodedTracks="true")
|
513
275
|
|
514
276
|
ET.SubElement(sequence, "name").text = name
|
515
|
-
ET.SubElement(sequence, "duration").text = f"{
|
277
|
+
ET.SubElement(sequence, "duration").text = f"{len(tl)}"
|
516
278
|
rate = ET.SubElement(sequence, "rate")
|
517
279
|
ET.SubElement(rate, "timebase").text = f"{timebase}"
|
518
280
|
ET.SubElement(rate, "ntsc").text = ntsc
|
@@ -582,4 +344,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
|
582
344
|
|
583
345
|
tree = ET.ElementTree(xmeml)
|
584
346
|
ET.indent(tree, space=" ", level=0)
|
585
|
-
|
347
|
+
if output == "-":
|
348
|
+
print(ET.tostring(xmeml, encoding="unicode"))
|
349
|
+
else:
|
350
|
+
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,16 +1,12 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
import xml.etree.ElementTree as ET
|
4
2
|
from typing import TYPE_CHECKING, Any, cast
|
5
3
|
|
6
|
-
from auto_editor.timeline import v3
|
4
|
+
from auto_editor.timeline import TlAudio, TlVideo, v3
|
7
5
|
from auto_editor.utils.func import aspect_ratio, to_timecode
|
8
6
|
|
9
7
|
if TYPE_CHECKING:
|
10
8
|
from collections.abc import Sequence
|
11
9
|
|
12
|
-
from auto_editor.timeline import TlAudio, TlVideo
|
13
|
-
from auto_editor.utils.log import Log
|
14
10
|
|
15
11
|
"""
|
16
12
|
Shotcut uses the MLT timeline format
|
@@ -21,10 +17,6 @@ https://mltframework.org/docs/mltxml/
|
|
21
17
|
"""
|
22
18
|
|
23
19
|
|
24
|
-
def shotcut_read_mlt(path: str, log: Log) -> v3:
|
25
|
-
raise NotImplementedError
|
26
|
-
|
27
|
-
|
28
20
|
def shotcut_write_mlt(output: str, tl: v3) -> None:
|
29
21
|
mlt = ET.Element(
|
30
22
|
"mlt",
|
@@ -61,7 +53,7 @@ def shotcut_write_mlt(output: str, tl: v3) -> None:
|
|
61
53
|
playlist_bin = ET.SubElement(mlt, "playlist", id="main_bin")
|
62
54
|
ET.SubElement(playlist_bin, "property", name="xml_retain").text = "1"
|
63
55
|
|
64
|
-
global_out = to_timecode(tl
|
56
|
+
global_out = to_timecode(len(tl) / tb, "standard")
|
65
57
|
|
66
58
|
producer = ET.SubElement(mlt, "producer", id="bg")
|
67
59
|
|
@@ -154,4 +146,7 @@ def shotcut_write_mlt(output: str, tl: v3) -> None:
|
|
154
146
|
|
155
147
|
ET.indent(tree, space="\t", level=0)
|
156
148
|
|
157
|
-
|
149
|
+
if output == "-":
|
150
|
+
print(ET.tostring(mlt, encoding="unicode"))
|
151
|
+
else:
|
152
|
+
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
|