auto-editor 28.1.0__py3-none-any.whl → 29.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.
Files changed (60) hide show
  1. auto_editor/__init__.py +3 -1
  2. auto_editor/__main__.py +31 -497
  3. auto_editor/cli.py +12 -0
  4. {auto_editor-28.1.0.dist-info → auto_editor-29.0.1.dist-info}/METADATA +5 -6
  5. auto_editor-29.0.1.dist-info/RECORD +9 -0
  6. auto_editor-29.0.1.dist-info/entry_points.txt +2 -0
  7. {auto_editor-28.1.0.dist-info → auto_editor-29.0.1.dist-info}/top_level.txt +0 -1
  8. auto_editor/analyze.py +0 -393
  9. auto_editor/cmds/__init__.py +0 -0
  10. auto_editor/cmds/cache.py +0 -69
  11. auto_editor/cmds/desc.py +0 -32
  12. auto_editor/cmds/info.py +0 -213
  13. auto_editor/cmds/levels.py +0 -199
  14. auto_editor/cmds/palet.py +0 -29
  15. auto_editor/cmds/repl.py +0 -113
  16. auto_editor/cmds/subdump.py +0 -72
  17. auto_editor/cmds/test.py +0 -816
  18. auto_editor/edit.py +0 -560
  19. auto_editor/exports/__init__.py +0 -0
  20. auto_editor/exports/fcp11.py +0 -195
  21. auto_editor/exports/fcp7.py +0 -313
  22. auto_editor/exports/json.py +0 -63
  23. auto_editor/exports/kdenlive.py +0 -322
  24. auto_editor/exports/shotcut.py +0 -147
  25. auto_editor/ffwrapper.py +0 -187
  26. auto_editor/help.py +0 -224
  27. auto_editor/imports/__init__.py +0 -0
  28. auto_editor/imports/fcp7.py +0 -275
  29. auto_editor/imports/json.py +0 -234
  30. auto_editor/json.py +0 -297
  31. auto_editor/lang/__init__.py +0 -0
  32. auto_editor/lang/libintrospection.py +0 -10
  33. auto_editor/lang/libmath.py +0 -23
  34. auto_editor/lang/palet.py +0 -724
  35. auto_editor/lang/stdenv.py +0 -1179
  36. auto_editor/lib/__init__.py +0 -0
  37. auto_editor/lib/contracts.py +0 -235
  38. auto_editor/lib/data_structs.py +0 -278
  39. auto_editor/lib/err.py +0 -2
  40. auto_editor/make_layers.py +0 -315
  41. auto_editor/preview.py +0 -93
  42. auto_editor/render/__init__.py +0 -0
  43. auto_editor/render/audio.py +0 -517
  44. auto_editor/render/subtitle.py +0 -205
  45. auto_editor/render/video.py +0 -307
  46. auto_editor/timeline.py +0 -331
  47. auto_editor/utils/__init__.py +0 -0
  48. auto_editor/utils/bar.py +0 -142
  49. auto_editor/utils/chunks.py +0 -2
  50. auto_editor/utils/cmdkw.py +0 -206
  51. auto_editor/utils/container.py +0 -101
  52. auto_editor/utils/func.py +0 -128
  53. auto_editor/utils/log.py +0 -126
  54. auto_editor/utils/types.py +0 -277
  55. auto_editor/vanparse.py +0 -313
  56. auto_editor-28.1.0.dist-info/RECORD +0 -57
  57. auto_editor-28.1.0.dist-info/entry_points.txt +0 -6
  58. docs/build.py +0 -70
  59. {auto_editor-28.1.0.dist-info → auto_editor-29.0.1.dist-info}/WHEEL +0 -0
  60. {auto_editor-28.1.0.dist-info → auto_editor-29.0.1.dist-info}/licenses/LICENSE +0 -0
