auto-editor 28.0.0__py3-none-any.whl → 28.0.2__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__ = "28.0.0"
1
+ __version__ = "28.0.2"
auto_editor/cmds/cache.py CHANGED
@@ -26,7 +26,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
26
26
  return
27
27
 
28
28
  def format_bytes(size: float) -> str:
29
- for unit in {"B", "KiB", "MiB", "GiB", "TiB"}:
29
+ for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
30
30
  if size < 1024:
31
31
  return f"{size:.2f} {unit}"
32
32
  size /= 1024
auto_editor/cmds/info.py CHANGED
@@ -113,7 +113,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
113
113
  f"{recTb.numerator}/{recTb.denominator}"
114
114
  )
115
115
 
116
- for track, v in enumerate(src.videos):
116
+ for v in src.videos:
117
117
  w, h = v.width, v.height
118
118
 
119
119
  vid: VideoJson = {
@@ -134,7 +134,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
134
134
  }
135
135
  file_info[file]["video"].append(vid)
136
136
 
137
- for track, a in enumerate(src.audios):
137
+ for a in src.audios:
138
138
  aud: AudioJson = {
139
139
  "codec": a.codec,
140
140
  "layout": a.layout,
@@ -145,7 +145,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
145
145
  }
146
146
  file_info[file]["audio"].append(aud)
147
147
 
148
- for track, s_stream in enumerate(src.subtitles):
148
+ for s_stream in src.subtitles:
149
149
  sub: SubtitleJson = {"codec": s_stream.codec, "lang": s_stream.lang}
150
150
  file_info[file]["subtitle"].append(sub)
151
151
 
@@ -36,6 +36,7 @@ class LevelArgs:
36
36
  input: list[str] = field(default_factory=list)
37
37
  edit: str = "audio"
38
38
  timebase: Fraction | None = None
39
+ no_cache: bool = False
39
40
  help: bool = False
40
41
 
41
42
 
@@ -53,16 +54,14 @@ def levels_options(parser: ArgumentParser) -> ArgumentParser:
53
54
  type=frame_rate,
54
55
  help="Set custom timebase",
55
56
  )
57
+ parser.add_argument("--no-cache", flag=True)
56
58
  return parser
57
59
 
58
60
 
59
61
  def print_arr(arr: NDArray) -> None:
60
62
  print("")
61
63
  print("@start")
62
- if arr.dtype in {np.float64, np.float32, np.float16}:
63
- for a in arr:
64
- sys.stdout.write(f"{a:.20f}\n")
65
- elif arr.dtype == np.bool_:
64
+ if arr.dtype == np.bool_:
66
65
  for a in arr:
67
66
  sys.stdout.write(f"{1 if a else 0}\n")
68
67
  else:
@@ -76,7 +75,7 @@ def print_arr_gen(arr: Iterator[float | np.float32]) -> None:
76
75
  print("")
77
76
  print("@start")
78
77
  for a in arr:
79
- print(f"{a:.20f}")
78
+ print(f"{a}")
80
79
  print("")
81
80
 
82
81
 
@@ -131,7 +130,11 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
131
130
  levels = initLevels(src, tb, bar, False, log)
132
131
  try:
133
132
  if method == "audio":
134
- if (arr := levels.read_cache("audio", (obj["stream"],))) is not None:
133
+ if (
134
+ not args.no_cache
135
+ and (arr := levels.read_cache("audio", (obj["stream"],)))
136
+ is not None
137
+ ):
135
138
  print_arr(arr)
136
139
  else:
137
140
  container = bv.open(src.path, "r")
@@ -148,11 +151,15 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
148
151
  container.close()
149
152
 
150
153
  cache_array = np.array(values, dtype=np.float32)
151
- levels.cache(cache_array, "audio", (obj["stream"],))
154
+ if not args.no_cache:
155
+ levels.cache(cache_array, "audio", (obj["stream"],))
152
156
 
153
157
  elif method == "motion":
154
158
  mobj = (obj["stream"], obj["width"], obj["blur"])
155
- if (arr := levels.read_cache("motion", mobj)) is not None:
159
+ if (
160
+ not args.no_cache
161
+ and (arr := levels.read_cache("motion", mobj)) is not None
162
+ ):
156
163
  print_arr(arr)
