auto-editor 27.1.1__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.1"
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
@@ -17,7 +17,7 @@ from auto_editor.make_layers import clipify, make_av, make_timeline
17
17
  from auto_editor.render.audio import make_new_audio
18
18
  from auto_editor.render.subtitle import make_new_subtitles
19
19
  from auto_editor.render.video import render_av
20
- from auto_editor.timeline import v1, v3
20
+ from auto_editor.timeline import set_stream_to_0, v1, v3
21
21
  from auto_editor.utils.bar import initBar
22
22
  from auto_editor.utils.chunks import Chunk, Chunks
23
23
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
31
31
  def set_output(
32
32
  out: str | None, _export: str | None, path: Path | None, log: Log
33
33
  ) -> tuple[str, dict[str, Any]]:
34
- if out is None:
34
+ if out is None or out == "-":
35
35
  if path is None:
36
36
  log.error("`--output` must be set.") # When a timeline file is the input.
37
37
  root, ext = splitext(path)
@@ -43,16 +43,19 @@ def set_output(
43
43
  ext = ".mp4" if path is None else path.suffix
44
44
 
45
45
  if _export is None:
46
- if ext == ".xml":
47
- export = {"export": "premiere"}
48
- elif ext == ".fcpxml":
49
- export = {"export": "final-cut-pro"}
50
- elif ext == ".mlt":
51
- export = {"export": "shotcut"}
52
- elif ext == ".json":
53
- export = {"export": "json"}
54
- else:
55
- 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"}
56
59
  else:
57
60
  export = parse_export(_export, log)
58
61
 
@@ -63,11 +66,13 @@ def set_output(
63
66
  "resolve": ".fcpxml",
64
67
  "shotcut": ".mlt",
65
68
  "json": ".json",
66
- "audio": ".wav",
67
69
  }
68
70
  if export["export"] in ext_map:
69
71
  ext = ext_map[export["export"]]
70
72
 
73
+ if out == "-":
74
+ return "-", export
75
+
71
76
  if out is None:
72
77
  return f"{root}_ALTERED{ext}", export
73
78
 
@@ -134,53 +139,44 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
134
139
  name, text = exploded
135
140
 
136
141
  name_attr = pAttr("name", "Auto-Editor Media Group", is_str)
137
-
138
- parsing: dict[str, pAttrs] = {
142
+ parsing = {
139
143
  "default": pAttrs("default"),
140
144
  "premiere": pAttrs("premiere", name_attr),
141
- "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
142
145
  "final-cut-pro": pAttrs(
143
146
  "final-cut-pro", name_attr, pAttr("version", 11, is_int)
144
147
  ),
145
148
  "resolve": pAttrs("resolve", name_attr),
149
+ "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
146
150
  "shotcut": pAttrs("shotcut"),
147
- "json": pAttrs("json", pAttr("api", 3, is_int)),
148
- "timeline": pAttrs("json", pAttr("api", 3, is_int)),
149
- "audio": pAttrs("audio"),
151
+ "v1": pAttrs("v1"),
152
+ "v3": pAttrs("v3"),
150
153
  "clip-sequence": pAttrs("clip-sequence"),
151
154
  }
152
155
 
153
156
  if name in parsing:
154
157
  try:
155
- _tmp = parse_with_palet(text, parsing[name], {})
156
- _tmp["export"] = name
157
- return _tmp
158
+ return {"export": name} | parse_with_palet(text, parsing[name], {})
158
159
  except ParserError as e:
159
160
  log.error(e)
160
161
 
161
- 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}')
162
164
 
163
165
 
164
166
  def edit_media(paths: list[str], args: Args, log: Log) -> None:
165
167
  bar = initBar(args.progress)
166
168
  tl = src = use_path = None
167
169
 
168
- if args.keep_tracks_separate:
169
- log.deprecated("--keep-tracks-separate is deprecated.")
170
- args.keep_tracks_separate = False
171
-
172
170
  if paths:
173
171
  path_ext = splitext(paths[0])[1].lower()
174
172
  if path_ext == ".xml":
175
- from auto_editor.formats.fcp7 import fcp7_read_xml
173
+ from auto_editor.imports.fcp7 import fcp7_read_xml
176
174
 
177
175
  tl = fcp7_read_xml(paths[0], log)
178
176
  elif path_ext == ".mlt":
179
- from auto_editor.formats.shotcut import shotcut_read_mlt
180
-
181
- tl = shotcut_read_mlt(paths[0], log)
182
- elif path_ext == ".json":
183
- 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
184
180
 
185
181
  tl = read_json(paths[0], log)
186
182
  else:
@@ -192,7 +188,8 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
192
188
  assert "export" in export_ops
193
189
  export = export_ops["export"]
194
190
 
195
- if export == "timeline":
191
+ if output == "-":
192
+ # When printing to stdout, silence all logs.
196
193
  log.quiet = True
197
194
 
198
195
  if not args.preview:
@@ -211,12 +208,15 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
211
208
 
212
209
  if tl is None:
213
210
  tl = make_timeline(sources, args, samplerate, bar, log)
214
-
215
- if export == "timeline":
216
- from auto_editor.formats.json import make_json_timeline
217
-
218
- make_json_timeline(export_ops["api"], 0, tl, log)
219
- 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
+ )
220
220
 
221
221
  if args.preview:
222
222
  from auto_editor.preview import preview
@@ -224,29 +224,28 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
224
224
  preview(tl, log)
225
225
  return
226
226
 
227
- if export == "json":
228
- 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
229
229
 
230
- make_json_timeline(export_ops["api"], output, tl, log)
230
+ make_json_timeline(export, output, tl, log)
231
231
  return
232
232
 
233
233
  if export in {"premiere", "resolve-fcp7"}:
234
- from auto_editor.formats.fcp7 import fcp7_write_xml
234
+ from auto_editor.exports.fcp7 import fcp7_write_xml
235
235
 
236
236
  is_resolve = export.startswith("resolve")
237
237
  fcp7_write_xml(export_ops["name"], output, is_resolve, tl)
238
238
  return
239
239
 
240
240
  if export == "final-cut-pro":
241
- from auto_editor.formats.fcp11 import fcp11_write_xml
241
+ from auto_editor.exports.fcp11 import fcp11_write_xml
242
242
 
243
243
  ver = export_ops["version"]
244
244
  fcp11_write_xml(export_ops["name"], ver, output, False, tl, log)
245
245
  return
246
246
 
247
247
  if export == "resolve":
248
- from auto_editor.formats.fcp11 import fcp11_write_xml
249
- from auto_editor.timeline import set_stream_to_0
248
+ from auto_editor.exports.fcp11 import fcp11_write_xml
250
249
 
251
250
  assert src is not None
252
251
  set_stream_to_0(src, tl, log)
@@ -254,11 +253,13 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
254
253
  return
255
254
 
256
255
  if export == "shotcut":
257
- from auto_editor.formats.shotcut import shotcut_write_mlt
256
+ from auto_editor.exports.shotcut import shotcut_write_mlt
258
257
 
259
258
  shotcut_write_mlt(output, tl)
260
259
  return
261
260
 
261
+ if output == "-":
262
+ log.error("Exporting media files to stdout is not supported.")
262
263
  out_ext = splitext(output)[1].replace(".", "")
263
264
 
264
265
  # Check if export options make sense.
@@ -522,7 +523,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
522
523
 
523
524
  log.stop_timer()
524
525
 
525
- if not args.no_open and export in {"default", "audio"}:
526
+ if not args.no_open and export == "default":
526
527
  if args.player is None:
527
528
  if sys.platform == "win32":
528
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")