auto_editor/edit.py DELETED
@@ -1,560 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import sys
5
- from fractions import Fraction
6
- from heapq import heappop, heappush
7
- from os.path import splitext
8
- from pathlib import Path
9
- from subprocess import run
10
- from typing import TYPE_CHECKING, Any
11
-
12
- import av
13
- from av import Codec
14
-
15
- from auto_editor.ffwrapper import FileInfo
16
- from auto_editor.lib.contracts import is_int, is_str
17
- from auto_editor.make_layers import clipify, make_av, make_timeline
18
- from auto_editor.render.audio import make_new_audio
19
- from auto_editor.render.subtitle import make_new_subtitles
20
- from auto_editor.render.video import render_av
21
- from auto_editor.timeline import set_stream_to_0, v1, v3
22
- from auto_editor.utils.bar import initBar
23
- from auto_editor.utils.chunks import Chunk, Chunks
24
- from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
25
- from auto_editor.utils.container import Container, container_constructor
26
- from auto_editor.utils.log import Log
27
-
28
- if TYPE_CHECKING:
29
- from auto_editor.__main__ import Args
30
-
31
-
32
- def set_output(
33
- out: str | None, export: str | None, path: Path | None, log: Log
34
- ) -> tuple[str, str]:
35
- if out is None or out == "-":
36
- if path is None:
37
- log.error("`--output` must be set.") # When a timeline file is the input.
38
- root, ext = splitext(path)
39
- else:
40
- root, ext = splitext(out)
41
-
42
- if ext == "":
43
- # Use `mp4` as the default, because it is most compatible.
44
- ext = ".mp4" if path is None else path.suffix
45
-
46
- if export is None:
47
- match ext:
48
- case ".xml":
49
- export = "premiere"
50
- case ".fcpxml":
51
- export = "final-cut-pro"
52
- case ".mlt":
53
- export = "shotcut"
54
- case ".kdenlive":
55
- export = "kdenlive"
56
- case ".json" | ".v1":
57
- export = "v1"
58
- case ".v3":
59
- export = "v3"
60
- case _:
61
- export = "default"
62
-
63
- match export:
64
- case "premiere" | "resolve-fcp7":
65
- ext = ".xml"
66
- case "final-cut-pro" | "resolve":
67
- ext = ".fcpxml"
68
- case "shotcut":
69
- ext = ".mlt"
70
- case "kdenlive":
71
- ext = ".kdenlive"
72
- case "v1":
73
- if ext != ".json":
74
- ext = ".v1"
75
- case "v3":
76
- ext = ".v3"
77
-
78
- if out == "-":
79
- return "-", export
80
-
81
- if out is None:
82
- return f"{root}_ALTERED{ext}", export
83
-
84
- return f"{root}{ext}", export
85
-
86
-
87
- def set_video_codec(
88
- codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
89
- ) -> str:
90
- if codec == "auto":
91
- codec = "h264" if (src is None or not src.videos) else src.videos[0].codec
92
- if codec not in ctr.vcodecs and ctr.default_vid != "none":
93
- return ctr.default_vid
94
- return codec
95
-
96
- if ctr.vcodecs is not None and codec not in ctr.vcodecs:
97
- try:
98
- cobj = Codec(codec, "w")
99
- except av.codec.codec.UnknownCodecError:
100
- log.error(f"Unknown encoder: {codec}")
101
- # Normalize encoder names
102
- if cobj.id not in (Codec(x, "w").id for x in ctr.vcodecs):
103
- log.error(
104
- f"'{codec}' video encoder is not supported in the '{out_ext}' container"
105
- )
106
-
107
- return codec
108
-
109
-
110
- def set_audio_codec(
111
- codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
112
- ) -> str:
113
- if codec == "auto":
114
- if src is None or not src.audios:
115
- codec = "aac"
116
- else:
117
- codec = src.audios[0].codec
118
- if Codec(codec, "w").audio_formats is None:
119
- codec = "aac"
120
- if codec not in ctr.acodecs and ctr.default_aud != "none":
121
- codec = ctr.default_aud
122
- if codec is None:
123
- codec = "aac"
124
- return codec
125
-
126
- if ctr.acodecs is None or codec not in ctr.acodecs:
127
- try:
128
- cobj = Codec(codec, "w")
129
- except av.codec.codec.UnknownCodecError:
130
- log.error(f"Unknown encoder: {codec}")
131
- # Normalize encoder names
132
- if cobj.id not in (Codec(x, "w").id for x in ctr.acodecs):
133
- log.error(
134
- f"'{codec}' audio encoder is not supported in the '{out_ext}' container"
135
- )
136
- return codec
137
-
138
-
139
- def parse_export(export: str, log: Log) -> dict[str, Any]:
140
- exploded = export.split(":", maxsplit=1)
141
- if len(exploded) == 1:
142
- name, text = exploded[0], ""
143
- else:
144
- name, text = exploded
145
-
146
- name_attr = pAttr("name", "Auto-Editor Media Group", is_str)
147
- parsing = {
148
- "default": pAttrs("default"),
149
- "premiere": pAttrs("premiere", name_attr),
150
- "final-cut-pro": pAttrs(
151
- "final-cut-pro", name_attr, pAttr("version", 11, is_int)
152
- ),
153
- "resolve": pAttrs("resolve", name_attr),
154
- "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
155
- "shotcut": pAttrs("shotcut"),
156
- "kdenlive": pAttrs("kdenlive"),
157
- "v1": pAttrs("v1"),
158
- "v3": pAttrs("v3"),
159
- "clip-sequence": pAttrs("clip-sequence"),
160
- }
161
-
162
- if name in parsing:
163
- try:
164
- return {"export": name} | parse_with_palet(text, parsing[name], {})
165
- except ParserError as e:
166
- log.error(e)
167
-
168
- valid_choices = " ".join(f'"{s}"' for s in parsing.keys())
169
- log.error(f'Invalid export format: "{name}"\nValid choices: {valid_choices}')
170
-
171
-
172
- def edit_media(paths: list[str], args: Args, log: Log) -> None:
173
- bar = initBar(args.progress)
174
- tl = src = use_path = None
175
-
176
- if paths:
177
- path_ext = splitext(paths[0])[1].lower()
178
- if path_ext == ".xml":
179
- from auto_editor.imports.fcp7 import fcp7_read_xml
180
-
181
- tl = fcp7_read_xml(paths[0], log)
182
- elif path_ext == ".mlt":
183
- log.error("Reading mlt files not implemented")
184
- elif path_ext in {".v1", ".v3", ".json"}:
185
- from auto_editor.imports.json import read_json
186
-
187
- tl = read_json(paths[0], log)
188
- else:
189
- sources = [FileInfo.init(path, log) for path in paths]
190
- src = sources[0]
191
- use_path = src.path
192
-
193
- if args.export is None:
194
- output, export = set_output(args.output, args.export, use_path, log)
195
- export_ops: dict[str, Any] = {"export": export}
196
- else:
197
- export_ops = parse_export(args.export, log)
198
- export = export_ops["export"]
199
- output, _ = set_output(args.output, export, use_path, log)
200
-
201
- if output == "-":
202
- # When printing to stdout, silence all logs.
203
- log.quiet = True
204
-
205
- if not args.preview:
206
- log.conwrite("Starting")
207
-
208
- if os.path.isdir(output):
209
- log.error("Output path already has an existing directory!")
210
-
211
- if args.sample_rate is None:
212
- if tl is None:
213
- samplerate = 48000 if src is None else src.get_sr()
214
- else:
215
- samplerate = tl.sr
216
- else:
217
- samplerate = args.sample_rate
218
-
219
- if tl is None:
220
- tl = make_timeline(sources, args, samplerate, bar, log)
221
- else:
222
- if args.resolution is not None:
223
- tl.T.res = args.resolution
224
- if args.background is not None:
225
- tl.background = args.background
226
- if args.frame_rate is not None:
227
- log.warning(
228
- "Setting timebase/framerate is not supported when importing timelines"
229
- )
230
-
231
- if args.preview:
232
- from auto_editor.preview import preview
233
-
234
- preview(tl, log)
235
- return
236
-
237
- if export in {"v1", "v3"}:
238
- from auto_editor.exports.json import make_json_timeline
239
-
240
- make_json_timeline(export, output, tl, log)
241
- return
242
-
243
- if export in {"premiere", "resolve-fcp7"}:
244
- from auto_editor.exports.fcp7 import fcp7_write_xml
245
-
246
- is_resolve = export.startswith("resolve")
247
- fcp7_write_xml(export_ops["name"], output, is_resolve, tl)
248
- return
249
-
250
- if export == "final-cut-pro":
251
- from auto_editor.exports.fcp11 import fcp11_write_xml
252
-
253
- ver = export_ops["version"]
254
- fcp11_write_xml(export_ops["name"], ver, output, False, tl, log)
255
- return
256
-
257
- if export == "resolve":
258
- from auto_editor.exports.fcp11 import fcp11_write_xml
259
-
260
- set_stream_to_0(tl, log)
261
- fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
262
- return
263
-
264
- if export == "shotcut":
265
- from auto_editor.exports.shotcut import shotcut_write_mlt
266
-
267
- shotcut_write_mlt(output, tl)
268
- return
269
-
270
- if export == "kdenlive":
271
- from auto_editor.exports.kdenlive import kdenlive_write
272
-
273
- kdenlive_write(output, tl)
274
- return
275
-
276
- if output == "-":
277
- log.error("Exporting media files to stdout is not supported.")
278
- out_ext = splitext(output)[1].replace(".", "")
279
-
280
- # Check if export options make sense.
281
- ctr = container_constructor(out_ext.lower(), log)
282
-
283
- if ctr.samplerate is not None and args.sample_rate not in ctr.samplerate:
284
- log.error(f"'{out_ext}' container only supports samplerates: {ctr.samplerate}")
285
-
286
- args.video_codec = set_video_codec(args.video_codec, src, out_ext, ctr, log)
287
- args.audio_codec = set_audio_codec(args.audio_codec, src, out_ext, ctr, log)
288
-
289
- def make_media(tl: v3, output_path: str) -> None:
290
- options = {}
291
- mov_flags = []
292
- if args.fragmented and not args.no_fragmented:
293
- mov_flags.extend(["default_base_moof", "frag_keyframe", "separate_moof"])
294
- options["frag_duration"] = "0.2"
295
- if args.faststart:
296
- log.warning("Fragmented is enabled, will not apply faststart.")
297
- elif not args.no_faststart:
298
- mov_flags.append("faststart")
299
- if mov_flags:
300
- options["movflags"] = "+".join(mov_flags)
301
-
302
- output = av.open(output_path, "w", container_options=options)
303
-
304
- # Setup video
305
- if ctr.default_vid not in ("none", "png") and tl.v:
306
- vframes = render_av(output, tl, args, log)
307
- output_stream: av.VideoStream | None
308
- output_stream = next(vframes) # type: ignore
309
- else:
310
- output_stream, vframes = None, iter([])
311
-
312
- # Setup audio
313
- try:
314
- audio_encoder = Codec(args.audio_codec, "w")
315
- except av.FFmpegError as e:
316
- log.error(e)
317
- if audio_encoder.audio_formats is None:
318
- log.error(f"{args.audio_codec}: No known audio formats avail.")
319
- fmt = audio_encoder.audio_formats[0]
320
-
321
- audio_streams: list[av.AudioStream] = []
322
-
323
- if ctr.default_aud == "none":
324
- while len(tl.a) > 0:
325
- tl.a.pop()
326
- elif len(tl.a) > 1 and ctr.max_audios == 1:
327
- log.warning("Dropping extra audio streams (container only allows one)")
328
-
329
- while len(tl.a) > 1:
330
- tl.a.pop()
331
-
332
- if len(tl.a) > 0:
333
- audio_streams, audio_gen_frames = make_new_audio(output, fmt, tl, args, log)
334
- else:
335
- audio_streams, audio_gen_frames = [], [iter([])]
336
-
337
- # Setup subtitles
338
- if ctr.default_sub != "none" and not args.sn:
339
- sub_paths = make_new_subtitles(tl, log)
340
- else:
341
- sub_paths = []
342
-
343
- subtitle_streams = []
344
- subtitle_inputs = []
345
- sub_gen_frames = []
346
-
347
- for i, sub_path in enumerate(sub_paths):
348
- subtitle_input = av.open(sub_path)
349
- subtitle_inputs.append(subtitle_input)
350
- subtitle_stream = output.add_stream_from_template(
351
- subtitle_input.streams.subtitles[0]
352
- )
353
- if i < len(tl.T.subtitles) and (lang := tl.T.subtitles[i].lang) is not None:
354
- subtitle_stream.metadata["language"] = lang
355
-
356
- subtitle_streams.append(subtitle_stream)
357
- sub_gen_frames.append(subtitle_input.demux(subtitles=0))
358
-
359
- no_color = log.no_color or log.machine
360
- encoder_titles = []
361
- if output_stream is not None:
362
- name = output_stream.codec.canonical_name
363
- encoder_titles.append(name if no_color else f"\033[95m{name}")
364
- if audio_streams:
365
- name = audio_streams[0].codec.canonical_name
366
- encoder_titles.append(name if no_color else f"\033[96m{name}")
367
- if subtitle_streams:
368
- name = subtitle_streams[0].codec.canonical_name
369
- encoder_titles.append(name if no_color else f"\033[32m{name}")
370
-
371
- title = f"({os.path.splitext(output_path)[1][1:]}) "
372
- if no_color:
373
- title += "+".join(encoder_titles)
374
- else:
375
- title += "\033[0m+".join(encoder_titles) + "\033[0m"
376
- bar.start(tl.end, title)
377
-
378
- MAX_AUDIO_AHEAD = 30 # In timebase, how far audio can be ahead of video.
379
- MAX_SUB_AHEAD = 30
380
-
381
- class Priority:
382
- __slots__ = ("index", "frame_type", "frame", "stream")
383
-
384
- def __init__(self, value: int | Fraction, frame, stream):
385
- self.frame_type: str = stream.type
386
- assert self.frame_type in ("audio", "subtitle", "video")
387
- if self.frame_type in {"audio", "subtitle"}:
388
- self.index: int | float = round(value * frame.time_base * tl.tb)
389
- else:
390
- self.index = float("inf") if value is None else int(value)
391
- self.frame = frame
392
- self.stream = stream
393
-
394
- def __lt__(self, other):
395
- return self.index < other.index
396
-
397
- def __eq__(self, other):
398
- return self.index == other.index
399
-
400
- # Priority queue for ordered frames by time_base.
401
- frame_queue: list[Priority] = []
402
- latest_audio_index = float("-inf")
403
- latest_sub_index = float("-inf")
404
- earliest_video_index = None
405
-
406
- while True:
407
- if earliest_video_index is None:
408
- should_get_audio = True
409
- should_get_sub = True
410
- else:
411
- for item in frame_queue:
412
- if item.frame_type == "audio":
413
- latest_audio_index = max(latest_audio_index, item.index)
414
- elif item.frame_type == "subtitle":
415
- latest_sub_index = max(latest_sub_index, item.index)
416
-
417
- should_get_audio = (
418
- latest_audio_index <= earliest_video_index + MAX_AUDIO_AHEAD
419
- )
420
- should_get_sub = (
421
- latest_sub_index <= earliest_video_index + MAX_SUB_AHEAD
422
- )
423
-
424
- index, video_frame = next(vframes, (0, None))
425
-
426
- if video_frame:
427
- earliest_video_index = index
428
- heappush(frame_queue, Priority(index, video_frame, output_stream))
429
-
430
- if should_get_audio:
431
- audio_frames = [next(frames, None) for frames in audio_gen_frames]
432
- if output_stream is None and audio_frames and audio_frames[-1]:
433
- assert audio_frames[-1].time is not None
434
- index = round(audio_frames[-1].time * tl.tb)
435
- else:
436
- audio_frames = [None]
437
- if should_get_sub:
438
- subtitle_frames = [next(packet, None) for packet in sub_gen_frames]
439
- else:
440
- subtitle_frames = [None]
441
-
442
- # Break if no more frames
443
- if (
444
- all(frame is None for frame in audio_frames)
445
- and video_frame is None
446
- and all(packet is None for packet in subtitle_frames)
447
- ):
448
- break
449
-
450
- if should_get_audio:
451
- for audio_stream, aframe in zip(audio_streams, audio_frames):
452
- if aframe is None:
453
- continue
454
- assert aframe.pts is not None
455
- heappush(frame_queue, Priority(aframe.pts, aframe, audio_stream))
456
- if should_get_sub:
457
- for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
458
- if packet and packet.pts is not None:
459
- packet.stream = subtitle_stream
460
- heappush(
461
- frame_queue, Priority(packet.pts, packet, subtitle_stream)
462
- )
463
-
464
- while frame_queue and frame_queue[0].index <= index:
465
- item = heappop(frame_queue)
466
- frame_type = item.frame_type
467
- bar_index = None
468
- try:
469
- if frame_type in {"video", "audio"}:
470
- if item.frame.time is not None:
471
- bar_index = round(item.frame.time * tl.tb)
472
- output.mux(item.stream.encode(item.frame))
473
- elif frame_type == "subtitle":
474
- output.mux(item.frame)
475
- except av.error.ExternalError:
476
- log.error(
477
- f"Generic error for encoder: {item.stream.name}\n"
478
- f"at {item.index} time_base\nPerhaps video quality settings are too low?"
479
- )
480
- except av.FileNotFoundError:
481
- log.error(f"File not found: {output_path}")
482
- except av.FFmpegError as e:
483
- log.error(e)
484
-
485
- if bar_index:
486
- bar.tick(bar_index)
487
-
488
- # Flush streams
489
- if output_stream is not None:
490
- output.mux(output_stream.encode(None))
491
- for audio_stream in audio_streams:
492
- output.mux(audio_stream.encode(None))
493
-
494
- bar.end()
495
-
496
- # Close resources
497
- for subtitle_input in subtitle_inputs:
498
- subtitle_input.close()
499
- output.close()
500
-
501
- if export == "clip-sequence":
502
- if tl.v1 is None:
503
- log.error("Timeline too complex to use clip-sequence export")
504
-
505
- def pad_chunk(chunk: Chunk, total: int) -> Chunks:
506
- start = [] if chunk[0] == 0 else [(0, chunk[0], 99999.0)]
507
- end = [] if chunk[1] == total else [(chunk[1], total, 99999.0)]
508
- return start + [chunk] + end
509
-
510
- def append_filename(path: str, val: str) -> str:
511
- root, ext = splitext(path)
512
- return root + val + ext
513
-
514
- total_frames = tl.v1.chunks[-1][1] - 1
515
- clip_num = 0
516
- for chunk in tl.v1.chunks:
517
- if chunk[2] == 0 or chunk[2] >= 99999:
518
- continue
519
-
520
- padded_chunks = pad_chunk(chunk, total_frames)
521
-
522
- vspace, aspace = make_av(
523
- tl.v1.source, [clipify(padded_chunks, tl.v1.source)]
524
- )
525
- my_timeline = v3(
526
- tl.tb,
527
- "#000",
528
- tl.template,
529
- vspace,
530
- aspace,
531
- v1(tl.v1.source, padded_chunks),
532
- )
533
-
534
- make_media(my_timeline, append_filename(output, f"-{clip_num}"))
535
- clip_num += 1
536
- else:
537
- make_media(tl, output)
538
-
539
- log.stop_timer()
540
-
541
- if not args.no_open and export == "default":
542
- if args.player is None:
543
- if sys.platform == "win32":
544
- try:
545
- os.startfile(output)
546
- except OSError:
547
- log.warning(f"Could not find application to open file: {output}")
548
- else:
549
- try: # MacOS case
550
- run(["open", output])
551
- except Exception:
552
- try: # WSL2 case
553
- run(["cmd.exe", "/C", "start", output])
554
- except Exception:
555
- try: # Linux case
556
- run(["xdg-open", output])
557
- except Exception:
558
- log.warning(f"Could not open output file: {output}")
559
- else:
560
- run(__import__("shlex").split(args.player) + [output])
File without changes