157
164
  else:
158
165
  container = bv.open(src.path, "r")
@@ -171,7 +178,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
171
178
  container.close()
172
179
 
173
180
  cache_array = np.array(values, dtype=np.float32)
174
- levels.cache(cache_array, "motion", mobj)
181
+ if not args.no_cache:
182
+ levels.cache(cache_array, "motion", mobj)
175
183
 
176
184
  elif method == "subtitle":
177
185
  print_arr(levels.subtitle(**obj))
auto_editor/cmds/palet.py CHANGED
@@ -14,7 +14,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
14
14
 
15
15
  env.update(make_standard_env())
16
16
  try:
17
- interpret(env, Parser(Lexer(sys_args[0], program_text, True)))
17
+ interpret(env, Parser(Lexer(sys_args[0], program_text)))
18
18
  except (MyError, ZeroDivisionError) as e:
19
19
  sys.stderr.write(f"error: {e}\n")
20
20
  sys.exit(1)
@@ -21,8 +21,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
21
21
  parser.add_argument("--json", flag=True)
22
22
  args = parser.parse_args(SubdumpArgs, sys_args)
23
23
 
24
- do_filter = True
25
-
26
24
  if args.json:
27
25
  data = {}
28
26
  for input_file in args.input:
@@ -46,7 +44,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
46
44
  startf = round(float(start), 3)
47
45
  endf = round(float(end), 3)
48
46
 
49
- if do_filter and endf - startf <= 0.02:
47
+ if endf - startf <= 0.02:
50
48
  continue
51
49
 
52
50
  for sub in packet.decode():
@@ -60,7 +58,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
60
58
  dump(data, sys.stdout, indent=4)
61
59
  return
62
60
 
63
- for i, input_file in enumerate(args.input):
61
+ for input_file in args.input:
64
62
  with bv.open(input_file) as container:
65
63
  for s in range(len(container.streams.subtitles)):
66
64
  print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
auto_editor/cmds/test.py CHANGED
@@ -202,6 +202,19 @@ class Runner:
202
202
  assert audio.language == "eng"
203
203
  assert audio.layout.name == "stereo"
204
204
 
205
+ def test_video_to_mp3(self) -> None:
206
+ out = self.main(["example.mp4"], [], output="example_ALTERED.mp3")
207
+ with bv.open(out) as container:
208
+ assert container.duration is not None
209
+ assert container.duration > 17300000 and container.duration < 2 << 24
210
+
211
+ assert len(container.streams) == 1
212
+ audio = container.streams[0]
213
+ assert isinstance(audio, bv.AudioStream)
214
+ assert audio.codec.name in ("mp3", "mp3float")
215
+ assert audio.sample_rate == 48000
216
+ assert audio.layout.name == "stereo"
217
+
205
218
  def test_to_mono(self) -> None:
206
219
  out = self.main(["example.mp4"], ["-layout", "mono"], output="example_mono.mp4")
207
220
  with bv.open(out) as container:
@@ -516,9 +529,6 @@ class Runner:
516
529
  assert output.videos[0].pix_fmt == "yuv420p"
517
530
 
518
531
  def test_encode_hevc(self):
519
- if os.environ.get("GITHUB_ACTIONS") == "true":
520
- raise SkipTest()
521
-
522
532
  out = self.main(["resources/testsrc.mp4"], ["-c:v", "hevc"], "out.mkv")
523
533
  output = fileinfo(out)
524
534
  assert output.videos[0].codec == "hevc"
@@ -563,15 +573,12 @@ class Runner:
563
573
  ("238.5", 238.5),
564
574
  ("-34", -34),
565
575
  ("-98.3", -98.3),
566
- ("+3i", 3j),
567
576
  ("3sec", 90),
568
577
  ("-3sec", -90),
569
578
  ("0.2sec", 6),
570
579
  ("(+ 4 3)", 7),
571
580
  ("(+ 4 3 2)", 9),
572
581
  ("(+ 10.5 3)", 13.5),
573
- ("(+ 3+4i -2-2i)", 1 + 2j),
574
- ("(+ 3+4i -2-2i 5)", 6 + 2j),
575
582
  ("(- 4 3)", 1),
576
583
  ("(- 3)", -3),
