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