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 CHANGED
@@ -1 +1 @@
1
- __version__ = "27.1.0"
1
+ __version__ = "28.0.0"
auto_editor/__main__.py CHANGED
@@ -78,7 +78,6 @@ class Args:
78
78
  audio_layout: str | None = None
79
79
  audio_bitrate: str = "auto"
80
80
  mix_audio_streams: bool = False
81
- keep_tracks_separate: bool = False
82
81
  audio_normalize: str = "#f"
83
82
 
84
83
  # Misc.
@@ -353,11 +352,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
353
352
  parser.add_argument(
354
353
  "--mix-audio-streams", flag=True, help="Mix all audio streams together into one"
355
354
  )
356
- parser.add_argument(
357
- "--keep-tracks-separate",
358
- flag=True,
359
- help="Don't mix all audio streams into one when exporting (default)",
360
- )
361
355
  parser.add_argument(
362
356
  "--audio-normalize",
363
357
  metavar="NORM-TYPE",
@@ -447,9 +441,7 @@ def main() -> None:
447
441
  ({"--export-to-resolve", "-exr"}, ["--export", "resolve"]),
448
442
  ({"--export-to-final-cut-pro", "-exf"}, ["--export", "final-cut-pro"]),
449
443
  ({"--export-to-shotcut", "-exs"}, ["--export", "shotcut"]),
450
- ({"--export-as-json"}, ["--export", "json"]),
451
444
  ({"--export-as-clip-sequence", "-excs"}, ["--export", "clip-sequence"]),
452
- ({"--keep-tracks-seperate"}, ["--keep-tracks-separate"]),
453
445
  ({"--edit-based-on"}, ["--edit"]),
454
446
  ],
455
447
  )
auto_editor/cmds/desc.py CHANGED
@@ -1,10 +1,8 @@
1
- from __future__ import annotations
2
-
3
1
  import sys
4
2
  from dataclasses import dataclass, field
5
3
 
6
- from auto_editor.ffwrapper import FileInfo
7
- from auto_editor.utils.log import Log
4
+ import bv
5
+
8
6
  from auto_editor.vanparse import ArgumentParser
9
7
 
10
8
 
@@ -22,11 +20,12 @@ def desc_options(parser: ArgumentParser) -> ArgumentParser:
22
20
  def main(sys_args: list[str] = sys.argv[1:]) -> None:
23
21
  args = desc_options(ArgumentParser("desc")).parse_args(DescArgs, sys_args)
24
22
  for path in args.input:
25
- src = FileInfo.init(path, Log())
26
- if src.description is not None:
27
- sys.stdout.write(f"\n{src.description}\n\n")
28
- else:
29
- sys.stdout.write("\nNo description.\n\n")
23
+ try:
24
+ container = bv.open(path)
25
+ desc = container.metadata.get("description", None)
26
+ except Exception:
27
+ desc = None
28
+ sys.stdout.write("\nNo description.\n\n" if desc is None else f"\n{desc}\n\n")
30
29
 
31
30
 
32
31
  if __name__ == "__main__":
auto_editor/cmds/info.py CHANGED
@@ -8,7 +8,6 @@ from typing import Any, Literal, TypedDict
8
8
  from auto_editor.ffwrapper import FileInfo
9
9
  from auto_editor.json import dump
10
10
  from auto_editor.make_layers import make_sane_timebase
11
- from auto_editor.timeline import v3
12
11
  from auto_editor.utils.func import aspect_ratio
13
12
  from auto_editor.utils.log import Log
14
13
  from auto_editor.vanparse import ArgumentParser
@@ -86,19 +85,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
86
85
  log.error(f"Could not find '{file}'")
87
86
 
88
87
  ext = os.path.splitext(file)[1]
89
- if ext == ".json":
90
- from auto_editor.formats.json import read_json
91
-
92
- tl = read_json(file, log)
93
- file_info[file] = {"type": "timeline"}
94
- file_info[file]["version"] = "v3" if isinstance(tl, v3) else "v1"
95
-
96
- clip_lens = [clip.dur / clip.speed for clip in tl.a[0]]
97
- file_info[file]["clips"] = len(clip_lens)
98
-
99
- continue
100
-
101
- if ext in {".xml", ".fcpxml", ".mlt"}:
88
+ if ext in {".v1", ".v3", ".json", ".xml", ".fcpxml", ".mlt"}:
102
89
  file_info[file] = {"type": "timeline"}