577
584
  ("(- 10.5 3)", 7.5),
@@ -580,7 +587,6 @@ class Runner:
580
587
  ("(/ 5)", 0.2),
581
588
  ("(/ 6 1)", 6.0),
582
589
  ("30/1", Fraction(30)),
583
- ("(sqrt -4)", 2j),
584
590
  ("(pow 2 3)", 8),
585
591
  ("(pow 4 0.5)", 2.0),
586
592
  ("(abs 1.0)", 1.0),
@@ -595,7 +601,6 @@ class Runner:
595
601
  ("(int? #t)", False),
596
602
  ("(int? #f)", False),
597
603
  ("(int? 4/5)", False),
598
- ("(int? 0+2i)", False),
599
604
  ('(int? "hello")', False),
600
605
  ('(int? "3")', False),
601
606
  ("(float? -23.4)", True),
@@ -609,7 +614,6 @@ class Runner:
609
614
  ('(define apple "Red Wood") apple', "Red Wood"),
610
615
  ("(= 1 1.0)", True),
611
616
  ("(= 1 2)", False),
612
- ("(= 2+3i 2+3i 2+3i)", True),
613
617
  ("(= 1)", True),
614
618
  ("(+)", 0),
615
619
  ("(*)", 1),
@@ -618,7 +622,6 @@ class Runner:
618
622
  ('(if #f mango "Hi")', "Hi"),
619
623
  ('{if (= [+ 3 4] 7) "yes" "no"}', "yes"),
620
624
  ("((if #t + -) 3 4)", 7),
621
- ("((if #t + oops) 3+3i 4-2i)", 7 + 1j),
622
625
  ("((if #f + -) 3 4)", -1),
623
626
  ("(when (positive? 3) 17)", 17),
624
627
  ("(string)", ""),
@@ -698,7 +701,7 @@ class Runner:
698
701
  self.raw(["palet", "resources/scripts/testmath.pal"])
699
702
 
700
703
 
701
- def run_tests(runner: Runner, tests: list[Callable], args: TestArgs) -> None:
704
+ def run_tests(tests: list[Callable], args: TestArgs) -> None:
702
705
  if args.only != []:
703
706
  tests = list(filter(lambda t: t.__name__ in args.only, tests))
704
707
 
@@ -797,7 +800,7 @@ def main(sys_args: list[str] | None = None) -> None:
797
800
  ]
798
801
  )
799
802
  try:
800
- run_tests(run, tests, args)
803
+ run_tests(tests, args)
801
804
  except KeyboardInterrupt:
802
805
  print("Testing Interrupted by User.")
803
806
  shutil.rmtree(run.temp_dir)
auto_editor/edit.py CHANGED
@@ -29,8 +29,8 @@ if TYPE_CHECKING:
29
29
 
30
30
 
31
31
  def set_output(
32
- out: str | None, _export: str | None, path: Path | None, log: Log
33
- ) -> tuple[str, dict[str, Any]]:
32
+ out: str | None, export: str | None, path: Path | None, log: Log
33
+ ) -> tuple[str, str]:
34
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.
@@ -42,33 +42,33 @@ def set_output(
42
42
  # Use `mp4` as the default, because it is most compatible.
43
43
  ext = ".mp4" if path is None else path.suffix
44
44
 
45
- if _export is None:
45
+ if export is None:
46
46
  match ext:
47
47
  case ".xml":
48
- export: dict[str, Any] = {"export": "premiere"}
48
+ export = "premiere"
49
49
  case ".fcpxml":
50
- export = {"export": "final-cut-pro"}
50
+ export = "final-cut-pro"
51
51
  case ".mlt":
52
- export = {"export": "shotcut"}
52
+ export = "shotcut"
53
53
  case ".json" | ".v1":
54
- export = {"export": "v1"}
54
+ export = "v1"
55
55
  case ".v3":
56
- export = {"export": "v3"}
56
+ export = "v3"
57
57
  case _:
58
- export = {"export": "default"}
59
- else:
60
- export = parse_export(_export, log)
61
-
62
- ext_map = {
63
- "premiere": ".xml",
64
- "resolve-fcp7": ".xml",
65
- "final-cut-pro": ".fcpxml",
66
- "resolve": ".fcpxml",
67
- "shotcut": ".mlt",
68
- "json": ".json",
69
- }
70
- if export["export"] in ext_map:
71
- ext = ext_map[export["export"]]
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
72
 
73
73
  if out == "-":
74
74
  return "-", export
@@ -184,9 +184,13 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
184
184
  src = sources[0]
185
185
  use_path = src.path
186
186
 
187
- output, export_ops = set_output(args.output, args.export, use_path, log)
188
- assert "export" in export_ops
189
- export = export_ops["export"]
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)
190
194
 
