auto-editor 24.24.1__py3-none-any.whl → 24.27.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.
auto_editor/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "24.24.1"
2
- version = "24w24a"
1
+ __version__ = "24.27.1"
2
+ version = "24w27a"
auto_editor/__main__.py CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  import sys
4
+ from os import environ
4
5
 
5
6
  import auto_editor
7
+ from auto_editor.edit import edit_media
8
+ from auto_editor.ffwrapper import FFmpeg
6
9
  from auto_editor.utils.func import setup_tempdir
7
- from auto_editor.utils.log import Log, Timer
10
+ from auto_editor.utils.log import Log
8
11
  from auto_editor.utils.types import (
9
12
  Args,
10
13
  bitrate,
@@ -277,11 +280,16 @@ def main() -> None:
277
280
  f"auto_editor.subcommands.{sys.argv[1]}", fromlist=["subcommands"]
278
281
  )
279
282
  obj.main(sys.argv[2:])
280
- sys.exit()
283
+ return
284
+
285
+ ff_color = "AV_LOG_FORCE_NOCOLOR"
286
+ no_color = bool(environ.get("NO_COLOR")) or bool(environ.get(ff_color))
287
+ log = Log(no_color=no_color)
281
288
 
282
289
  args = main_options(ArgumentParser("Auto-Editor")).parse_args(
283
290
  Args,
284
291
  sys.argv[1:],
292
+ log,
285
293
  macros=[
286
294
  ({"--frame-margin"}, ["--margin"]),
287
295
  ({"--export-to-premiere", "-exp"}, ["--export", "premiere"]),
@@ -296,41 +304,36 @@ def main() -> None:
296
304
 
297
305
  if args.version:
298
306
  print(f"{auto_editor.version} ({auto_editor.__version__})")
299
- sys.exit()
300
-
301
- from auto_editor.edit import edit_media
302
- from auto_editor.ffwrapper import FFmpeg
303
-
304
- log = Log(args.debug, args.quiet)
305
- ffmpeg = FFmpeg(
306
- args.ffmpeg_location,
307
- args.my_ffmpeg,
308
- args.show_ffmpeg_commands,
309
- args.show_ffmpeg_output,
310
- )
307
+ return
311
308
 
312
- if args.debug and args.input == []:
309
+ if args.debug and not args.input:
313
310
  import platform as plat
314
311
 
315
- print(f"Python Version: {plat.python_version()}")
316
- print(f"Platform: {plat.system()} {plat.release()} {plat.machine().lower()}")
317
- print(f"FFmpeg Version: {ffmpeg.version}\nFFmpeg Path: {ffmpeg.path}")
318
- print(f"Auto-Editor Version: {auto_editor.version}")
319
- sys.exit()
312
+ import av
313
+
314
+ print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
315
+ print(f"Python: {plat.python_version()}")
316
+ print(f"PyAV: {av.__version__}")
317
+ print(f"Auto-Editor: {auto_editor.version}")
318
+ return
320
319
 
321
- if args.input == []:
320
+ if not args.input:
322
321
  log.error("You need to give auto-editor an input file.")
323
322
 
324
- temp = setup_tempdir(args.temp_dir, Log())
325
- log = Log(args.debug, args.quiet, temp)
326
- log.machine = args.progress == "machine"
323
+ temp = setup_tempdir(args.temp_dir, log)
324
+ log = Log(args.debug, args.quiet, temp, args.progress == "machine", no_color)
327
325
  log.debug(f"Temp Directory: {temp}")
328
326
 
327
+ ffmpeg = FFmpeg(
328
+ args.ffmpeg_location,
329
+ args.my_ffmpeg,
330
+ args.show_ffmpeg_commands,
331
+ args.show_ffmpeg_output,
332
+ )
329
333
  paths = valid_input(args.input, ffmpeg, args, log)
330
- timer = Timer(args.quiet or log.machine)
331
334
 
332
335
  try:
333
- edit_media(paths, ffmpeg, args, temp, timer, log)
336
+ edit_media(paths, ffmpeg, args, temp, log)
334
337
  except KeyboardInterrupt:
335
338
  log.error("Keyboard Interrupt")
336
339
  log.cleanup()
auto_editor/analyze.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import re
5
5
  from dataclasses import dataclass
6
+ from fractions import Fraction
6
7
  from typing import TYPE_CHECKING
7
8
 
8
9
  import numpy as np
@@ -19,12 +20,12 @@ from auto_editor.lib.contracts import (
19
20
  orc,
20
21
  )
21
22
  from auto_editor.lib.data_structs import Sym
22
- from auto_editor.render.subtitle import SubtitleParser
23
23
  from auto_editor.utils.cmdkw import (
24
24
  Required,
25
25
  pAttr,
26
26
  pAttrs,
27
27
  )
28
+ from auto_editor.utils.subtitle_tools import convert_ass_to_text
28
29
  from auto_editor.wavfile import read
29
30
 
30
31
  if TYPE_CHECKING:
@@ -307,31 +308,65 @@ class Levels:
307
308
  except re.error as e:
308
309
  self.log.error(e)
309
310
 
310
- sub_file = self.ensure.subtitle(self.src, stream)
311
- parser = SubtitleParser(self.tb)
311
+ import av
312
+ from av.subtitles.subtitle import AssSubtitle, TextSubtitle
312
313
 
313
- with open(sub_file, encoding="utf-8") as file:
314
- parser.parse(file.read(), "webvtt")
314
+ try:
315
+ container = av.open(self.src.path, "r")
316
+ subtitle_stream = container.streams.subtitles[stream]
317
+ assert isinstance(subtitle_stream.time_base, Fraction)
318
+ except Exception as e:
319
+ self.log.error(e)
315
320
 
316
- # stackoverflow.com/questions/9662346/python-code-to-remove-html-tags-from-a-string
317
- def cleanhtml(raw_html: str) -> str:
318
- cleanr = re.compile("<.*?>")
319
- return re.sub(cleanr, "", raw_html)
321
+ # Get the length of the subtitle stream.
322
+ sub_length = 0
323
+ for packet in container.demux(subtitle_stream):
324
+ if packet.pts is None or packet.duration is None:
325
+ continue
326
+ for subset in packet.decode():
327
+ # See definition of `AVSubtitle`
328
+ # in: https://ffmpeg.org/doxygen/trunk/avcodec_8h_source.html
329
+ start = float(packet.pts * subtitle_stream.time_base)
330
+ dur = float(packet.duration * subtitle_stream.time_base)
320
331
 
321
- if not parser.contents:
322
- self.log.error("subtitle has no valid entries")
332
+ end = round((start + dur) * self.tb)
333
+ sub_length = max(sub_length, end)
323
334
 
324
- result = np.zeros((parser.contents[-1].end), dtype=np.bool_)
335
+ result = np.zeros((sub_length), dtype=np.bool_)
336
+ del sub_length
325
337
 
326
338
  count = 0
327
- for content in parser.contents:
328
- if max_count is not None and count >= max_count:
339
+ early_exit = False
340
+ container.seek(0)
341
+ for packet in container.demux(subtitle_stream):
342
+ if packet.pts is None or packet.duration is None:
343
+ continue
344
+ if early_exit:
329
345
  break
330
-
331
- line = cleanhtml(content.after.strip())
332
- if line and re.search(pattern, line):
333
- result[content.start : content.end] = 1
334
- count += 1
346
+ for subset in packet.decode():
347
+ if max_count is not None and count >= max_count:
348
+ early_exit = True
349
+ break
350
+
351
+ start = float(packet.pts * subtitle_stream.time_base)
352
+ dur = float(packet.duration * subtitle_stream.time_base)
353
+
354
+ san_start = round(start * self.tb)
355
+ san_end = round((start + dur) * self.tb)
356
+
357
+ for sub in subset:
358
+ if isinstance(sub, AssSubtitle):
359
+ line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
360
+ elif isinstance(sub, TextSubtitle):
361
+ line = sub.text.decode(errors="ignore")
362
+ else:
363
+ continue
364
+
365
+ if line and re.search(pattern, line):
366
+ result[san_start:san_end] = 1
367
+ count += 1
368
+
369
+ container.close()
335
370
 
336
371
  return result
337
372
 
auto_editor/edit.py CHANGED
@@ -15,7 +15,7 @@ from auto_editor.utils.bar import Bar
15
15
  from auto_editor.utils.chunks import Chunk, Chunks
16
16
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
17
17
  from auto_editor.utils.container import Container, container_constructor
18
- from auto_editor.utils.log import Log, Timer
18
+ from auto_editor.utils.log import Log
19
19
  from auto_editor.utils.types import Args
20
20
 
21
21
 
@@ -150,7 +150,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
150
150
 
151
151
 
152
152
  def edit_media(
153
- paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str, timer: Timer, log: Log
153
+ paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str, log: Log
154
154
  ) -> None:
155
155
  bar = Bar(args.progress)
156
156
  tl = None
@@ -160,7 +160,7 @@ def edit_media(
160
160
  if path_ext == ".xml":
161
161
  from auto_editor.formats.fcp7 import fcp7_read_xml
162
162
 
163
- tl = fcp7_read_xml(paths[0], ffmpeg, log)
163
+ tl = fcp7_read_xml(paths[0], log)
164
164
  assert tl.src is not None
165
165
  sources: list[FileInfo] = [tl.src]
166
166
  src: FileInfo | None = tl.src
@@ -168,7 +168,7 @@ def edit_media(
168
168
  elif path_ext == ".mlt":
169
169
  from auto_editor.formats.shotcut import shotcut_read_mlt
170
170
 
171
- tl = shotcut_read_mlt(paths[0], ffmpeg, log)
171
+ tl = shotcut_read_mlt(paths[0], log)
172
172
  assert tl.src is not None
173
173
  sources = [tl.src]
174
174
  src = tl.src
@@ -190,7 +190,6 @@ def edit_media(
190
190
 
191
191
  if export["export"] == "timeline":
192
192
  log.quiet = True
193
- timer.quiet = True
194
193
 
195
194
  if not args.preview:
196
195
  log.conwrite("Starting")
@@ -210,18 +209,9 @@ def edit_media(
210
209
  else:
211
210
  samplerate = args.sample_rate
212
211
 
213
- ensure = Ensure(ffmpeg, samplerate, temp, log)
212
+ ensure = Ensure(ffmpeg, bar, samplerate, temp, log)
214
213
 
215
214
  if tl is None:
216
- # Extract subtitles in their native format.
217
- if src is not None and len(src.subtitles) > 0 and not args.sn:
218
- cmd = ["-i", f"{src.path}", "-hide_banner"]
219
- for s, sub in enumerate(src.subtitles):
220
- cmd.extend(["-map", f"0:s:{s}"])
221
- for s, sub in enumerate(src.subtitles):
222
- cmd.extend([os.path.join(temp, f"{s}s.{sub.ext}")])
223
- ffmpeg.run(cmd)
224
-
225
215
  tl = make_timeline(sources, ensure, args, samplerate, bar, temp, log)
226
216
 
227
217
  if export["export"] == "timeline":
@@ -283,7 +273,7 @@ def edit_media(
283
273
  apply_later = False
284
274
 
285
275
  if ctr.allow_subtitle and not args.sn:
286
- sub_output = make_new_subtitles(tl, ffmpeg, temp, log)
276
+ sub_output = make_new_subtitles(tl, ensure, temp)
287
277
 
288
278
  if ctr.allow_audio:
289
279
  audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, temp, log)
@@ -357,7 +347,7 @@ def edit_media(
357
347
  else:
358
348
  make_media(tl, output)
359
349
 
360
- timer.stop()
350
+ log.stop_timer()
361
351
 
362
352
  if not args.no_open and export["export"] in ("default", "audio", "clip-sequence"):
363
353
  if args.player is None:
auto_editor/ffwrapper.py CHANGED
@@ -11,6 +11,8 @@ from shutil import which
11
11
  from subprocess import PIPE, Popen
12
12
  from typing import Any
13
13
 
14
+ import av
15
+
14
16
  from auto_editor.utils.func import get_stdout
15
17
  from auto_editor.utils.log import Log
16
18
 
@@ -190,10 +192,12 @@ class FileInfo:
190
192
 
191
193
 
192
194
  def initFileInfo(path: str, log: Log) -> FileInfo:
193
- import av
194
-
195
195
  try:
196
196
  cont = av.open(path, "r")
197
+ except av.error.FileNotFoundError:
198
+ log.error(f"Could not find '{path}'")
199
+ except av.error.IsADirectoryError:
200
+ log.error(f"Expected a media file, but got a directory: {path}")
197
201
  except av.error.InvalidDataError:
198
202
  log.error(f"Invalid data when processing: {path}")
199
203
 
@@ -7,7 +7,7 @@ from math import ceil
7
7
  from typing import TYPE_CHECKING
8
8
  from xml.etree.ElementTree import Element
9
9
 
10
- from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
10
+ from auto_editor.ffwrapper import FileInfo, initFileInfo
11
11
  from auto_editor.timeline import ASpace, TlAudio, TlVideo, VSpace, v3
12
12
 
13
13
  from .utils import Validator, show
@@ -177,7 +177,7 @@ def read_filters(clipitem: Element, log: Log) -> float:
177
177
  return 1.0
178
178
 
179
179
 
180
- def fcp7_read_xml(path: str, ffmpeg: FFmpeg, log: Log) -> v3:
180
+ def fcp7_read_xml(path: str, log: Log) -> v3:
181
181
  def xml_bool(val: str) -> bool:
182
182
  if val == "TRUE":
183
183
  return True
@@ -188,7 +188,7 @@ def fcp7_read_xml(path: str, ffmpeg: FFmpeg, log: Log) -> v3:
188
188
  try:
189
189
  tree = ET.parse(path)
190
190
  except FileNotFoundError:
191
- log.nofile(path)
191
+ log.error(f"Could not find '{path}'")
192
192
 
193
193
  root = tree.getroot()
194
194
 
@@ -9,7 +9,6 @@ from auto_editor.utils.func import aspect_ratio, to_timecode
9
9
  if TYPE_CHECKING:
10
10
  from collections.abc import Sequence
11
11
 
12
- from auto_editor.ffwrapper import FFmpeg
13
12
  from auto_editor.timeline import TlAudio, TlVideo
14
13
  from auto_editor.utils.log import Log
15
14
 
@@ -22,7 +21,7 @@ https://mltframework.org/docs/mltxml/
22
21
  """
23
22
 
24
23
 
25
- def shotcut_read_mlt(path: str, ffmpeg: FFmpeg, log: Log) -> v3:
24
+ def shotcut_read_mlt(path: str, log: Log) -> v3:
26
25
  raise NotImplementedError
27
26
 
28
27
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from fractions import Fraction
4
+ from math import ceil
4
5
  from typing import TYPE_CHECKING, NamedTuple
5
6
 
6
7
  import numpy as np
@@ -37,7 +38,7 @@ def clipify(chunks: Chunks, src: FileInfo, start: int = 0) -> list[Clip]:
37
38
  clips: list[Clip] = []
38
39
  i = 0
39
40
  for chunk in chunks:
40
- if chunk[2] != 99999:
41
+ if chunk[2] > 0 and chunk[2] < 99999.0:
41
42
  dur = round((chunk[1] - chunk[0]) / chunk[2])
42
43
  if dur == 0:
43
44
  continue
@@ -112,8 +113,13 @@ def run_interpreter_for_edit_option(
112
113
 
113
114
  def make_sane_timebase(fps: Fraction) -> Fraction:
114
115
  tb = round(fps, 2)
116
+
117
+ ntsc_60 = Fraction(60_000, 1001)
115
118
  ntsc = Fraction(30_000, 1001)
116
119
  film_ntsc = Fraction(24_000, 1001)
120
+
121
+ if tb == round(ntsc_60, 2):
122
+ return ntsc_60
117
123
  if tb == round(ntsc, 2):
118
124
  return ntsc
119
125
  if tb == round(film_ntsc, 2):
@@ -226,21 +232,35 @@ def make_timeline(
226
232
  chunks.append((src, start, arr_length, speed_map[arr[j]]))
227
233
  return chunks
228
234
 
235
+ # Assert timeline is monotonic because non-monotonic timelines are incorrect
236
+ # here and causes back-seeking (performance issue) in video rendering.
237
+
238
+ # We don't properly check monotonicity for multiple sources, so skip those.
239
+
240
+ check_monotonic = len(sources) == 1
241
+ last_i = 0
242
+
229
243
  clips: list[Clip] = []
230
- i = 0
231
244
  start = 0
245
+
232
246
  for chunk in echunk(speed_index, src_index):
233
247
  if chunk[3] != 99999:
234
- dur = round((chunk[2] - chunk[1]) / chunk[3])
248
+ dur = int((chunk[2] - chunk[1]) / chunk[3])
235
249
  if dur == 0:
236
250
  continue
237
251
 
238
- offset = int(chunk[1] / chunk[3])
252
+ offset = ceil(chunk[1] / chunk[3])
253
+
254
+ if check_monotonic:
255
+ max_end = start + dur - 1
256
+ this_i = round((offset + max_end - start) * chunk[3])
257
+ if this_i < last_i:
258
+ raise ValueError("not monotonic", sources, this_i, last_i)
259
+ last_i = this_i
260
+
261
+ clips.append(Clip(start, dur, offset, chunk[3], chunk[0]))
239
262
 
240
- if not (clips and clips[-1].start == round(start)):
241
- clips.append(Clip(start, dur, offset, chunk[3], chunk[0]))
242
263
  start += dur
243
- i += 1
244
264
 
245
265
  vtl: VSpace = []
246
266
  atl: ASpace = []
@@ -276,4 +296,22 @@ def make_timeline(
276
296
  else:
277
297
  v1_compatiable = None
278
298
 
279
- return v3(inp, tb, sr, res, args.background, vtl, atl, v1_compatiable)
299
+ tl = v3(inp, tb, sr, res, args.background, vtl, atl, v1_compatiable)
300
+
301
+ # Additional monotonic check, o(n^2) time complexity so disable by default.
302
+
303
+ # if len(sources) != 1:
304
+ # return tl
305
+
306
+ # last_i = 0
307
+ # for index in range(tl.end):
308
+ # for layer in tl.v:
309
+ # for lobj in layer:
310
+ # if index >= lobj.start and index < (lobj.start + lobj.dur):
311
+ # _i = round((lobj.offset + index - lobj.start) * lobj.speed)
312
+ # if (_i < last_i):
313
+ # print(_i, last_i)
314
+ # raise ValueError("not monotonic")
315
+ # last_i = _i
316
+
317
+ return tl
auto_editor/output.py CHANGED
@@ -4,7 +4,11 @@ import os.path
4
4
  from dataclasses import dataclass, field
5
5
  from fractions import Fraction
6
6
 
7
+ import av
8
+ from av.audio.resampler import AudioResampler
9
+
7
10
  from auto_editor.ffwrapper import FFmpeg, FileInfo
11
+ from auto_editor.utils.bar import Bar
8
12
  from auto_editor.utils.container import Container
9
13
  from auto_editor.utils.log import Log
10
14
  from auto_editor.utils.types import Args
@@ -13,44 +17,76 @@ from auto_editor.utils.types import Args
13
17
  @dataclass(slots=True)
14
18
  class Ensure:
15
19
  _ffmpeg: FFmpeg
20
+ _bar: Bar
16
21
  _sr: int
17
22
  temp: str
18
23
  log: Log
19
- labels: list[tuple[FileInfo, int]] = field(default_factory=list)
20
- sub_labels: list[tuple[FileInfo, int]] = field(default_factory=list)
24
+ _audios: list[tuple[FileInfo, int]] = field(default_factory=list)
25
+ _subtitles: list[tuple[FileInfo, int, str]] = field(default_factory=list)
21
26
 
22
27
  def audio(self, src: FileInfo, stream: int) -> str:
23
28
  try:
24
- label = self.labels.index((src, stream))
29
+ label = self._audios.index((src, stream))
25
30
  first_time = False
26
31
  except ValueError:
27
- self.labels.append((src, stream))
28
- label = len(self.labels) - 1
32
+ self._audios.append((src, stream))
33
+ label = len(self._audios) - 1
29
34
  first_time = True
30
35
 
31
36
  out_path = os.path.join(self.temp, f"{label:x}.wav")
32
37
 
33
38
  if first_time:
34
- self.log.conwrite("Extracting audio")
39
+ sample_rate = self._sr
40
+ bar = self._bar
41
+ self.log.debug(f"Making external audio: {out_path}")
42
+
43
+ in_container = av.open(src.path, "r")
44
+ out_container = av.open(
45
+ out_path, "w", format="wav", options={"rf64": "always"}
46
+ )
47
+ astream = in_container.streams.audio[stream]
48
+
49
+ if astream.duration is None or astream.time_base is None:
50
+ dur = 1
51
+ else:
52
+ dur = int(astream.duration * astream.time_base)
53
+
54
+ bar.start(dur, "Extracting audio")
55
+
56
+ # PyAV always uses "stereo" layout, which is what we want.
57
+ output_astream = out_container.add_stream("pcm_s16le", rate=sample_rate)
58
+ assert isinstance(output_astream, av.audio.stream.AudioStream)
59
+
60
+ resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate) # type: ignore
61
+ for i, frame in enumerate(in_container.decode(astream)):
62
+ if i % 1500 == 0:
63
+ bar.tick(0 if frame.time is None else frame.time)
64
+
65
+ for new_frame in resampler.resample(frame):
66
+ for packet in output_astream.encode(new_frame):
67
+ out_container.mux_one(packet)
68
+
69
+ for packet in output_astream.encode():
70
+ out_container.mux_one(packet)
35
71
 
36
- cmd = ["-i", f"{src.path}", "-map", f"0:a:{stream}"]
37
- cmd += ["-ac", "2", "-ar", f"{self._sr}", "-rf64", "always", out_path]
38
- self._ffmpeg.run(cmd)
72
+ out_container.close()
73
+ in_container.close()
74
+ bar.end()
39
75
 
40
76
  return out_path
41
77
 
42
- def subtitle(self, src: FileInfo, stream: int) -> str:
78
+ def subtitle(self, src: FileInfo, stream: int, ext: str) -> str:
43
79
  try:
44
- label = self.sub_labels.index((src, stream))
80
+ self._subtitles.index((src, stream, ext))
45
81
  first_time = False
46
82
  except ValueError:
47
- self.sub_labels.append((src, stream))
48
- label = len(self.sub_labels) - 1
83
+ self._subtitles.append((src, stream, ext))
49
84
  first_time = True
50
85
 
51
- out_path = os.path.join(self.temp, f"{label:x}.vtt")
86
+ out_path = os.path.join(self.temp, f"{stream}s.{ext}")
52
87
 
53
88
  if first_time:
89
+ self.log.debug(f"Making external subtitle: {out_path}")
54
90
  self.log.conwrite("Extracting subtitle")
55
91
  self._ffmpeg.run(["-i", f"{src.path}", "-map", f"0:s:{stream}", out_path])
56
92
 
@@ -10,10 +10,9 @@ from auto_editor.utils.func import to_timecode
10
10
  if TYPE_CHECKING:
11
11
  from fractions import Fraction
12
12
 
13
- from auto_editor.ffwrapper import FFmpeg
13
+ from auto_editor.output import Ensure
14
14
  from auto_editor.timeline import v3
15
15
  from auto_editor.utils.chunks import Chunks
16
- from auto_editor.utils.log import Log
17
16
 
18
17
 
19
18
  @dataclass(slots=True)
@@ -122,26 +121,21 @@ class SubtitleParser:
122
121
  file.write(self.footer)
123
122
 
124
123
 
125
- def make_new_subtitles(tl: v3, ffmpeg: FFmpeg, temp: str, log: Log) -> list[str]:
124
+ def make_new_subtitles(tl: v3, ensure: Ensure, temp: str) -> list[str]:
126
125
  if tl.v1 is None:
127
126
  return []
128
127
 
129
128
  new_paths = []
130
129
 
131
130
  for s, sub in enumerate(tl.v1.source.subtitles):
132
- file_path = os.path.join(temp, f"{s}s.{sub.ext}")
133
131
  new_path = os.path.join(temp, f"new{s}s.{sub.ext}")
134
-
135
132
  parser = SubtitleParser(tl.tb)
136
133
 
137
- if sub.codec in parser.supported_codecs:
138
- with open(file_path, encoding="utf-8") as file:
139
- parser.parse(file.read(), sub.codec)
140
- else:
141
- convert_path = os.path.join(temp, f"{s}s_convert.vtt")
142
- ffmpeg.run(["-i", file_path, convert_path])
143
- with open(convert_path, encoding="utf-8") as file:
144
- parser.parse(file.read(), "webvtt")
134
+ ext = sub.ext if sub.codec in parser.supported_codecs else "vtt"
135
+ file_path = ensure.subtitle(tl.v1.source, s, ext)
136
+
137
+ with open(file_path, encoding="utf-8") as file:
138
+ parser.parse(file.read(), sub.codec)
145
139
 
146
140
  parser.edit(tl.v1.chunks)
147
141
  parser.write(new_path)
@@ -103,8 +103,8 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
103
103
  graph.add("scale", f"{obj.width}:-1"),
104
104
  graph.add("buffersink"),
105
105
  )
106
- graph.push(frame)
107
- frame = graph.pull()
106
+ graph.vpush(frame)
107
+ frame = graph.vpull()
108
108
  img_cache[(obj.src, obj.width)] = frame.to_ndarray(
109
109
  format="rgb24"
110
110
  )
@@ -122,7 +122,7 @@ def render_av(
122
122
  log: Log,
123
123
  ) -> tuple[str, bool]:
124
124
  src = tl.src
125
- cns: dict[FileInfo, av.InputContainer] = {}
125
+ cns: dict[FileInfo, av.container.InputContainer] = {}
126
126
  decoders: dict[FileInfo, Iterator[av.VideoFrame]] = {}
127
127
  seek_cost: dict[FileInfo, int] = {}
128
128
  tous: dict[FileInfo, int] = {}
@@ -302,8 +302,8 @@ def render_av(
302
302
  graph.add("pad", f"{width}:{height}:-1:-1:color={bg}"),
303
303
  graph.add("buffersink"),
304
304
  )
305
- graph.push(frame)
306
- frame = graph.pull()
305
+ graph.vpush(frame)
306
+ frame = graph.vpull()
307
307
  elif isinstance(obj, TlRect):
308
308
  graph = av.filter.Graph()
309
309
  x, y = apply_anchor(obj.x, obj.y, obj.width, obj.height, obj.anchor)
@@ -315,8 +315,8 @@ def render_av(
315
315
  ),
316
316
  graph.add("buffersink"),
317
317
  )
318
- graph.push(frame)
319
- frame = graph.pull()
318
+ graph.vpush(frame)
319
+ frame = graph.vpull()
320
320
  elif isinstance(obj, TlImage):
321
321
  img = img_cache[(obj.src, obj.width)]
322
322
  array = frame.to_ndarray(format="rgb24")
@@ -355,8 +355,8 @@ def render_av(
355
355
  frame = av.VideoFrame.from_ndarray(array, format="rgb24")
356
356
 
357
357
  if scale_graph is not None and frame.width != target_width:
358
- scale_graph.push(frame)
359
- frame = scale_graph.pull()
358
+ scale_graph.vpush(frame)
359
+ frame = scale_graph.vpull()
360
360
 
361
361
  if frame.format.name != target_pix_fmt:
362
362
  frame = frame.reformat(format=target_pix_fmt)
@@ -83,7 +83,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
83
83
 
84
84
  for file in args.input:
85
85
  if not os.path.isfile(file):
86
- log.nofile(file)
86
+ log.error(f"Could not find '{file}'")
87
87
 
88
88
  ext = os.path.splitext(file)[1]
89
89
  if ext == ".json":
@@ -85,7 +85,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
85
85
  src = sources[0]
86
86
 
87
87
  tb = src.get_fps() if args.timebase is None else args.timebase
88
- ensure = Ensure(ffmpeg, src.get_sr(), temp, log)
88
+ ensure = Ensure(ffmpeg, bar, src.get_sr(), temp, log)
89
89
 
90
90
  if ":" in args.edit:
91
91
  method, attrs = args.edit.split(":", 1)
@@ -73,8 +73,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
73
73
  sources = [initFileInfo(path, log) for path in args.input]
74
74
  src = sources[0]
75
75
  tb = src.get_fps() if args.timebase is None else args.timebase
76
- ensure = Ensure(ffmpeg, src.get_sr(), temp, log)
77
76
  bar = Bar("none")
77
+ ensure = Ensure(ffmpeg, bar, src.get_sr(), temp, log)
78
78
  env["timebase"] = tb
79
79
  env["@levels"] = Levels(ensure, src, tb, bar, temp, log)
80
80
  env["@filesetup"] = FileSetup(src, ensure, strict, tb, bar, temp, log)
@@ -340,11 +340,19 @@ def main(sys_args: list[str] | None = None):
340
340
  def track_tests():
341
341
  return run.main(["resources/multi-track.mov"], ["--keep_tracks_seperate"])
342
342
 
343
- def json_tests():
343
+ def export_json_tests():
344
344
  out = run.main(["example.mp4"], ["--export_as_json"])
345
345
  out2 = run.main([out], [])
346
346
  return out, out2
347
347
 
348
+ def import_v1_tests():
349
+ with open("v1.json", "w") as file:
350
+ file.write(
351
+ """{"version": "1", "source": "example.mp4", "chunks": [ [0, 26, 1.0], [26, 34, 0] ]}"""
352
+ )
353
+
354
+ return run.main(["v1.json"], [])
355
+
348
356
  def premiere_named_export():
349
357
  run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
350
358
 
@@ -524,9 +532,7 @@ def main(sys_args: list[str] | None = None):
524
532
  # Issue 280
525
533
  def SAR():
526
534
  out = run.main(["resources/SAR-2by3.mp4"], [])
527
-
528
- # It's working, PyAV just can't detect the changes.
529
- # assert checker.check(out).videos[0].sar == Fraction(2, 3)
535
+ assert checker.check(out).videos[0].sar == Fraction(2, 3)
530
536
 
531
537
  return out
532
538
 
@@ -711,7 +717,8 @@ def main(sys_args: list[str] | None = None):
711
717
  edit_positive_tests,
712
718
  audio_norm_f,
713
719
  audio_norm_ebu,
714
- json_tests,
720
+ export_json_tests,
721
+ import_v1_tests,
715
722
  high_speed_test,
716
723
  video_speed,
717
724
  multi_track_edit,
auto_editor/utils/log.py CHANGED
@@ -2,37 +2,28 @@ from __future__ import annotations
2
2
 
3
3
  import sys
4
4
  from datetime import timedelta
5
- from pathlib import Path
6
5
  from shutil import get_terminal_size, rmtree
7
6
  from time import perf_counter, sleep
8
7
  from typing import NoReturn
9
8
 
10
9
 
11
- class Timer:
12
- __slots__ = ("start_time", "quiet")
13
-
14
- def __init__(self, quiet: bool = False):
15
- self.start_time = perf_counter()
16
- self.quiet = quiet
17
-
18
- def stop(self) -> None:
19
- if not self.quiet:
20
- second_len = round(perf_counter() - self.start_time, 2)
21
- minute_len = timedelta(seconds=round(second_len))
22
-
23
- sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
24
-
25
-
26
10
  class Log:
27
- __slots__ = ("is_debug", "quiet", "temp", "machine")
11
+ __slots__ = ("is_debug", "quiet", "temp", "machine", "start_time", "no_color")
28
12
 
29
13
  def __init__(
30
- self, show_debug: bool = False, quiet: bool = False, temp: str | None = None
14
+ self,
15
+ is_debug: bool = False,
16
+ quiet: bool = False,
17
+ temp: str | None = None,
18
+ machine: bool = False,
19
+ no_color: bool = True,
31
20
  ):
32
- self.is_debug = show_debug
21
+ self.is_debug = is_debug
33
22
  self.quiet = quiet
34
23
  self.temp = temp
35
- self.machine = False
24
+ self.machine = machine
25
+ self.no_color = no_color
26
+ self.start_time = 0 if self.quiet or self.machine else perf_counter()
36
27
 
37
28
  def debug(self, message: object) -> None:
38
29
  if self.is_debug:
@@ -62,13 +53,34 @@ class Log:
62
53
  buffer = " " * (get_terminal_size().columns - len(message) - 3)
63
54
  sys.stdout.write(f" {message}{buffer}\r")
64
55
 
56
+ def print(self, message: str) -> None:
57
+ if not self.quiet:
58
+ self.conwrite("")
59
+ sys.stdout.write(f"{message}\n")
60
+
61
+ def warning(self, message: str) -> None:
62
+ if not self.quiet:
63
+ self.conwrite("")
64
+ sys.stderr.write(f"Warning! {message}\n")
65
+
66
+ def stop_timer(self) -> None:
67
+ if not self.quiet and not self.machine:
68
+ second_len = round(perf_counter() - self.start_time, 2)
69
+ minute_len = timedelta(seconds=round(second_len))
70
+
71
+ sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
72
+
65
73
  def error(self, message: str | Exception) -> NoReturn:
66
74
  if self.is_debug and isinstance(message, Exception):
67
75
  self.cleanup()
68
76
  raise message
69
77
 
70
78
  self.conwrite("")
71
- sys.stderr.write(f"Error! {message}\n")
79
+ if self.no_color:
80
+ sys.stderr.write(f"Error! {message}\n")
81
+ else:
82
+ sys.stderr.write(f"\033[31;40mError! {message}\033[0m\n")
83
+
72
84
  self.cleanup()
73
85
  from platform import system
74
86
 
@@ -81,16 +93,3 @@ class Log:
81
93
  import os
82
94
 
83
95
  os._exit(1)
84
-
85
- def nofile(self, path: str | Path) -> NoReturn:
86
- self.error(f"Could not find '{path}'")
87
-
88
- def warning(self, message: str) -> None:
89
- if not self.quiet:
90
- self.conwrite("")
91
- sys.stderr.write(f"Warning! {message}\n")
92
-
93
- def print(self, message: str) -> None:
94
- if not self.quiet:
95
- self.conwrite("")
96
- sys.stdout.write(f"{message}\n")
@@ -0,0 +1,29 @@
1
+ def convert_ass_to_text(ass_text: str) -> str:
2
+ result = ""
3
+ comma_count = i = 0
4
+
5
+ while comma_count < 8 and i < len(ass_text):
6
+ if ass_text[i] == ",":
7
+ comma_count += 1
8
+ i += 1
9
+
10
+ state = False
11
+ while i < len(ass_text):
12
+ char = ass_text[i]
13
+ next_char = "" if i + 1 >= len(ass_text) else ass_text[i + 1]
14
+
15
+ if char == "\\" and next_char == "N":
16
+ result += "\n"
17
+ i += 2
18
+ continue
19
+
20
+ if not state:
21
+ if char == "{":
22
+ state = True
23
+ else:
24
+ result += ass_text[i]
25
+ elif char == "}":
26
+ state = False
27
+ i += 1
28
+
29
+ return result
@@ -67,20 +67,17 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
67
67
 
68
68
 
69
69
  def valid_input(inputs: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> list[str]:
70
- new_inputs = []
70
+ result = []
71
71
 
72
72
  for my_input in inputs:
73
- if os.path.isfile(my_input):
73
+ if my_input.startswith("http://") or my_input.startswith("https://"):
74
+ result.append(download_video(my_input, args, ffmpeg, log))
75
+ else:
74
76
  _, ext = os.path.splitext(my_input)
75
77
  if ext == "":
76
- log.error("File must have an extension.")
77
- new_inputs.append(my_input)
78
-
79
- elif my_input.startswith("http://") or my_input.startswith("https://"):
80
- new_inputs.append(download_video(my_input, args, ffmpeg, log))
81
- else:
82
- if os.path.isdir(my_input):
83
- log.error("Input must be a file or a URL, not a directory.")
84
- log.nofile(my_input)
78
+ if os.path.isdir(my_input):
79
+ log.error("Input must be a file or a URL, not a directory.")
80
+ log.error("Input file must have an extension.")
81
+ result.append(my_input)
85
82
 
86
- return new_inputs
83
+ return result
auto_editor/vanparse.py CHANGED
@@ -119,7 +119,7 @@ def to_key(op: Options | Required) -> str:
119
119
  return op.names[0][:2].replace("-", "") + op.names[0][2:].replace("-", "_")
120
120
 
121
121
 
122
- def print_option_help(program_name: str | None, ns_obj: T, option: Options) -> None:
122
+ def print_option_help(name: str | None, ns_obj: object, option: Options) -> None:
123
123
  text = StringIO()
124
124
  text.write(
125
125
  f" {', '.join(option.names)} {'' if option.metavar is None else option.metavar}\n\n"
@@ -145,8 +145,8 @@ def print_option_help(program_name: str | None, ns_obj: T, option: Options) -> N
145
145
 
146
146
  from auto_editor.help import data
147
147
 
148
- if program_name is not None and option.names[0] in data[program_name]:
149
- text.write(indent(data[program_name][option.names[0]], " ") + "\n")
148
+ if name is not None and option.names[0] in data[name]:
149
+ text.write(indent(data[name][option.names[0]], " ") + "\n")
150
150
  else:
151
151
  text.write(f" {option.help}\n\n")
152
152
 
@@ -160,25 +160,6 @@ def get_option(name: str, options: list[Options]) -> Options | None:
160
160
  return None
161
161
 
162
162
 
163
- def parse_value(option: Options | Required, val: str | None) -> Any:
164
- if val is None and option.nargs == 1:
165
- Log().error(f"{option.names[0]} needs argument.")
166
-
167
- try:
168
- value = option.type(val)
169
- except CoerceError as e:
170
- Log().error(e)
171
-
172
- if option.choices is not None and value not in option.choices:
173
- choices = ", ".join(option.choices)
174
-
175
- Log().error(
176
- f"{value} is not a choice for {option.names[0]}\nchoices are:\n {choices}"
177
- )
178
-
179
- return value
180
-
181
-
182
163
  class ArgumentParser:
183
164
  def __init__(self, program_name: str | None):
184
165
  self.program_name = program_name
@@ -201,6 +182,7 @@ class ArgumentParser:
201
182
  self,
202
183
  ns_obj: type[T],
203
184
  sys_args: list[str],
185
+ log_: Log | None = None,
204
186
  macros: list[tuple[set[str], list[str]]] | None = None,
205
187
  ) -> T:
206
188
  if not sys_args and self.program_name is not None:
@@ -219,9 +201,26 @@ class ArgumentParser:
219
201
  del _macros
220
202
  del macros
221
203
 
204
+ log = Log() if log_ is None else log_
222
205
  ns = ns_obj()
223
206
  option_names: list[str] = []
224
207
 
208
+ def parse_value(option: Options | Required, val: str | None) -> Any:
209
+ if val is None and option.nargs == 1:
210
+ log.error(f"{option.names[0]} needs argument.")
211
+
212
+ try:
213
+ value = option.type(val)
214
+ except CoerceError as e:
215
+ log.error(e)
216
+
217
+ if option.choices is not None and value not in option.choices:
218
+ log.error(
219
+ f"{value} is not a choice for {option.names[0]}\n"
220
+ f"choices are:\n {', '.join(option.choices)}"
221
+ )
222
+ return value
223
+
225
224
  program_name = self.program_name
226
225
  requireds = self.requireds
227
226
  options = self.options
@@ -256,7 +255,7 @@ class ArgumentParser:
256
255
  val = oplist_coerce(arg)
257
256
  ns.__setattr__(oplist_name, getattr(ns, oplist_name) + [val])
258
257
  except (CoerceError, ValueError) as e:
259
- Log().error(e)
258
+ log.error(e)
260
259
  elif requireds and not arg.startswith("--"):
261
260
  if requireds[0].nargs == 1:
262
261
  ns.__setattr__(req_list_name, parse_value(requireds[0], arg))
@@ -268,19 +267,19 @@ class ArgumentParser:
268
267
 
269
268
  # 'Did you mean' message might appear that options need a comma.
270
269
  if arg.replace(",", "") in option_names:
271
- Log().error(f"Option '{arg}' has an unnecessary comma.")
270
+ log.error(f"Option '{arg}' has an unnecessary comma.")
272
271
 
273
272
  close_matches = difflib.get_close_matches(arg, option_names)
274
273
  if close_matches:
275
- Log().error(
274
+ log.error(
276
275
  f"Unknown {label}: {arg}\n\n Did you mean:\n "
277
276
  + ", ".join(close_matches)
278
277
  )
279
- Log().error(f"Unknown {label}: {arg}")
278
+ log.error(f"Unknown {label}: {arg}")
280
279
  else:
281
280
  if option.nargs != "*":
282
281
  if option in used_options:
283
- Log().error(
282
+ log.error(
284
283
  f"Option {option.names[0]} may not be used more than once."
285
284
  )
286
285
  used_options.append(option)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 24.24.1
3
+ Version: 24.27.1
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
6
  License: Unlicense
@@ -12,7 +12,7 @@ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: numpy >=1.22.0
15
- Requires-Dist: pyav ==12.1.0
15
+ Requires-Dist: pyav ==12.2.*
16
16
  Requires-Dist: ae-ffmpeg ==1.2.*
17
17
 
18
18
  <p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
@@ -105,16 +105,14 @@ Auto-Editor can also export to:
105
105
  - Individual media clips with `--export clip-sequence`
106
106
 
107
107
  ### Naming Timelines
108
- By default, auto-editor will name the timeline to "Auto-Editor Media Group" if the export supports naming.
108
+ Some editors support naming timelines. By default, auto-editor will use the name "Auto-Editor Media Group". For `premiere` `resolve` and `final-cut-pro` export options, you can change the name with the following syntax.
109
109
 
110
110
  ```
111
+ # for POSIX shells
111
112
  auto-editor example.mp4 --export 'premiere:name="Your name here"'
112
113
 
113
- auto-editor example.mp4 --export 'resolve:name="Your name here"'
114
-
115
- auto-editor example.mp4 --export 'final-cut-pro:name="Your name here"'
116
-
117
- # No other export options support naming
114
+ # for Powershell
115
+ auto-editor example.mp4 --export 'premiere:name=""Your name here""'
118
116
  ```
119
117
 
120
118
  ### Split by Clip
@@ -1,24 +1,24 @@
1
1
  ae-ffmpeg/setup.py,sha256=HeORyrs8OyJ32lSnMaIhI2B7U1lkk3QP6wOjxpoiF3Y,1891
2
2
  ae-ffmpeg/ae_ffmpeg/__init__.py,sha256=Fd2YsCINa0dB3tf9VVKDTPT9P6MDH-ve3RT2pqArImI,453
3
3
  ae-ffmpeg/ae_ffmpeg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- auto_editor/__init__.py,sha256=2fihE0IJI1TOaKjbK47zmlNVGPFXtfaph4E4BM9SOh0,43
5
- auto_editor/__main__.py,sha256=YgupapPwIuOo93Csgn7a674v8g_shDthvrZbXdd8WkE,9852
6
- auto_editor/analyze.py,sha256=kKXXm_EffOrxFpYNa-fn1m5J012SOFaJ8SHBimvqtZ0,11805
7
- auto_editor/edit.py,sha256=KwzNMQRDCD_9ckgLE9qM5MxS44S1NyBB0aKSpaXf0jM,11973
8
- auto_editor/ffwrapper.py,sha256=tZv12az2HLGjVjLfUukhjiuC75D_Ga6JJJppCrwJmMc,7618
4
+ auto_editor/__init__.py,sha256=x3FkUVgQK07IAJYRgjIfWDxsjSINz0ig_JBv3pG5gas,43
5
+ auto_editor/__main__.py,sha256=nkb8N_bxF_qld53LWo4c6Y0n9NDRdsPfANpVF1RD1cQ,9863
6
+ auto_editor/analyze.py,sha256=NVDARF7ZxNLWMJ89HsuqCS1TMSeYhf2e5C66cQUTKrk,13200
7
+ auto_editor/edit.py,sha256=EEB9PJdGpXh5b1vDdSnsEtI8aJEcF64-SNZjFRg6Nrs,11484
8
+ auto_editor/ffwrapper.py,sha256=jga7-HbQnC4w21qZk4aY4kwLT7UPqkGn6NJPFM5Qssc,7811
9
9
  auto_editor/help.py,sha256=BFiP7vBz42TUzum4-zaQIrV1OY7kHeN0pe0MPE0T5xw,7997
10
- auto_editor/make_layers.py,sha256=0Vwawcx-MvfqM9yNcBPDIiTd2nv0LMWUdZ1KZhqE37c,8394
11
- auto_editor/output.py,sha256=ySTt0WiU4-VszsATLxpsz5HIIL-7FzoOm-yJrJRqi3E,6714
10
+ auto_editor/make_layers.py,sha256=P49tkQ2td0s6-IPWpnM9iFlyVV-KckLK4Y9xdxrRyhk,9613
11
+ auto_editor/output.py,sha256=5_4HtSkd6Lwv2ATEwLKJuXL1yBGScWAPLxvh_nHrBA4,8101
12
12
  auto_editor/preview.py,sha256=fo2BDIkvl96q_ssq8AAu1tl6FN_j23h8987aDPSmjDs,3094
13
13
  auto_editor/timeline.py,sha256=JwcS-8AS5vsoTL_m03aosYijScQef4AGa2lyutQ8wbI,7069
14
- auto_editor/validate_input.py,sha256=G4LzUdt0fSrIPRd-wvP7x9cOzXmHTd7-BPrFk2ZNEWk,2671
15
- auto_editor/vanparse.py,sha256=kHvGK7itqt37q0MPTSriPljB7ilFpjG5LuEVdulUbyg,9902
14
+ auto_editor/validate_input.py,sha256=JQt7J5xOBJDp6lnd2sQptpYhf7Z_hyxNEzLsE9E7LKU,2596
15
+ auto_editor/vanparse.py,sha256=p0u1X2gKM_lWB9bg-Lotqq9-bjfLgsVW9-U4Lwbz0aU,10029
16
16
  auto_editor/wavfile.py,sha256=7N2LX_WfFVRnoXrKveLvuyTYpIz2htpGqfCD8tR4kJ8,9168
17
17
  auto_editor/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  auto_editor/formats/fcp11.py,sha256=VwJWJs1qNDIVC8-pswipmKCk0e4V3LnE5fAMA0pPWVg,5857
19
- auto_editor/formats/fcp7.py,sha256=i6MKTErzROu0VveHfZSTIJXDrH3VL8u_IXD9pblXsIk,17613
19
+ auto_editor/formats/fcp7.py,sha256=fH86sxhlUysWisjvlqzZJgDrRpj3dSzFG-Eho2sehdc,17610
20
20
  auto_editor/formats/json.py,sha256=Br-xHVHj59C0OPP2FwfJeht_fImepRXsaw0iDFvK7-w,7693
21
- auto_editor/formats/shotcut.py,sha256=pbBQwOZ8Kqfm5ns0k_rBUX0XH_urIGfp77GORrzoW5Y,4984
21
+ auto_editor/formats/shotcut.py,sha256=-ES854LLFCMCBe100JRJedDmuk8zPev17aQMTrzPv-g,4923
22
22
  auto_editor/formats/utils.py,sha256=GIZw28WHuCIaZ_zMI0v6Kxbq0QaIpbLsdSegdYwQxQ8,1990
23
23
  auto_editor/lang/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  auto_editor/lang/json.py,sha256=OsNcYlfEj8ZLlzLK-gkLcrCCKI7mJz9rpe-6XLr4f9U,9231
@@ -30,16 +30,16 @@ auto_editor/lib/data_structs.py,sha256=EXNcdMsdmZxMRlpbXmIbRoC-YfGzvPZi7EdBQGwvp
30
30
  auto_editor/lib/err.py,sha256=UlszQJdzMZwkbT8x3sY4GkCV_5x9yrd6uVVUzvA8iiI,35
31
31
  auto_editor/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  auto_editor/render/audio.py,sha256=pUhD4rQZfUnyzKgpuxNxl_2CUGwbkAWo2356HUAW7VM,8835
33
- auto_editor/render/subtitle.py,sha256=D4WDiY4iM9HsNfJvZay7zv_gvZPvyd12nd9Fi9vbPjQ,4646
34
- auto_editor/render/video.py,sha256=eSklzWvIdoagjJ7r4yTJMgHWUs2xqxAoF4gZtv90yIg,13184
33
+ auto_editor/render/subtitle.py,sha256=RUG0hNh0Mbt3IRxef9hKUpUPXvyTa3HVoqoSHYivAT4,4355
34
+ auto_editor/render/video.py,sha256=E1snevFC4mMk_S_oDquJPKyElQfdDodO7EJEQ22JJF8,13202
35
35
  auto_editor/subcommands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  auto_editor/subcommands/desc.py,sha256=GDrKJYiHMaeTrplZAceXl1JwoqD78XsV2_5lc0Xd7po,869
37
- auto_editor/subcommands/info.py,sha256=Xq4dVPOC44lmzkvuMJg0INtN_n-hlQ8Nu9mVGRehtC8,6906
38
- auto_editor/subcommands/levels.py,sha256=XHMG3jsdoXBvG0TlP1bBbtjD0m5EgWnOMBTIYx8VAnA,4001
37
+ auto_editor/subcommands/info.py,sha256=7Sgt9WR0rWxe7juCRKseMxW6gKv3z3voqFcL8-MOVVM,6927
38
+ auto_editor/subcommands/levels.py,sha256=qSEXSkYPOCmr4_VZ1xAwtCZzaBOe8wXY0T-orN3Qg_A,4006
39
39
  auto_editor/subcommands/palet.py,sha256=tbQoRWoT4jR3yu0etGApfprM-oQgXIjC-rIY-QG3nM0,655
40
- auto_editor/subcommands/repl.py,sha256=xoNq88PtbvX3r1-FLStOb5jNoJ_rFzrl7R3Tk8a7zyI,3717
40
+ auto_editor/subcommands/repl.py,sha256=nbCVIyFwG3HRtGr8q-yLtASVMbMKDzAbn5vqA6RC9bk,3722
41
41
  auto_editor/subcommands/subdump.py,sha256=2rIaGVtWWMBbPJ0NouPD7fY5lhk0QD_XKE_4EnAeWPw,892
42
- auto_editor/subcommands/test.py,sha256=2N1Hk03Oofs9WssvrUfDMaDFEp9ngcu9IwIgXkYfcGk,24810
42
+ auto_editor/subcommands/test.py,sha256=UcpJp-PMcoUyz6LDD4y2V6EQ9w1ed66fPZr4GpzJReg,25050
43
43
  auto_editor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  auto_editor/utils/bar.py,sha256=RJqkJ8gNr8op_Z-2hh48ExjSonmTPX-RshctK_itv14,3988
45
45
  auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,52
@@ -47,11 +47,12 @@ auto_editor/utils/cmdkw.py,sha256=XApxw7FZBOEJV9N4LHhdw1GVfHbFfCjr-zCZ1gJsSvY,60
47
47
  auto_editor/utils/container.py,sha256=cl8wN5w-PjShPabnppil56r2dykQCfWdsR45jBbCkuo,7976
48
48
  auto_editor/utils/encoder.py,sha256=auNYo7HXbcU4iTUCc0LE5lpwFmSvdWvBm6-5KIaRK8w,2983
49
49
  auto_editor/utils/func.py,sha256=H38xO6Wxg1TZILVrx-nCowCzj_mqBUtJuOFp4DV3Hsc,4843
50
- auto_editor/utils/log.py,sha256=edfQPmdfBJMBFeCfxVjitXb74vk2o07xBrbgEccJ00U,2774
50
+ auto_editor/utils/log.py,sha256=ry-C92PRkJ-c8PQYIs1imk1qigDYfsCoLBYK6CQSP7I,2844
51
+ auto_editor/utils/subtitle_tools.py,sha256=TjjVPiT8bWzZJcrZjF7ddpgfIsVkLE4LyxXzBswHAGU,693
51
52
  auto_editor/utils/types.py,sha256=zWbU_VkcdP4yHHzKyaSiXu560n5U53i0x5SPkUDsCZU,11570
52
- auto_editor-24.24.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
53
- auto_editor-24.24.1.dist-info/METADATA,sha256=7iZzk70Se4J3tk0cafCj9J6PLuza2JF0gOtPi5guV3A,6284
54
- auto_editor-24.24.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
55
- auto_editor-24.24.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
56
- auto_editor-24.24.1.dist-info/top_level.txt,sha256=xwV1JV1ZeRmlH9VeBRZXgXtWHpWSD4w1mY5II56D3ns,22
57
- auto_editor-24.24.1.dist-info/RECORD,,
53
+ auto_editor-24.27.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
54
+ auto_editor-24.27.1.dist-info/METADATA,sha256=GBxzQhnfrlu3KJ89iT7RZ04nvEhUeJfAOzPl6OxtS6A,6322
55
+ auto_editor-24.27.1.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
56
+ auto_editor-24.27.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
57
+ auto_editor-24.27.1.dist-info/top_level.txt,sha256=xwV1JV1ZeRmlH9VeBRZXgXtWHpWSD4w1mY5II56D3ns,22
58
+ auto_editor-24.27.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (70.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5