103
90
  continue
104
91
 
auto_editor/cmds/test.py CHANGED
@@ -290,6 +290,7 @@ class Runner:
290
290
 
291
291
  def test_silent_threshold(self):
292
292
  with bv.open("resources/new-commentary.mp3") as container:
293
+ assert container.duration is not None
293
294
  assert container.duration / bv.time_base == 6.732
294
295
 
295
296
  out = self.main(
@@ -298,6 +299,7 @@ class Runner:
298
299
  out += ".mp3"
299
300
 
300
301
  with bv.open(out) as container:
302
+ assert container.duration is not None
301
303
  assert container.duration / bv.time_base == 6.552
302
304
 
303
305
  def test_track(self):
@@ -305,7 +307,9 @@ class Runner:
305
307
  assert len(fileinfo(out).audios) == 2
306
308
 
307
309
  def test_export_json(self):
308
- out = self.main(["example.mp4"], ["--export_as_json"], "c77130d763d40e8.json")
310
+ out = self.main(["example.mp4"], ["--export", "v1"], "c77130d763d40e8.json")
311
+ self.main([out], [])
312
+ out = self.main(["example.mp4"], ["--export", "v1"], "c77130d763d40e8.v1")
309
313
  self.main([out], [])
310
314
 
311
315
  def test_import_v1(self):
@@ -317,10 +321,19 @@ class Runner:
317
321
 
318
322
  self.main([path], [])
319
323
 
320
- def test_premiere_named_export(self):
324
+ def test_res_with_v1(self):
325
+ v1 = self.main(["example.mp4"], ["--export", "v1"], "input.v1")
326
+ out = self.main([v1], ["-res", "720,720"], "output.mp4")
327
+
328
+ output = fileinfo(out)
329
+ assert output.videos[0].width == 720
330
+ assert output.videos[0].height == 720
331
+ assert len(output.audios) == 1
332
+
333
+ def test_premiere_named_export(self) -> None:
321
334
  self.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
322
335
 
323
- def test_export_subtitles(self):
336
+ def test_export_subtitles(self) -> None:
324
337
  # cn = fileinfo(self.main(["resources/mov_text.mp4"], [], "movtext_out.mp4"))
325
338
 
326
339
  # assert len(cn.videos) == 1
@@ -332,7 +345,7 @@ class Runner:
332
345
  assert len(cn.audios) == 1
333
346
  assert len(cn.subtitles) == 1
334
347
 
335
- def test_scale(self):
348
+ def test_scale(self) -> None:
336
349
  cn = fileinfo(self.main(["example.mp4"], ["--scale", "1.5"], "scale.mp4"))
337
350
  assert cn.videos[0].fps == 30
338
351
  assert cn.videos[0].width == 1920
@@ -363,7 +376,7 @@ class Runner:
363
376
  # assert len(cn.videos) == 1
364
377
  # assert len(cn.audios) == 2
365
378
 
366
- def test_premiere(self):
379
+ def test_premiere(self) -> None:
367
380
  for test_name in all_files:
368
381
  if test_name == "multi-track.mov":
369
382
  continue
@@ -379,14 +392,14 @@ class Runner:
379
392
  self.main([test_file], ["-exs"])
380
393
  self.main([test_file], ["--stats"])
381
394
 
382
- def test_clip_sequence(self):
395
+ def test_clip_sequence(self) -> None:
383
396
  for test_name in all_files:
384
397
  test_file = f"resources/{test_name}"
385
- self.main([test_file], ["--export_as_clip_sequence"])
398
+ self.main([test_file], ["--export", "clip-sequence"])
386
399
 
387
- def test_codecs(self):
388
- self.main(["example.mp4"], ["--video_codec", "h264"])
389
- self.main(["example.mp4"], ["--audio_codec", "ac3"])
400
+ def test_codecs(self) -> None:
401
+ self.main(["example.mp4"], ["--video-codec", "h264"])
402
+ self.main(["example.mp4"], ["--audio-codec", "ac3"])
390
403
 
391
404
  # Issue #241
392
405
  def test_multi_track_edit(self):
@@ -512,15 +525,15 @@ class Runner:
512
525
  assert output.videos[0].pix_fmt == "yuv420p"
513
526
 
514
527
  # Issue 280
515
- def test_SAR(self):
528
+ def test_SAR(self) -> None:
516
529
  out = self.main(["resources/SAR-2by3.mp4"], [], "2by3_out.mp4")
517
530
  assert fileinfo(out).videos[0].sar == Fraction(2, 3)
518
531
 
519
- def test_audio_norm_f(self):
520
- return self.main(["example.mp4"], ["--audio-normalize", "#f"])
532
+ def test_audio_norm_f(self) -> None:
533
+ self.main(["example.mp4"], ["--audio-normalize", "#f"])
521
534
 
522
- def test_audio_norm_ebu(self):
523
- return self.main(
535
+ def test_audio_norm_ebu(self) -> None:
536
+ self.main(
524
537
  ["example.mp4"], ["--audio-normalize", "ebu:i=-5,lra=20,gain=5,tp=-1"]
525
538
  )
526
539
 
@@ -536,11 +549,14 @@ class Runner:
536
549
  except MyError as e:
537
550
  raise ValueError(f"{text}\nMyError: {e}")
538
551
 
552
+ result_val = results[-1]
539
553
  if isinstance(expected, np.ndarray):
540
- if not np.array_equal(expected, results[-1]):
554
+ if not isinstance(result_val, np.ndarray):
555
+ raise ValueError(f"{text}: Result is not an ndarray")
556
+ if not np.array_equal(expected, result_val):
541
557
  raise ValueError(f"{text}: Numpy arrays don't match")
542
- elif expected != results[-1]:
543
- raise ValueError(f"{text}: Expected: {expected}, got {results[-1]}")
558
+ elif expected != result_val:
559
+ raise ValueError(f"{text}: Expected: {expected}, got {result_val}")
544
560
 
545
561
  cases(
546
562
  ("345", 345),
@@ -675,7 +691,7 @@ class Runner:
675
691
  ('#(#("sym" "symbol?") "bool?")', [["sym", "symbol?"], "bool?"]),
676
692
  )
677
693
 
678
- def palet_scripts(self):
694
+ def palet_scripts(self) -> None:
679
695
  self.raw(["palet", "resources/scripts/scope.pal"])
680
696
  self.raw(["palet", "resources/scripts/maxcut.pal"])
681
697
  self.raw(["palet", "resources/scripts/case.pal"])
auto_editor/edit.py CHANGED
@@ -5,6 +5,7 @@ import sys
5
5
  from fractions import Fraction
6
6
  from heapq import heappop, heappush
7
7
  from os.path import splitext
8
+ from pathlib import Path
8
9
  from subprocess import run
9
10
  from typing import TYPE_CHECKING, Any
10
11
 
@@ -16,7 +17,7 @@ from auto_editor.make_layers import clipify, make_av, make_timeline
16
17
  from auto_editor.render.audio import make_new_audio
17
18
  from auto_editor.render.subtitle import make_new_subtitles
18
19
  from auto_editor.render.video import render_av
19
- from auto_editor.timeline import v1, v3
20
+ from auto_editor.timeline import set_stream_to_0, v1, v3
20
21
  from auto_editor.utils.bar import initBar
21
22
  from auto_editor.utils.chunks import Chunk, Chunks
22
23
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
@@ -28,26 +29,33 @@ if TYPE_CHECKING:
28
29
 
29
30
 
30
31
  def set_output(
31
- out: str | None, _export: str | None, src: FileInfo | None, log: Log
32
+ out: str | None, _export: str | None, path: Path | None, log: Log
32
33
  ) -> tuple[str, dict[str, Any]]:
33
- if src is None:
34
- root, ext = "out", ".mp4"
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)
35
38
  else:
36
- root, ext = splitext(src.path if out is None else out)
37
- if ext == "":
38
- ext = src.path.suffix
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
39
44
 
40
45
  if _export is None:
41
- if ext == ".xml":
42
- export = {"export": "premiere"}
43
- elif ext == ".fcpxml":
44
- export = {"export": "final-cut-pro"}
45
- elif ext == ".mlt":
46
- export = {"export": "shotcut"}
47
- elif ext == ".json":
48
- export = {"export": "json"}
49
- else:
50
- export = {"export": "default"}
46
+ match ext:
47
+ case ".xml":
48
+ export: dict[str, Any] = {"export": "premiere"}
49
+ case ".fcpxml":
50
+ export = {"export": "final-cut-pro"}
51
+ case ".mlt":
52
+ export = {"export": "shotcut"}
53
+ case ".json" | ".v1":
54
+ export = {"export": "v1"}
55
+ case ".v3":
56
+ export = {"export": "v3"}
57
+ case _:
58
+ export = {"export": "default"}
51
59
  else:
52
60
  export = parse_export(_export, log)
53
61
 
@@ -58,11 +66,13 @@ def set_output(
58
66
  "resolve": ".fcpxml",
59
67
  "shotcut": ".mlt",
60
68
  "json": ".json",
61
- "audio": ".wav",
62
69
  }
63
70
  if export["export"] in ext_map:
64
71
  ext = ext_map[export["export"]]
65
72
 
73
+ if out == "-":
74
+ return "-", export
75
+
66
76
  if out is None:
67
77
  return f"{root}_ALTERED{ext}", export
68
78
 
@@ -129,65 +139,57 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
129
139
  name, text = exploded
130
140
 
131
141
  name_attr = pAttr("name", "Auto-Editor Media Group", is_str)
132
-
133
- parsing: dict[str, pAttrs] = {
142
+ parsing = {
134
143
  "default": pAttrs("default"),
135
144
  "premiere": pAttrs("premiere", name_attr),
136
- "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
137
145
  "final-cut-pro": pAttrs(
138
146
  "final-cut-pro", name_attr, pAttr("version", 11, is_int)
139
147
  ),
140
148
  "resolve": pAttrs("resolve", name_attr),
149
+ "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
141
150
  "shotcut": pAttrs("shotcut"),
142
- "json": pAttrs("json", pAttr("api", 3, is_int)),
143
- "timeline": pAttrs("json", pAttr("api", 3, is_int)),
144
- "audio": pAttrs("audio"),
151
+ "v1": pAttrs("v1"),
152
+ "v3": pAttrs("v3"),
145
153
  "clip-sequence": pAttrs("clip-sequence"),
146
154
  }
147
155
 
148
156
  if name in parsing:
149
157
  try:
150
- _tmp = parse_with_palet(text, parsing[name], {})
151
- _tmp["export"] = name
152
- return _tmp
158
+ return {"export": name} | parse_with_palet(text, parsing[name], {})
153
159
  except ParserError as e:
154
160
  log.error(e)
155
161
 
156
- log.error(f"'{name}': Export must be [{', '.join([s for s in parsing.keys()])}]")
162
+ valid_choices = " ".join(f'"{s}"' for s in parsing.keys())
163
+ log.error(f'Invalid export format: "{name}"\nValid choices: {valid_choices}')
157
164
 
158
165
 
159
166
  def edit_media(paths: list[str], args: Args, log: Log) -> None:
160
167
  bar = initBar(args.progress)
161
- tl = None
162
- src = None
163
-
164
- if args.keep_tracks_separate:
165
- log.deprecated("--keep-tracks-separate is deprecated.")
166
- args.keep_tracks_separate = False
168
+ tl = src = use_path = None
167
169
 
168
170
  if paths:
169
171
  path_ext = splitext(paths[0])[1].lower()
170
172
  if path_ext == ".xml":
171
- from auto_editor.formats.fcp7 import fcp7_read_xml
173
+ from auto_editor.imports.fcp7 import fcp7_read_xml
172
174
 
173
175
  tl = fcp7_read_xml(paths[0], log)
174
176
  elif path_ext == ".mlt":
175
- from auto_editor.formats.shotcut import shotcut_read_mlt
176
-
177
- tl = shotcut_read_mlt(paths[0], log)
178
- elif path_ext == ".json":
179
- from auto_editor.formats.json import read_json
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
180
 
181
181
  tl = read_json(paths[0], log)
182
182
  else:
183
183
  sources = [FileInfo.init(path, log) for path in paths]
184
- src = None if not sources else sources[0]
184
+ src = sources[0]
185
+ use_path = src.path
185
186
 
186
- output, export_ops = set_output(args.output, args.export, src, log)
187
+ output, export_ops = set_output(args.output, args.export, use_path, log)
187
188
  assert "export" in export_ops
188
189
  export = export_ops["export"]
189
190
 
190
- if export == "timeline":
191
+ if output == "-":
192
+ # When printing to stdout, silence all logs.
191
193
  log.quiet = True
192
194
 
193
195
  if not args.preview:
@@ -206,12 +208,15 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
206
208
 
207
209
  if tl is None:
208
210
  tl = make_timeline(sources, args, samplerate, bar, log)
209
-
210
- if export == "timeline":
211
- from auto_editor.formats.json import make_json_timeline
212
-
213
- make_json_timeline(export_ops["api"], 0, tl, log)
214
- return
211
+ else:
212
+ if args.resolution is not None:
213
+ tl.T.res = args.resolution
214
+ if args.background is not None:
215
+ tl.background = args.background
216
+ if args.frame_rate is not None:
217
+ log.warning(
218
+ "Setting timebase/framerate is not supported when importing timelines"
219
+ )
215
220
 
216
221
  if args.preview:
217
222
  from auto_editor.preview import preview
@@ -219,29 +224,28 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
219
224
  preview(tl, log)
220
225
  return
221
226
 
222
- if export == "json":
223
- from auto_editor.formats.json import make_json_timeline
227
+ if export in {"v1", "v3"}:
228
+ from auto_editor.exports.json import make_json_timeline
224
229
 
225
- make_json_timeline(export_ops["api"], output, tl, log)
230
+ make_json_timeline(export, output, tl, log)
226
231
  return
227
232
 
228
233
  if export in {"premiere", "resolve-fcp7"}:
229
- from auto_editor.formats.fcp7 import fcp7_write_xml
234
+ from auto_editor.exports.fcp7 import fcp7_write_xml
230
235
 
231
236
  is_resolve = export.startswith("resolve")
232
237
  fcp7_write_xml(export_ops["name"], output, is_resolve, tl)
233
238
  return
234
239
 
235
240
  if export == "final-cut-pro":
236
- from auto_editor.formats.fcp11 import fcp11_write_xml
241
+ from auto_editor.exports.fcp11 import fcp11_write_xml
237
242
 
238
243
  ver = export_ops["version"]
239
244
  fcp11_write_xml(export_ops["name"], ver, output, False, tl, log)
240
245
  return
241
246
 
242
247
  if export == "resolve":
243
- from auto_editor.formats.fcp11 import fcp11_write_xml
244
- from auto_editor.timeline import set_stream_to_0
248
+ from auto_editor.exports.fcp11 import fcp11_write_xml
245
249
 
246
250
  assert src is not None
247
251
  set_stream_to_0(src, tl, log)
@@ -249,11 +253,13 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
249
253
  return
250
254
 
251
255
  if export == "shotcut":
252
- from auto_editor.formats.shotcut import shotcut_write_mlt
256
+ from auto_editor.exports.shotcut import shotcut_write_mlt
253
257
 
254
258
  shotcut_write_mlt(output, tl)
255
259
  return
256
260
 
261
+ if output == "-":
262
+ log.error("Exporting media files to stdout is not supported.")
257
263
  out_ext = splitext(output)[1].replace(".", "")
258
264
 
259
265
  # Check if export options make sense.
@@ -517,7 +523,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
517
523
 
518
524
  log.stop_timer()
519
525
 
520
- if not args.no_open and export in {"default", "audio"}:
526
+ if not args.no_open and export == "default":
521
527
  if args.player is None:
522
528
  if sys.platform == "win32":
523
529
  try:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import xml.etree.ElementTree as ET
3
4
  from typing import TYPE_CHECKING, Any, cast
4
5
  from xml.etree.ElementTree import Element, ElementTree, SubElement, indent
5
6
 
@@ -70,7 +71,7 @@ def fcp11_write_xml(
70
71
  resources = SubElement(fcpxml, "resources")
71
72
 
72
73
  src_dur = 0
73
- tl_dur = 0 if resolve else tl.out_len()
74
+ tl_dur = 0 if resolve else len(tl)
74
75
 
75
76
  for i, one_src in enumerate(tl.unique_sources()):
76
77
  if i == 0:
@@ -162,4 +163,7 @@ def fcp11_write_xml(
162
163
 
163
164
  tree = ElementTree(fcpxml)
164
165
  indent(tree, space="\t", level=0)
165
- tree.write(output, xml_declaration=True, encoding="utf-8")
166
+ if output == "-":
167
+ print(ET.tostring(fcpxml, encoding="unicode"))
168
+ else:
169
+ tree.write(output, xml_declaration=True, encoding="utf-8")