191
195
  if output == "-":
192
196
  # When printing to stdout, silence all logs.
@@ -247,8 +251,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
247
251
  if export == "resolve":
248
252
  from auto_editor.exports.fcp11 import fcp11_write_xml
249
253
 
250
- assert src is not None
251
- set_stream_to_0(src, tl, log)
254
+ set_stream_to_0(tl, log)
252
255
  fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
253
256
  return
254
257
 
@@ -287,7 +290,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
287
290
  output = bv.open(output_path, "w", container_options=options)
288
291
 
289
292
  # Setup video
290
- if ctr.default_vid != "none" and tl.v:
293
+ if ctr.default_vid not in ("none", "png") and tl.v:
291
294
  vframes = render_av(output, tl, args, log)
292
295
  output_stream: bv.VideoStream | None
293
296
  output_stream = next(vframes) # type: ignore
@@ -1,17 +1,10 @@
1
- from __future__ import annotations
2
-
3
1
  import xml.etree.ElementTree as ET
4
- from typing import TYPE_CHECKING, Any, cast
2
+ from fractions import Fraction
5
3
  from xml.etree.ElementTree import Element, ElementTree, SubElement, indent
6
4
 
7
- if TYPE_CHECKING:
8
- from collections.abc import Sequence
9
- from fractions import Fraction
10
-
11
- from auto_editor.ffwrapper import FileInfo
12
- from auto_editor.timeline import TlAudio, TlVideo, v3
13
- from auto_editor.utils.log import Log
14
-
5
+ from auto_editor.ffwrapper import FileInfo
6
+ from auto_editor.timeline import Clip, v3
7
+ from auto_editor.utils.log import Log
15
8
 
