auto-editor 28.1.0__py3-none-any.whl → 29.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-28.1.0.dist-info → auto_editor-29.0.0.dist-info}/METADATA +4 -3
- auto_editor-29.0.0.dist-info/RECORD +5 -0
- auto_editor-29.0.0.dist-info/top_level.txt +1 -0
- auto_editor/__init__.py +0 -1
- auto_editor/__main__.py +0 -504
- auto_editor/analyze.py +0 -393
- auto_editor/cmds/__init__.py +0 -0
- auto_editor/cmds/cache.py +0 -69
- auto_editor/cmds/desc.py +0 -32
- auto_editor/cmds/info.py +0 -213
- auto_editor/cmds/levels.py +0 -199
- auto_editor/cmds/palet.py +0 -29
- auto_editor/cmds/repl.py +0 -113
- auto_editor/cmds/subdump.py +0 -72
- auto_editor/cmds/test.py +0 -816
- auto_editor/edit.py +0 -560
- auto_editor/exports/__init__.py +0 -0
- auto_editor/exports/fcp11.py +0 -195
- auto_editor/exports/fcp7.py +0 -313
- auto_editor/exports/json.py +0 -63
- auto_editor/exports/kdenlive.py +0 -322
- auto_editor/exports/shotcut.py +0 -147
- auto_editor/ffwrapper.py +0 -187
- auto_editor/help.py +0 -224
- auto_editor/imports/__init__.py +0 -0
- auto_editor/imports/fcp7.py +0 -275
- auto_editor/imports/json.py +0 -234
- auto_editor/json.py +0 -297
- auto_editor/lang/__init__.py +0 -0
- auto_editor/lang/libintrospection.py +0 -10
- auto_editor/lang/libmath.py +0 -23
- auto_editor/lang/palet.py +0 -724
- auto_editor/lang/stdenv.py +0 -1179
- auto_editor/lib/__init__.py +0 -0
- auto_editor/lib/contracts.py +0 -235
- auto_editor/lib/data_structs.py +0 -278
- auto_editor/lib/err.py +0 -2
- auto_editor/make_layers.py +0 -315
- auto_editor/preview.py +0 -93
- auto_editor/render/__init__.py +0 -0
- auto_editor/render/audio.py +0 -517
- auto_editor/render/subtitle.py +0 -205
- auto_editor/render/video.py +0 -307
- auto_editor/timeline.py +0 -331
- auto_editor/utils/__init__.py +0 -0
- auto_editor/utils/bar.py +0 -142
- auto_editor/utils/chunks.py +0 -2
- auto_editor/utils/cmdkw.py +0 -206
- auto_editor/utils/container.py +0 -101
- auto_editor/utils/func.py +0 -128
- auto_editor/utils/log.py +0 -126
- auto_editor/utils/types.py +0 -277
- auto_editor/vanparse.py +0 -313
- auto_editor-28.1.0.dist-info/RECORD +0 -57
- auto_editor-28.1.0.dist-info/entry_points.txt +0 -6
- auto_editor-28.1.0.dist-info/top_level.txt +0 -2
- docs/build.py +0 -70
- {auto_editor-28.1.0.dist-info → auto_editor-29.0.0.dist-info}/WHEEL +0 -0
- {auto_editor-28.1.0.dist-info → auto_editor-29.0.0.dist-info}/licenses/LICENSE +0 -0
auto_editor/exports/kdenlive.py
DELETED
@@ -1,322 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import xml.etree.ElementTree as ET
|
3
|
-
from os import getcwd
|
4
|
-
from uuid import uuid4
|
5
|
-
|
6
|
-
from auto_editor.timeline import Clip, v3
|
7
|
-
from auto_editor.utils.func import aspect_ratio, to_timecode
|
8
|
-
|
9
|
-
"""
|
10
|
-
kdenlive uses the MLT timeline format
|
11
|
-
|
12
|
-
See docs here:
|
13
|
-
https://mltframework.org/docs/mltxml/
|
14
|
-
|
15
|
-
kdenlive specifics:
|
16
|
-
https://github.com/KDE/kdenlive/blob/master/dev-docs/fileformat.md
|
17
|
-
"""
|
18
|
-
|
19
|
-
|
20
|
-
def kdenlive_write(output: str, tl: v3) -> None:
|
21
|
-
mlt = ET.Element(
|
22
|
-
"mlt",
|
23
|
-
attrib={
|
24
|
-
"LC_NUMERIC": "C",
|
25
|
-
"version": "7.22.0",
|
26
|
-
"producer": "main_bin",
|
27
|
-
"root": f"{getcwd()}",
|
28
|
-
},
|
29
|
-
)
|
30
|
-
|
31
|
-
width, height = tl.res
|
32
|
-
num, den = aspect_ratio(width, height)
|
33
|
-
tb = tl.tb
|
34
|
-
seq_uuid = uuid4()
|
35
|
-
|
36
|
-
ET.SubElement(
|
37
|
-
mlt,
|
38
|
-
"profile",
|
39
|
-
attrib={
|
40
|
-
"description": "automatic",
|
41
|
-
"width": f"{width}",
|
42
|
-
"height": f"{height}",
|
43
|
-
"progressive": "1",
|
44
|
-
"sample_aspect_num": "1",
|
45
|
-
"sample_aspect_den": "1",
|
46
|
-
"display_aspect_num": f"{num}",
|
47
|
-
"display_aspect_den": f"{den}",
|
48
|
-
"frame_rate_num": f"{tb.numerator}",
|
49
|
-
"frame_rate_den": f"{tb.denominator}",
|
50
|
-
"colorspace": "709",
|
51
|
-
},
|
52
|
-
)
|
53
|
-
|
54
|
-
# Reserved producer0
|
55
|
-
global_out = to_timecode(len(tl) / tb, "standard")
|
56
|
-
producer = ET.SubElement(mlt, "producer", id="producer0")
|
57
|
-
ET.SubElement(producer, "property", name="length").text = global_out
|
58
|
-
ET.SubElement(producer, "property", name="eof").text = "continue"
|
59
|
-
ET.SubElement(producer, "property", name="resource").text = "black"
|
60
|
-
ET.SubElement(producer, "property", name="mlt_service").text = "color"
|
61
|
-
ET.SubElement(producer, "property", name="kdenlive:playlistid").text = "black_track"
|
62
|
-
ET.SubElement(producer, "property", name="mlt_image_format").text = "rgba"
|
63
|
-
ET.SubElement(producer, "property", name="aspect_ratio").text = "1"
|
64
|
-
|
65
|
-
# Get all clips
|
66
|
-
if tl.v:
|
67
|
-
clips = [clip for clip in tl.v[0] if isinstance(clip, Clip)]
|
68
|
-
elif tl.a:
|
69
|
-
clips = tl.a[0]
|
70
|
-
else:
|
71
|
-
clips = []
|
72
|
-
|
73
|
-
source_ids = {}
|
74
|
-
source_id = 4
|
75
|
-
clip_playlists = []
|
76
|
-
chains = 0
|
77
|
-
playlists = 0
|
78
|
-
producers = 1
|
79
|
-
a_channels = len(tl.a)
|
80
|
-
v_channels = len(tl.v)
|
81
|
-
warped_clips = [i for i, clip in enumerate(clips) if clip.speed != 1]
|
82
|
-
|
83
|
-
# create all producers for warped clips
|
84
|
-
for clip_idx in warped_clips:
|
85
|
-
for i in range(a_channels + v_channels):
|
86
|
-
clip = clips[clip_idx]
|
87
|
-
path = str(clip.src.path)
|
88
|
-
|
89
|
-
if path not in source_ids:
|
90
|
-
source_ids[path] = str(source_id)
|
91
|
-
source_id += 1
|
92
|
-
|
93
|
-
prod = ET.SubElement(
|
94
|
-
mlt,
|
95
|
-
"producer",
|
96
|
-
attrib={
|
97
|
-
"id": f"producer{producers}",
|
98
|
-
"in": "00:00:00.000",
|
99
|
-
"out": global_out,
|
100
|
-
},
|
101
|
-
)
|
102
|
-
ET.SubElement(
|
103
|
-
prod, "property", name="resource"
|
104
|
-
).text = f"{clip.speed}:{path}"
|
105
|
-
ET.SubElement(prod, "property", name="warp_speed").text = str(clip.speed)
|
106
|
-
ET.SubElement(prod, "property", name="warp_resource").text = path
|
107
|
-
ET.SubElement(prod, "property", name="warp_pitch").text = "0"
|
108
|
-
ET.SubElement(prod, "property", name="mlt_service").text = "timewarp"
|
109
|
-
ET.SubElement(prod, "property", name="kdenlive:id").text = source_ids[path]
|
110
|
-
|
111
|
-
if i < a_channels:
|
112
|
-
ET.SubElement(prod, "property", name="vstream").text = "0"
|
113
|
-
ET.SubElement(prod, "property", name="astream").text = str(
|
114
|
-
a_channels - 1 - i
|
115
|
-
)
|
116
|
-
ET.SubElement(prod, "property", name="set.test_audio").text = "0"
|
117
|
-
ET.SubElement(prod, "property", name="set.test_video").text = "1"
|
118
|
-
else:
|
119
|
-
ET.SubElement(prod, "property", name="vstream").text = str(
|
120
|
-
v_channels - 1 - (i - a_channels)
|
121
|
-
)
|
122
|
-
ET.SubElement(prod, "property", name="astream").text = "0"
|
123
|
-
ET.SubElement(prod, "property", name="set.test_audio").text = "1"
|
124
|
-
ET.SubElement(prod, "property", name="set.test_video").text = "0"
|
125
|
-
|
126
|
-
producers += 1
|
127
|
-
|
128
|
-
# create chains, playlists and tractors for audio channels
|
129
|
-
for i, audio in enumerate(tl.a):
|
130
|
-
path = str(audio[0].src.path)
|
131
|
-
|
132
|
-
if path not in source_ids:
|
133
|
-
source_ids[path] = str(source_id)
|
134
|
-
source_id += 1
|
135
|
-
|
136
|
-
chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
|
137
|
-
ET.SubElement(chain, "property", name="resource").text = path
|
138
|
-
ET.SubElement(
|
139
|
-
chain, "property", name="mlt_service"
|
140
|
-
).text = "avformat-novalidate"
|
141
|
-
ET.SubElement(chain, "property", name="vstream").text = "0"
|
142
|
-
ET.SubElement(chain, "property", name="astream").text = str(a_channels - 1 - i)
|
143
|
-
ET.SubElement(chain, "property", name="set.test_audio").text = "0"
|
144
|
-
ET.SubElement(chain, "property", name="set.test_video").text = "1"
|
145
|
-
ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
|
146
|
-
|
147
|
-
for _i in range(2):
|
148
|
-
playlist = ET.SubElement(mlt, "playlist", id=f"playlist{playlists}")
|
149
|
-
clip_playlists.append(playlist)
|
150
|
-
ET.SubElement(playlist, "property", name="kdenlive:audio_track").text = "1"
|
151
|
-
playlists += 1
|
152
|
-
|
153
|
-
tractor = ET.SubElement(
|
154
|
-
mlt,
|
155
|
-
"tractor",
|
156
|
-
attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
|
157
|
-
)
|
158
|
-
ET.SubElement(tractor, "property", name="kdenlive:audio_track").text = "1"
|
159
|
-
ET.SubElement(tractor, "property", name="kdenlive:timeline_active").text = "1"
|
160
|
-
ET.SubElement(tractor, "property", name="kdenlive:audio_rec")
|
161
|
-
ET.SubElement(
|
162
|
-
tractor,
|
163
|
-
"track",
|
164
|
-
attrib={"hide": "video", "producer": f"playlist{playlists - 2}"},
|
165
|
-
)
|
166
|
-
ET.SubElement(
|
167
|
-
tractor,
|
168
|
-
"track",
|
169
|
-
attrib={"hide": "video", "producer": f"playlist{playlists - 1}"},
|
170
|
-
)
|
171
|
-
chains += 1
|
172
|
-
|
173
|
-
# create chains, playlists and tractors for video channels
|
174
|
-
for i, video in enumerate(tl.v):
|
175
|
-
path = f"{video[0].src.path}" # type: ignore
|
176
|
-
|
177
|
-
if path not in source_ids:
|
178
|
-
source_ids[path] = str(source_id)
|
179
|
-
source_id += 1
|
180
|
-
|
181
|
-
chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
|
182
|
-
ET.SubElement(chain, "property", name="resource").text = path
|
183
|
-
ET.SubElement(
|
184
|
-
chain, "property", name="mlt_service"
|
185
|
-
).text = "avformat-novalidate"
|
186
|
-
ET.SubElement(chain, "property", name="vstream").text = str(v_channels - 1 - i)
|
187
|
-
ET.SubElement(chain, "property", name="astream").text = "0"
|
188
|
-
ET.SubElement(chain, "property", name="set.test_audio").text = "1"
|
189
|
-
ET.SubElement(chain, "property", name="set.test_video").text = "0"
|
190
|
-
ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
|
191
|
-
|
192
|
-
for _i in range(2):
|
193
|
-
playlist = ET.SubElement(mlt, "playlist", id=f"playlist{playlists}")
|
194
|
-
clip_playlists.append(playlist)
|
195
|
-
playlists += 1
|
196
|
-
|
197
|
-
tractor = ET.SubElement(
|
198
|
-
mlt,
|
199
|
-
"tractor",
|
200
|
-
attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
|
201
|
-
)
|
202
|
-
ET.SubElement(tractor, "property", name="kdenlive:timeline_active").text = "1"
|
203
|
-
ET.SubElement(
|
204
|
-
tractor,
|
205
|
-
"track",
|
206
|
-
attrib={"hide": "audio", "producer": f"playlist{playlists - 2}"},
|
207
|
-
)
|
208
|
-
ET.SubElement(
|
209
|
-
tractor,
|
210
|
-
"track",
|
211
|
-
attrib={"hide": "audio", "producer": f"playlist{playlists - 1}"},
|
212
|
-
)
|
213
|
-
chains += 1
|
214
|
-
|
215
|
-
# final chain for the project bin
|
216
|
-
path = str(clips[0].src.path)
|
217
|
-
chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
|
218
|
-
ET.SubElement(chain, "property", name="resource").text = path
|
219
|
-
ET.SubElement(chain, "property", name="mlt_service").text = "avformat-novalidate"
|
220
|
-
ET.SubElement(chain, "property", name="audio_index").text = "1"
|
221
|
-
ET.SubElement(chain, "property", name="video_index").text = "0"
|
222
|
-
ET.SubElement(chain, "property", name="vstream").text = "0"
|
223
|
-
ET.SubElement(chain, "property", name="astream").text = "0"
|
224
|
-
ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
|
225
|
-
|
226
|
-
groups = []
|
227
|
-
group_counter = 0
|
228
|
-
producers = 1
|
229
|
-
|
230
|
-
for clip in clips:
|
231
|
-
group_children: list[object] = []
|
232
|
-
_in = to_timecode(clip.offset / tb, "standard")
|
233
|
-
_out = to_timecode((clip.offset + clip.dur) / tb, "standard")
|
234
|
-
path = str(clip.src.path)
|
235
|
-
|
236
|
-
for i, playlist in enumerate(clip_playlists[::2]):
|
237
|
-
# adding 1 extra frame for each previous group to the start time works but feels hacky?
|
238
|
-
group_children.append(
|
239
|
-
{
|
240
|
-
"data": f"{i}:{clip.start + group_counter}",
|
241
|
-
"leaf": "clip",
|
242
|
-
"type": "Leaf",
|
243
|
-
}
|
244
|
-
)
|
245
|
-
clip_prod = ""
|
246
|
-
|
247
|
-
if clip.speed == 1:
|
248
|
-
clip_prod = f"chain{i}"
|
249
|
-
else:
|
250
|
-
clip_prod = f"producer{producers}"
|
251
|
-
producers += 1
|
252
|
-
|
253
|
-
entry = ET.SubElement(
|
254
|
-
playlist,
|
255
|
-
"entry",
|
256
|
-
attrib={"producer": f"{clip_prod}", "in": _in, "out": _out},
|
257
|
-
)
|
258
|
-
ET.SubElement(entry, "property", name="kdenlive:id").text = source_ids[path]
|
259
|
-
|
260
|
-
groups.append({"children": group_children[:], "type": "Normal"})
|
261
|
-
group_counter += 1
|
262
|
-
|
263
|
-
# default sequence tractor
|
264
|
-
sequence = ET.SubElement(
|
265
|
-
mlt,
|
266
|
-
"tractor",
|
267
|
-
attrib={"id": f"{{{seq_uuid}}}", "in": "00:00:00.000", "out": "00:00:00.000"},
|
268
|
-
)
|
269
|
-
ET.SubElement(sequence, "property", name="kdenlive:uuid").text = f"{{{seq_uuid}}}"
|
270
|
-
ET.SubElement(sequence, "property", name="kdenlive:clipname").text = "Sequence 1"
|
271
|
-
ET.SubElement(
|
272
|
-
sequence, "property", name="kdenlive:sequenceproperties.groups"
|
273
|
-
).text = json.dumps(groups, indent=4)
|
274
|
-
ET.SubElement(sequence, "track", producer="producer0")
|
275
|
-
|
276
|
-
for i in range(chains):
|
277
|
-
ET.SubElement(sequence, "track", producer=f"tractor{i}")
|
278
|
-
|
279
|
-
# main bin
|
280
|
-
playlist_bin = ET.SubElement(mlt, "playlist", id="main_bin")
|
281
|
-
ET.SubElement(
|
282
|
-
playlist_bin, "property", name="kdenlive:docproperties.uuid"
|
283
|
-
).text = f"{{{seq_uuid}}}"
|
284
|
-
ET.SubElement(
|
285
|
-
playlist_bin, "property", name="kdenlive:docproperties.version"
|
286
|
-
).text = "1.1"
|
287
|
-
ET.SubElement(playlist_bin, "property", name="xml_retain").text = "1"
|
288
|
-
ET.SubElement(
|
289
|
-
playlist_bin,
|
290
|
-
"entry",
|
291
|
-
attrib={
|
292
|
-
"producer": f"{{{seq_uuid}}}",
|
293
|
-
"in": "00:00:00.000",
|
294
|
-
"out": "00:00:00.000",
|
295
|
-
},
|
296
|
-
)
|
297
|
-
ET.SubElement(
|
298
|
-
playlist_bin,
|
299
|
-
"entry",
|
300
|
-
attrib={"producer": f"chain{chains}", "in": "00:00:00.000"},
|
301
|
-
)
|
302
|
-
|
303
|
-
# reserved last tractor for project
|
304
|
-
tractor = ET.SubElement(
|
305
|
-
mlt,
|
306
|
-
"tractor",
|
307
|
-
attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
|
308
|
-
)
|
309
|
-
ET.SubElement(tractor, "property", name="kdenlive:projectTractor").text = "1"
|
310
|
-
ET.SubElement(
|
311
|
-
tractor,
|
312
|
-
"track",
|
313
|
-
attrib={"producer": f"{{{seq_uuid}}}", "in": "00:00:00.000", "out": global_out},
|
314
|
-
)
|
315
|
-
tree = ET.ElementTree(mlt)
|
316
|
-
|
317
|
-
ET.indent(tree, space="\t", level=0)
|
318
|
-
|
319
|
-
if output == "-":
|
320
|
-
print(ET.tostring(mlt, encoding="unicode"))
|
321
|
-
else:
|
322
|
-
tree.write(output, xml_declaration=True, encoding="utf-8")
|
auto_editor/exports/shotcut.py
DELETED
@@ -1,147 +0,0 @@
|
|
1
|
-
import xml.etree.ElementTree as ET
|
2
|
-
|
3
|
-
from auto_editor.timeline import Clip, v3
|
4
|
-
from auto_editor.utils.func import aspect_ratio, to_timecode
|
5
|
-
|
6
|
-
"""
|
7
|
-
Shotcut uses the MLT timeline format
|
8
|
-
|
9
|
-
See docs here:
|
10
|
-
https://mltframework.org/docs/mltxml/
|
11
|
-
|
12
|
-
"""
|
13
|
-
|
14
|
-
|
15
|
-
def shotcut_write_mlt(output: str, tl: v3) -> None:
|
16
|
-
mlt = ET.Element(
|
17
|
-
"mlt",
|
18
|
-
attrib={
|
19
|
-
"LC_NUMERIC": "C",
|
20
|
-
"version": "7.9.0",
|
21
|
-
"title": "Shotcut version 22.09.23",
|
22
|
-
"producer": "main_bin",
|
23
|
-
},
|
24
|
-
)
|
25
|
-
|
26
|
-
width, height = tl.res
|
27
|
-
num, den = aspect_ratio(width, height)
|
28
|
-
tb = tl.tb
|
29
|
-
|
30
|
-
ET.SubElement(
|
31
|
-
mlt,
|
32
|
-
"profile",
|
33
|
-
attrib={
|
34
|
-
"description": "automatic",
|
35
|
-
"width": f"{width}",
|
36
|
-
"height": f"{height}",
|
37
|
-
"progressive": "1",
|
38
|
-
"sample_aspect_num": "1",
|
39
|
-
"sample_aspect_den": "1",
|
40
|
-
"display_aspect_num": f"{num}",
|
41
|
-
"display_aspect_den": f"{den}",
|
42
|
-
"frame_rate_num": f"{tb.numerator}",
|
43
|
-
"frame_rate_den": f"{tb.denominator}",
|
44
|
-
"colorspace": "709",
|
45
|
-
},
|
46
|
-
)
|
47
|
-
|
48
|
-
playlist_bin = ET.SubElement(mlt, "playlist", id="main_bin")
|
49
|
-
ET.SubElement(playlist_bin, "property", name="xml_retain").text = "1"
|
50
|
-
|
51
|
-
global_out = to_timecode(len(tl) / tb, "standard")
|
52
|
-
|
53
|
-
producer = ET.SubElement(mlt, "producer", id="bg")
|
54
|
-
|
55
|
-
ET.SubElement(producer, "property", name="length").text = global_out
|
56
|
-
ET.SubElement(producer, "property", name="eof").text = "pause"
|
57
|
-
ET.SubElement(producer, "property", name="resource").text = "#000" # background
|
58
|
-
ET.SubElement(producer, "property", name="mlt_service").text = "color"
|
59
|
-
ET.SubElement(producer, "property", name="mlt_image_format").text = "rgba"
|
60
|
-
ET.SubElement(producer, "property", name="aspect_ratio").text = "1"
|
61
|
-
|
62
|
-
playlist = ET.SubElement(mlt, "playlist", id="background")
|
63
|
-
ET.SubElement(
|
64
|
-
playlist,
|
65
|
-
"entry",
|
66
|
-
attrib={"producer": "bg", "in": "00:00:00.000", "out": global_out},
|
67
|
-
).text = "1"
|
68
|
-
|
69
|
-
chains = 0
|
70
|
-
producers = 0
|
71
|
-
|
72
|
-
if tl.v:
|
73
|
-
clips = [clip for clip in tl.v[0] if isinstance(clip, Clip)]
|
74
|
-
elif tl.a:
|
75
|
-
clips = tl.a[0]
|
76
|
-
else:
|
77
|
-
clips = []
|
78
|
-
|
79
|
-
for clip in clips:
|
80
|
-
src = clip.src
|
81
|
-
length = to_timecode((clip.offset + clip.dur) / tb, "standard")
|
82
|
-
|
83
|
-
if clip.speed == 1:
|
84
|
-
resource = f"{src.path}"
|
85
|
-
caption = f"{src.path.stem}"
|
86
|
-
chain = ET.SubElement(
|
87
|
-
mlt, "chain", attrib={"id": f"chain{chains}", "out": length}
|
88
|
-
)
|
89
|
-
else:
|
90
|
-
chain = ET.SubElement(
|
91
|
-
mlt, "producer", attrib={"id": f"producer{producers}", "out": length}
|
92
|
-
)
|
93
|
-
resource = f"{clip.speed}:{src.path}"
|
94
|
-
caption = f"{src.path.stem} ({clip.speed}x)"
|
95
|
-
|
96
|
-
producers += 1
|
97
|
-
|
98
|
-
ET.SubElement(chain, "property", name="length").text = length
|
99
|
-
ET.SubElement(chain, "property", name="resource").text = resource
|
100
|
-
|
101
|
-
if clip.speed != 1:
|
102
|
-
ET.SubElement(chain, "property", name="warp_speed").text = f"{clip.speed}"
|
103
|
-
ET.SubElement(chain, "property", name="warp_pitch").text = "1"
|
104
|
-
ET.SubElement(chain, "property", name="mlt_service").text = "timewarp"
|
105
|
-
|
106
|
-
ET.SubElement(chain, "property", name="caption").text = caption
|
107
|
-
|
108
|
-
chains += 1
|
109
|
-
|
110
|
-
main_playlist = ET.SubElement(mlt, "playlist", id="playlist0")
|
111
|
-
ET.SubElement(main_playlist, "property", name="shotcut:video").text = "1"
|
112
|
-
ET.SubElement(main_playlist, "property", name="shotcut:name").text = "V1"
|
113
|
-
|
114
|
-
producers = 0
|
115
|
-
for i, clip in enumerate(clips):
|
116
|
-
_in = to_timecode(clip.offset / tb, "standard")
|
117
|
-
_out = to_timecode((clip.offset + clip.dur) / tb, "standard")
|
118
|
-
|
119
|
-
tag_name = f"chain{i}"
|
120
|
-
if clip.speed != 1:
|
121
|
-
tag_name = f"producer{producers}"
|
122
|
-
producers += 1
|
123
|
-
|
124
|
-
ET.SubElement(
|
125
|
-
main_playlist,
|
126
|
-
"entry",
|
127
|
-
attrib={"producer": tag_name, "in": _in, "out": _out},
|
128
|
-
)
|
129
|
-
|
130
|
-
tractor = ET.SubElement(
|
131
|
-
mlt,
|
132
|
-
"tractor",
|
133
|
-
attrib={"id": "tractor0", "in": "00:00:00.000", "out": global_out},
|
134
|
-
)
|
135
|
-
ET.SubElement(tractor, "property", name="shotcut").text = "1"
|
136
|
-
ET.SubElement(tractor, "property", name="shotcut:projectAudioChannels").text = "2"
|
137
|
-
ET.SubElement(tractor, "track", producer="background")
|
138
|
-
ET.SubElement(tractor, "track", producer="playlist0")
|
139
|
-
|
140
|
-
tree = ET.ElementTree(mlt)
|
141
|
-
|
142
|
-
ET.indent(tree, space="\t", level=0)
|
143
|
-
|
144
|
-
if output == "-":
|
145
|
-
print(ET.tostring(mlt, encoding="unicode"))
|
146
|
-
else:
|
147
|
-
tree.write(output, xml_declaration=True, encoding="utf-8")
|
auto_editor/ffwrapper.py
DELETED
@@ -1,187 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from fractions import Fraction
|
5
|
-
from pathlib import Path
|
6
|
-
|
7
|
-
import av
|
8
|
-
|
9
|
-
from auto_editor.utils.log import Log
|
10
|
-
|
11
|
-
|
12
|
-
def mux(input: Path, output: Path, stream: int) -> None:
|
13
|
-
input_container = av.open(input, "r")
|
14
|
-
output_container = av.open(output, "w")
|
15
|
-
|
16
|
-
input_audio_stream = input_container.streams.audio[stream]
|
17
|
-
output_audio_stream = output_container.add_stream("pcm_s16le")
|
18
|
-
|
19
|
-
for frame in input_container.decode(input_audio_stream):
|
20
|
-
output_container.mux(output_audio_stream.encode(frame))
|
21
|
-
|
22
|
-
output_container.mux(output_audio_stream.encode(None))
|
23
|
-
|
24
|
-
output_container.close()
|
25
|
-
input_container.close()
|
26
|
-
|
27
|
-
|
28
|
-
@dataclass(slots=True, frozen=True)
|
29
|
-
class VideoStream:
|
30
|
-
width: int
|
31
|
-
height: int
|
32
|
-
codec: str
|
33
|
-
fps: Fraction
|
34
|
-
duration: float
|
35
|
-
sar: Fraction
|
36
|
-
time_base: Fraction | None
|
37
|
-
pix_fmt: str | None
|
38
|
-
color_range: int
|
39
|
-
color_space: int
|
40
|
-
color_primaries: int
|
41
|
-
color_transfer: int
|
42
|
-
bitrate: int
|
43
|
-
lang: str | None
|
44
|
-
|
45
|
-
|
46
|
-
@dataclass(slots=True, frozen=True)
|
47
|
-
class AudioStream:
|
48
|
-
codec: str
|
49
|
-
samplerate: int
|
50
|
-
layout: str
|
51
|
-
channels: int
|
52
|
-
duration: float
|
53
|
-
bitrate: int
|
54
|
-
lang: str | None
|
55
|
-
|
56
|
-
|
57
|
-
@dataclass(slots=True, frozen=True)
|
58
|
-
class SubtitleStream:
|
59
|
-
codec: str
|
60
|
-
ext: str
|
61
|
-
lang: str | None
|
62
|
-
|
63
|
-
|
64
|
-
@dataclass(slots=True, frozen=True)
|
65
|
-
class FileInfo:
|
66
|
-
path: Path
|
67
|
-
bitrate: int
|
68
|
-
duration: float
|
69
|
-
timecode: str # in SMPTE
|
70
|
-
videos: tuple[VideoStream, ...]
|
71
|
-
audios: tuple[AudioStream, ...]
|
72
|
-
subtitles: tuple[SubtitleStream, ...]
|
73
|
-
|
74
|
-
def get_res(self) -> tuple[int, int]:
|
75
|
-
if self.videos:
|
76
|
-
return self.videos[0].width, self.videos[0].height
|
77
|
-
return 1920, 1080
|
78
|
-
|
79
|
-
def get_fps(self) -> Fraction:
|
80
|
-
if self.videos:
|
81
|
-
return self.videos[0].fps
|
82
|
-
return Fraction(30)
|
83
|
-
|
84
|
-
def get_sr(self) -> int:
|
85
|
-
if self.audios:
|
86
|
-
return self.audios[0].samplerate
|
87
|
-
return 48000
|
88
|
-
|
89
|
-
@classmethod
|
90
|
-
def init(self, path: str, log: Log) -> FileInfo:
|
91
|
-
try:
|
92
|
-
cont = av.open(path, "r")
|
93
|
-
except av.error.FileNotFoundError:
|
94
|
-
log.error(f"Input file doesn't exist: {path}")
|
95
|
-
except av.error.IsADirectoryError:
|
96
|
-
log.error(f"Expected a media file, but got a directory: {path}")
|
97
|
-
except av.error.InvalidDataError:
|
98
|
-
log.error(f"Invalid data when processing: {path}")
|
99
|
-
|
100
|
-
videos: tuple[VideoStream, ...] = ()
|
101
|
-
audios: tuple[AudioStream, ...] = ()
|
102
|
-
subtitles: tuple[SubtitleStream, ...] = ()
|
103
|
-
|
104
|
-
for v in cont.streams.video:
|
105
|
-
if v.duration is not None and v.time_base is not None:
|
106
|
-
vdur = float(v.duration * v.time_base)
|
107
|
-
else:
|
108
|
-
vdur = 0.0
|
109
|
-
|
110
|
-
fps = v.average_rate
|
111
|
-
if (fps is None or fps < 1) and v.name in {"png", "mjpeg", "webp"}:
|
112
|
-
fps = Fraction(25)
|
113
|
-
if fps is None or fps == 0:
|
114
|
-
fps = Fraction(30)
|
115
|
-
|
116
|
-
if v.sample_aspect_ratio is None:
|
117
|
-
sar = Fraction(1)
|
118
|
-
else:
|
119
|
-
sar = v.sample_aspect_ratio
|
120
|
-
|
121
|
-
cc = v.codec_context
|
122
|
-
|
123
|
-
if v.name is None:
|
124
|
-
log.error(f"Can't detect codec for video stream {v}")
|
125
|
-
|
126
|
-
videos += (
|
127
|
-
VideoStream(
|
128
|
-
v.width,
|
129
|
-
v.height,
|
130
|
-
v.codec.canonical_name,
|
131
|
-
fps,
|
132
|
-
vdur,
|
133
|
-
sar,
|
134
|
-
v.time_base,
|
135
|
-
getattr(v.format, "name", None),
|
136
|
-
cc.color_range,
|
137
|
-
cc.colorspace,
|
138
|
-
cc.color_primaries,
|
139
|
-
cc.color_trc,
|
140
|
-
0 if v.bit_rate is None else v.bit_rate,
|
141
|
-
v.language,
|
142
|
-
),
|
143
|
-
)
|
144
|
-
|
145
|
-
for a in cont.streams.audio:
|
146
|
-
adur = 0.0
|
147
|
-
if a.duration is not None and a.time_base is not None:
|
148
|
-
adur = float(a.duration * a.time_base)
|
149
|
-
|
150
|
-
a_cc = a.codec_context
|
151
|
-
audios += (
|
152
|
-
AudioStream(
|
153
|
-
a_cc.codec.canonical_name,
|
154
|
-
0 if a_cc.sample_rate is None else a_cc.sample_rate,
|
155
|
-
a.layout.name,
|
156
|
-
a_cc.channels,
|
157
|
-
adur,
|
158
|
-
0 if a_cc.bit_rate is None else a_cc.bit_rate,
|
159
|
-
a.language,
|
160
|
-
),
|
161
|
-
)
|
162
|
-
|
163
|
-
for s in cont.streams.subtitles:
|
164
|
-
codec = s.codec_context.name
|
165
|
-
sub_exts = {"mov_text": "srt", "ass": "ass", "webvtt": "vtt"}
|
166
|
-
ext = sub_exts.get(codec, "vtt")
|
167
|
-
subtitles += (SubtitleStream(codec, ext, s.language),)
|
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()
|
179
|
-
bitrate = 0 if cont.bit_rate is None else cont.bit_rate
|
180
|
-
dur = 0 if cont.duration is None else cont.duration / av.time_base
|
181
|
-
|
182
|
-
cont.close()
|
183
|
-
|
184
|
-
return FileInfo(Path(path), bitrate, dur, timecode, videos, audios, subtitles)
|
185
|
-
|
186
|
-
def __repr__(self) -> str:
|
187
|
-
return f"@{self.path.name}"
|