16
9
  """
17
10
  Export a FCPXML 11 file readable with Final Cut Pro 10.6.8 or later.
@@ -52,6 +45,35 @@ def make_name(src: FileInfo, tb: Fraction) -> str:
52
45
  return "FFVideoFormatRateUndefined"
53
46
 
54
47
 
48
+ def parseSMPTE(val: str, fps: Fraction, log: Log) -> int:
49
+ if len(val) == 0:
50
+ return 0
51
+ try:
52
+ parts = val.split(":")
53
+ if len(parts) != 4:
54
+ raise ValueError(f"Invalid SMPTE format: {val}")
55
+
56
+ hours, minutes, seconds, frames = map(int, parts)
57
+
58
+ if (
59
+ hours < 0
60
+ or minutes < 0
61
+ or minutes >= 60
62
+ or seconds < 0
63
+ or seconds >= 60
64
+ or frames < 0
65
+ ):
66
+ raise ValueError(f"Invalid SMPTE values: {val}")
67
+
68
+ if frames >= fps:
69
+ raise ValueError(f"Frame count {frames} exceeds fps {fps}")
70
+
71
+ total_frames = (hours * 3600 + minutes * 60 + seconds) * fps + frames
72
+ return int(round(total_frames))
73
+ except (ValueError, ZeroDivisionError) as e:
74
+ log.error(f"Cannot parse SMPTE timecode '{val}': {e}")
75
+
76
+
55
77
  def fcp11_write_xml(
56
78
  group_name: str, version: int, output: str, resolve: bool, tl: v3, log: Log
57
79
  ) -> None:
@@ -90,12 +112,14 @@ def fcp11_write_xml(
90
112
  height=f"{tl.res[1]}",
91
113
  colorSpace=get_colorspace(one_src),
92
114
  )
115
+
116
+ startPoint = parseSMPTE(one_src.timecode, tl.tb, log)
93
117
  r2 = SubElement(
94
118
  resources,
95
119
  "asset",
96
120
  id=f"r{i * 2 + 2}",
97
121
  name=one_src.path.stem,
98
- start="0s",
122
+ start=fraction(startPoint),
99
123
  hasVideo="1" if one_src.videos else "0",
100
124
  format=f"r{i * 2 + 1}",
101
125
  hasAudio="1" if one_src.audios else "0",
@@ -121,13 +145,15 @@ def fcp11_write_xml(
121
145
  )
122
146
  spine = SubElement(sequence, "spine")
123
147
 
124
- def make_clip(ref: str, clip: TlVideo | TlAudio) -> None:
148
+ def make_clip(ref: str, clip: Clip) -> None:
149
+ startPoint = parseSMPTE(clip.src.timecode, tl.tb, log)
150
+
125
151
  clip_properties = {
126
152
  "name": proj_name,
127
153
  "ref": ref,
128
- "offset": fraction(clip.start),
154
+ "offset": fraction(clip.start + startPoint),
129
155
  "duration": fraction(clip.dur),
130
- "start": fraction(clip.offset),
156
+ "start": fraction(clip.offset + startPoint),
131
157
  "tcFormat": "NDF",
132
158
  }
133
159
  asset = SubElement(spine, "asset-clip", clip_properties)
@@ -146,7 +172,7 @@ def fcp11_write_xml(
146
172
  )
147
173
 
148
174
  if tl.v and tl.v[0]:
149
- clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
175
+ clips = [clip for clip in tl.v[0] if isinstance(clip, Clip)]
150
176
  elif tl.a and tl.a[0]:
151
177
  clips = tl.a[0]
152
178
  else:
@@ -3,14 +3,10 @@ from __future__ import annotations
3
3
  import xml.etree.ElementTree as ET
4
4
  from fractions import Fraction
5
5
  from math import ceil
6
- from typing import TYPE_CHECKING
7
6
  from xml.etree.ElementTree import Element
8
7
 
9
8
  from auto_editor.ffwrapper import FileInfo
10
- from auto_editor.timeline import TlVideo, v3
11
-
12
- if TYPE_CHECKING:
13
- from auto_editor.utils.log import Log
9
+ from auto_editor.timeline import Clip, v3
14
10
 
15
11
  """
16
12
  Premiere Pro uses the Final Cut Pro 7 XML Interchange Format
@@ -69,39 +65,6 @@ def speedup(speed: float) -> Element:
69
65
  return fil
70
66
 
71
67
 
72
- SUPPORTED_EFFECTS = ("timeremap",)
73
-
74
-
75
- def read_filters(clipitem: Element, log: Log) -> float:
76
- for effect_tag in clipitem:
77
- if effect_tag.tag in {"enabled", "start", "end"}:
78
- continue
79
- if len(effect_tag) < 3:
80
- log.error("<effect> requires: <effectid> <name> and one <parameter>")
81
- for i, effects in enumerate(effect_tag):
82
- if i == 0 and effects.tag != "name":
83
- log.error("<effect>: <name> must be first tag")
84
- if i == 1 and effects.tag != "effectid":
85
- log.error("<effect>: <effectid> must be second tag")
86
- if effects.text not in SUPPORTED_EFFECTS:
87
- log.error(f"`{effects.text}` is not a supported effect.")
88
-
89
- if i > 1:
90
- for j, parms in enumerate(effects):
91
- if j == 0:
92
- if parms.tag != "parameterid":
93
- log.error("<parameter>: <parameterid> must be first tag")
94
- if parms.text != "speed":
95
- break
96
-
97
- if j > 0 and parms.tag == "value":
98
- if parms.text is None:
99
- log.error("<value>: number required")
100
- return float(parms.text) / 100
101
-
102
- return 1.0
103
-
104
-
105
68
  def media_def(
106
69
  filedef: Element, url: str, src: FileInfo, tl: v3, tb: int, ntsc: str
107
70
  ) -> None:
@@ -295,7 +258,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
295
258
  track = ET.SubElement(video, "track")
296
259
 
297
260
  for j, clip in enumerate(tl.v[0]):
298
- assert isinstance(clip, TlVideo)
261
+ assert isinstance(clip, Clip)
299
262
 
300
263
  _start = f"{clip.start}"
301
264
  _end = f"{clip.start + clip.dur}"
@@ -1,13 +1,44 @@
1
- from __future__ import annotations
2
-
3
1
  import sys
4
- from typing import TYPE_CHECKING
5
2
 
6
3
  from auto_editor.json import dump
7
- from auto_editor.timeline import v3
8
-
9
- if TYPE_CHECKING:
10
- from auto_editor.utils.log import Log
4
+ from auto_editor.timeline import Clip, v3
5
+ from auto_editor.utils.log import Log
6
+
7
+
8
+ def as_dict(self: v3) -> dict:
9
+ def aclip_to_dict(self: Clip) -> dict:
10
+ return {
11
+ "name": "audio",
12
+ "src": self.src,
13
+ "start": self.start,
14
+ "dur": self.dur,
15
+ "offset": self.offset,
16
+ "speed": self.speed,
17
+ "volume": self.volume,
18
+ "stream": self.stream,
19
+ }
20
+
21
+ v = []
22
+ a = []
23
+ for vlayer in self.v:
24
+ vb = [vobj.as_dict() for vobj in vlayer]
25
+ if vb:
26
+ v.append(vb)
27
+ for layer in self.a:
28
+ ab = [aclip_to_dict(clip) for clip in layer]
29
+ if ab:
30
+ a.append(ab)
31
+
32
+ return {
33
+ "version": "3",
34
+ "timebase": f"{self.tb.numerator}/{self.tb.denominator}",
35
+ "background": self.background,
36
+ "resolution": self.T.res,
37
+ "samplerate": self.T.sr,
38
+ "layout": self.T.layout,
39
+ "v": v,
40
+ "a": a,
41
+ }
11
42
 
12
43
 
13
44
  def make_json_timeline(ver: str, out: str, tl: v3, log: Log) -> None:
@@ -20,7 +51,7 @@ def make_json_timeline(ver: str, out: str, tl: v3, log: Log) -> None:
20
51
  outfile = open(out, "w")
21
52
 
22
53
  if ver == "v3":
23
- dump(tl.as_dict(), outfile, indent=2)
54
+ dump(as_dict(tl), outfile, indent=2)
24
55
  else:
25
56
  if tl.v1 is None:
26
57
  log.error("Timeline can't be converted to v1 format")
@@ -1,13 +1,8 @@
1
1
  import xml.etree.ElementTree as ET
2
- from typing import TYPE_CHECKING, Any, cast
3
2
 
4
- from auto_editor.timeline import TlAudio, TlVideo, v3
3
+ from auto_editor.timeline import Clip, v3
5
4
  from auto_editor.utils.func import aspect_ratio, to_timecode
6
5
 
7
- if TYPE_CHECKING:
8
- from collections.abc import Sequence
9
-
10
-
11
6
  """
12
7
  Shotcut uses the MLT timeline format
13
8
 
@@ -75,7 +70,7 @@ def shotcut_write_mlt(output: str, tl: v3) -> None:
75
70
  producers = 0
76
71
 
77
72
  if tl.v:
78
- clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
73
+ clips = [clip for clip in tl.v[0] if isinstance(clip, Clip)]
79
74
  elif tl.a:
80
75
  clips = tl.a[0]
81
76
  else:
auto_editor/ffwrapper.py CHANGED
@@ -66,6 +66,7 @@ class FileInfo:
66
66
  path: Path
67
67
  bitrate: int
68
68
  duration: float
69
+ timecode: str # in SMPTE
69
70
  videos: tuple[VideoStream, ...]
70
71
  audios: tuple[AudioStream, ...]
71
72
  subtitles: tuple[SubtitleStream, ...]
@@ -165,12 +166,22 @@ class FileInfo:
165
166
  ext = sub_exts.get(codec, "vtt")
166
167
  subtitles += (SubtitleStream(codec, ext, s.language),)
167
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()
168
179
  bitrate = 0 if cont.bit_rate is None else cont.bit_rate
169
180
  dur = 0 if cont.duration is None else cont.duration / bv.time_base
170
181
 
171
182
  cont.close()
172
183
 
173
- return FileInfo(Path(path), bitrate, dur, videos, audios, subtitles)
184
+ return FileInfo(Path(path), bitrate, dur, timecode, videos, audios, subtitles)
174
185
 
175
186
  def __repr__(self) -> str:
176
187
  return f"@{self.path.name}"