auto-editor 26.3.0__py3-none-any.whl → 26.3.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 +1 @@
1
- __version__ = "26.3.0"
1
+ __version__ = "26.3.1"
auto_editor/analyze.py CHANGED
@@ -178,7 +178,6 @@ class Levels:
178
178
 
179
179
  with av.open(self.src.path, "r") as container:
180
180
  audio_stream = container.streams.audio[0]
181
- self.log.experimental(audio_stream.codec)
182
181
  result = sum(1 for _ in iter_audio(audio_stream, self.tb))
183
182
 
184
183
  self.log.debug(f"Audio Length: {result}")
@@ -263,9 +262,6 @@ class Levels:
263
262
  container = av.open(self.src.path, "r")
264
263
  audio = container.streams.audio[stream]
265
264
 
266
- if audio.codec.experimental:
267
- self.log.error(f"`{audio.codec.name}` is an experimental codec")
268
-
269
265
  if audio.duration is not None and audio.time_base is not None:
270
266
  inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
271
267
  elif container.duration is not None:
@@ -304,9 +300,6 @@ class Levels:
304
300
  container = av.open(self.src.path, "r")
305
301
  video = container.streams.video[stream]
306
302
 
307
- if video.codec.experimental:
308
- self.log.experimental(video.codec)
309
-
310
303
  inaccurate_dur = (
311
304
  1024
312
305
  if video.duration is None or video.time_base is None
@@ -136,7 +136,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
136
136
  else:
137
137
  container = av.open(src.path, "r")
138
138
  audio_stream = container.streams.audio[obj["stream"]]
139
- log.experimental(audio_stream.codec)
140
139
 
141
140
  values = []
142
141
 
@@ -158,7 +157,6 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
158
157
  else:
159
158
  container = av.open(src.path, "r")
160
159
  video_stream = container.streams.video[obj["stream"]]
161
- log.experimental(video_stream.codec)
162
160
 
163
161
  values = []
164
162
 
auto_editor/cmds/test.py CHANGED
@@ -9,6 +9,7 @@ from fractions import Fraction
9
9
  from time import perf_counter
10
10
  from typing import TYPE_CHECKING
11
11
 
12
+ import av
12
13
  import numpy as np
13
14
 
14
15
  from auto_editor.ffwrapper import FileInfo, initFileInfo
@@ -235,6 +236,11 @@ def main(sys_args: list[str] | None = None):
235
236
 
236
237
  def example():
237
238
  out = run.main(inputs=["example.mp4"], cmd=[])
239
+
240
+ with av.open(out) as container:
241
+ assert container.streams[0].type == "video"
242
+ assert container.streams[1].type == "audio"
243
+
238
244
  cn = fileinfo(out)
239
245
  video = cn.videos[0]
240
246
 
auto_editor/edit.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import sys
5
5
  from fractions import Fraction
6
+ from heapq import heappop, heappush
6
7
  from os.path import splitext
7
8
  from subprocess import run
8
9
  from typing import Any
@@ -309,6 +310,13 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
309
310
  else:
310
311
  audio_paths = []
311
312
 
313
+ # Setup video
314
+ if ctr.default_vid != "none" and tl.v:
315
+ vframes = render_av(output, tl, args, log)
316
+ output_stream = next(vframes)
317
+ else:
318
+ output_stream, vframes = None, iter([])
319
+
312
320
  # Setup audio
313
321
  if audio_paths:
314
322
  try:
@@ -363,23 +371,16 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
363
371
  subtitle_streams.append(subtitle_stream)
364
372
  sub_gen_frames.append(subtitle_input.demux(subtitles=0))
365
373
 
366
- # Setup video
367
- if ctr.default_vid != "none" and tl.v:
368
- vframes = render_av(output, tl, args, log)
369
- output_stream = next(vframes)
370
- else:
371
- output_stream, vframes = None, iter([])
372
-
373
374
  no_color = log.no_color or log.machine
374
375
  encoder_titles = []
375
376
  if output_stream is not None:
376
377
  name = output_stream.codec.canonical_name
377
378
  encoder_titles.append(name if no_color else f"\033[95m{name}")
378
379
  if audio_streams:
379
- name = audio_streams[0].name
380
+ name = audio_streams[0].codec.canonical_name
380
381
  encoder_titles.append(name if no_color else f"\033[96m{name}")
381
382
  if subtitle_streams:
382
- name = subtitle_streams[0].name
383
+ name = subtitle_streams[0].codec.canonical_name
383
384
  encoder_titles.append(name if no_color else f"\033[32m{name}")
384
385
 
385
386
  title = f"({os.path.splitext(output_path)[1][1:]}) "
@@ -389,11 +390,68 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
389
390
  title += "\033[0m+".join(encoder_titles) + "\033[0m"
390
391
  bar.start(tl.end, title)
391
392
 
392
- # Process frames
393
+ MAX_AUDIO_AHEAD = 30 # In timebase, how far audio can be ahead of video.
394
+ MAX_SUB_AHEAD = 30
395
+
396
+ class Priority:
397
+ __slots__ = ("index", "frame_type", "frame", "stream")
398
+
399
+ def __init__(self, value: int | Fraction, frame, stream):
400
+ self.frame_type: str = stream.type
401
+ assert self.frame_type in ("audio", "subtitle", "video")
402
+ if self.frame_type in {"audio", "subtitle"}:
403
+ self.index: int | float = round(value * frame.time_base * tl.tb)
404
+ else:
405
+ self.index = float("inf") if value is None else int(value)
406
+ self.frame = frame
407
+ self.stream = stream
408
+
409
+ def __lt__(self, other):
410
+ return self.index < other.index
411
+
412
+ def __eq__(self, other):
413
+ return self.index == other.index
414
+
415
+ # Priority queue for ordered frames by time_base.
416
+ frame_queue: list[Priority] = []
417
+ latest_audio_index = float("-inf")
418
+ latest_sub_index = float("-inf")
419
+ earliest_video_index = None
420
+
393
421
  while True:
394
- audio_frames = [next(frames, None) for frames in audio_gen_frames]
422
+ if earliest_video_index is None:
423
+ should_get_audio = True
424
+ should_get_sub = True
425
+ else:
426
+ for item in frame_queue:
427
+ if item.frame_type == "audio":
428
+ latest_audio_index = max(latest_audio_index, item.index)
429
+ elif item.frame_type == "subtitle":
430
+ latest_sub_index = max(latest_sub_index, item.index)
431
+
432
+ should_get_audio = (
433
+ latest_audio_index <= earliest_video_index + MAX_AUDIO_AHEAD
434
+ )
435
+ should_get_sub = (
436
+ latest_sub_index <= earliest_video_index + MAX_SUB_AHEAD
437
+ )
438
+
395
439
  index, video_frame = next(vframes, (0, None))
396
- subtitle_frames = [next(packet, None) for packet in sub_gen_frames]
440
+
441
+ if video_frame:
442
+ earliest_video_index = index
443
+ heappush(frame_queue, Priority(index, video_frame, output_stream))
444
+
445
+ if should_get_audio:
446
+ audio_frames = [next(frames, None) for frames in audio_gen_frames]
447
+ else:
448
+ audio_frames = [None]
449
+ if should_get_sub:
450
+ subtitle_frames = [next(packet, None) for packet in sub_gen_frames]
451
+ else:
452
+ subtitle_frames = [None]
453
+
454
+ # Break if no more frames
397
455
  if (
398
456
  all(frame is None for frame in audio_frames)
399
457
  and video_frame is None
@@ -401,24 +459,34 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
401
459
  ):
402
460
  break
403
461
 
404
- for audio_stream, audio_frame in zip(audio_streams, audio_frames):
405
- if audio_frame:
462
+ if should_get_audio:
463
+ for audio_stream, audio_frame in zip(audio_streams, audio_frames):
406
464
  for reframe in resampler.resample(audio_frame):
407
- output.mux(audio_stream.encode(reframe))
408
-
409
- for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
410
- if not packet or packet.dts is None:
411
- continue
412
- packet.stream = subtitle_stream
413
- output.mux(packet)
414
-
415
- if video_frame:
465
+ assert reframe.pts is not None
466
+ heappush(
467
+ frame_queue,
468
+ Priority(reframe.pts, reframe, audio_stream),
469
+ )
470
+ if should_get_sub:
471
+ for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
472
+ if packet and packet.pts is not None:
473
+ packet.stream = subtitle_stream
474
+ heappush(
475
+ frame_queue, Priority(packet.pts, packet, subtitle_stream)
476
+ )
477
+
478
+ while frame_queue and frame_queue[0].index <= index:
479
+ item = heappop(frame_queue)
480
+ frame_type = item.frame_type
416
481
  try:
417
- output.mux(output_stream.encode(video_frame))
482
+ if frame_type in {"video", "audio"}:
483
+ output.mux(item.stream.encode(item.frame))
484
+ elif frame_type == "subtitle":
485
+ output.mux(item.frame)
418
486
  except av.error.ExternalError:
419
487
  log.error(
420
- f"Generic error for encoder: {output_stream.name}\n"
421
- "Perhaps video quality settings are too low?"
488
+ f"Generic error for encoder: {item.stream.name}\n"
489
+ f"at {item.index} time_base\nPerhaps video quality settings are too low?"
422
490
  )
423
491
  except av.FFmpegError as e:
424
492
  log.error(e)
auto_editor/utils/bar.py CHANGED
@@ -89,20 +89,26 @@ class Bar:
89
89
  percent = round(progress * 100, 1)
90
90
  p_pad = " " * (4 - len(str(percent)))
91
91
  columns = get_terminal_size().columns
92
- bar_len = max(1, columns - len_title - 32)
92
+ bar_len = max(1, columns - len_title - 35)
93
93
  bar_str = self._bar_str(progress, bar_len)
94
94
 
95
- bar = f" {self.icon}{title} {bar_str} {p_pad}{percent}% ETA {new_time}"
96
-
97
- if len(bar) > columns - 2:
98
- bar = bar[: columns - 2]
99
- else:
100
- bar += " " * (columns - len(bar) - 4)
101
-
102
- sys.stdout.write(bar + "\r")
95
+ bar = f" {self.icon}{title} {bar_str} {p_pad}{percent}% ETA {new_time} \r"
96
+ sys.stdout.write(bar)
103
97
 
104
98
  def start(self, total: float, title: str = "Please wait") -> None:
105
- self.stack.append((title, len(title), total, time()))
99
+ len_title = 0
100
+ in_escape = False
101
+
102
+ for char in title:
103
+ if not in_escape:
104
+ if char == "\033":
105
+ in_escape = True
106
+ else:
107
+ len_title += 1
108
+ elif char == "m":
109
+ in_escape = False
110
+
111
+ self.stack.append((title, len_title, total, time()))
106
112
 
107
113
  try:
108
114
  self.tick(0)
auto_editor/utils/log.py CHANGED
@@ -1,14 +1,9 @@
1
- from __future__ import annotations
2
-
3
1
  import sys
4
2
  from datetime import timedelta
5
3
  from shutil import get_terminal_size, rmtree
6
4
  from tempfile import mkdtemp
7
5
  from time import perf_counter, sleep
8
- from typing import TYPE_CHECKING, NoReturn
9
-
10
- if TYPE_CHECKING:
11
- import av
6
+ from typing import NoReturn
12
7
 
13
8
 
14
9
  class Log:
@@ -100,10 +95,6 @@ class Log:
100
95
 
101
96
  sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
102
97
 
103
- def experimental(self, codec: av.Codec) -> None:
104
- if codec.experimental:
105
- self.error(f"`{codec.name}` is an experimental codec")
106
-
107
98
  @staticmethod
108
99
  def deprecated(message: str) -> None:
109
100
  sys.stderr.write(f"\033[1m\033[33m{message}\033[0m\n")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: auto-editor
3
- Version: 26.3.0
3
+ Version: 26.3.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.14,>=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: numpy<3.0,>=1.24
15
- Requires-Dist: pyav==14.2.0
15
+ Requires-Dist: pyav==14.2.*
16
16
 
17
17
  <p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
18
18
 
@@ -1,7 +1,7 @@
1
- auto_editor/__init__.py,sha256=foFMRrw-Br8Dq0ZIes1siNTsxZj5nQywX_cfWHfB380,23
1
+ auto_editor/__init__.py,sha256=cbuXE-xYiZp6P9Ipk0EAUgPxPvaOpg-ve_HurO96fCM,23
2
2
  auto_editor/__main__.py,sha256=g-9q3i6oFeNPeFMeNJEwZua6ZloOPLcaLN-B0FlxWXo,11371
3
- auto_editor/analyze.py,sha256=7p_SDRRKQzlc6wtoW6MXYvJkDV4NO4DSOtJPFAuYcwM,12721
4
- auto_editor/edit.py,sha256=gz2MocM5s1uz-hLrFWmiNl1TwOm4S_1EkYn-ZBuu_sE,17401
3
+ auto_editor/analyze.py,sha256=tkNZdRz1Nf1mQfC3jKHaiLbHRkacX_7-2TrKTKvwpxY,12463
4
+ auto_editor/edit.py,sha256=unOxLPiPUMoLgM_ScdRA3rNnxOicUUu7iLO1lG0zcfc,20260
5
5
  auto_editor/ffwrapper.py,sha256=1lYYfq8gVgMVkYWeAEYDPAHCwFCYbKQwy0FxYBxMzk8,4765
6
6
  auto_editor/help.py,sha256=CzfDTsL4GuGu596ySHKj_wKnxGR9h8B0KUdkZpo33oE,8044
7
7
  auto_editor/make_layers.py,sha256=vEeJt0PnE1vc9-cQZ_AlXVDjvWhObRCWJSCQGraoMvU,9016
@@ -14,11 +14,11 @@ auto_editor/cmds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
14
14
  auto_editor/cmds/cache.py,sha256=bViYbtVXefTeEIUvSanDfA6cG35ep1N_Jvtz7ZjgIkY,1959
15
15
  auto_editor/cmds/desc.py,sha256=GDrKJYiHMaeTrplZAceXl1JwoqD78XsV2_5lc0Xd7po,869
16
16
  auto_editor/cmds/info.py,sha256=vYa1hYdE8kDTE8AS3kwXlnd59X6CrE2GtIEQ7UmlpRY,7010
17
- auto_editor/cmds/levels.py,sha256=qMxTOlj4ezna0K2jYWDFyZ0Srn4pT6nIdR-479IOEvw,5758
17
+ auto_editor/cmds/levels.py,sha256=RBBs0YM8Qb7XteCy7Pr4QpU9b5maMsDe6xEkAuyGs1c,5644
18
18
  auto_editor/cmds/palet.py,sha256=ONzTqemaQq9YEfIOsDRNnwzfqnEMUMSXIQrETxyroRU,749
19
19
  auto_editor/cmds/repl.py,sha256=TF_I7zsFY7-KdgidrqjafTz7o_eluVbLvgTcOBG-UWQ,3449
20
20
  auto_editor/cmds/subdump.py,sha256=af_XBf7kaevqHn1A71z8C-7x8pS5WKD9FE_ugkCw6rk,665
21
- auto_editor/cmds/test.py,sha256=Wih-KQnv5Ld1CgbOUdtrp_fMnINsr_khzONvlrJ-bhw,26163
21
+ auto_editor/cmds/test.py,sha256=D8l4Zr0UjrUtFkqsADqlVQ1wvNP3s97ifNnyf1UBTFU,26327
22
22
  auto_editor/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  auto_editor/formats/fcp11.py,sha256=sqjC36jI47ICPLjZJYiqGwY7foOnWOiNjkPFLdgSnI4,5208
24
24
  auto_editor/formats/fcp7.py,sha256=x5cagTzGCAW3i3M6m7TZC1h8gLfSmX1UK-iiDuCpdfs,20289
@@ -40,17 +40,17 @@ auto_editor/render/audio.py,sha256=_GuX0WNY1YeumgBN3bWqgwVXiuhpvx7sijABxqyO2ag,1
40
40
  auto_editor/render/subtitle.py,sha256=jtNRKvgo1fpHTrAfGZqdkNeNgGgasw-K-4PwIKiWwfM,6231
41
41
  auto_editor/render/video.py,sha256=JBVl8w-hQ6zrs97iA527LPsBZ9s601SVSJs2bSMCq88,12185
42
42
  auto_editor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- auto_editor/utils/bar.py,sha256=0ZSfuWdBA1zySwodkb4mw5uahC6UwCk2umaGnB8A7n0,3996
43
+ auto_editor/utils/bar.py,sha256=Ky9JRf37JTgLyvNuIXDfucaUE8H1vBbCqKLjttmsmmo,4156
44
44
  auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,52
45
45
  auto_editor/utils/cmdkw.py,sha256=aUGBvBel2Ko1o6Rwmr4rEL-BMc5hEnzYLbyZ1GeJdcY,5729
46
46
  auto_editor/utils/container.py,sha256=C_Ahh7nlMEX4DNQ2M_cITPPbYcIL68r4I_AgFy0OD6o,2487
47
47
  auto_editor/utils/func.py,sha256=C8ucgsSEzPyBc-8obhsCXd_uQW0cnCdBn1KVxB7FHjU,2747
48
- auto_editor/utils/log.py,sha256=8fOdyTG3vjKhA1tJTMKRjXVqhrY2q3tFnXU8tKm_twA,3937
48
+ auto_editor/utils/log.py,sha256=wPNf6AabV-0cnoS_bPLv1Lh7llQBtNqPKeh07einOuc,3701
49
49
  auto_editor/utils/types.py,sha256=r5f6QB81xH7NRwGntITIOCVx-fupOl8l3X3LSFkt3nE,10756
50
50
  docs/build.py,sha256=POy8X8QOBYe_8A8HI_yiVI_Qg9E5mLpn1z7AHQr0_vQ,1888
51
- auto_editor-26.3.0.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
52
- auto_editor-26.3.0.dist-info/METADATA,sha256=YOFNFBSIMQveLah-PSIrBtlfY-xwEveHxKNAneUAs_0,6111
53
- auto_editor-26.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
54
- auto_editor-26.3.0.dist-info/entry_points.txt,sha256=UAsTc7qJQbnAzHd7KWg-ALo_X9Hj2yDs3M9I2DV3eyI,212
55
- auto_editor-26.3.0.dist-info/top_level.txt,sha256=jBV5zlbWRbKOa-xaWPvTD45QL7lGExx2BDzv-Ji4dTw,17
56
- auto_editor-26.3.0.dist-info/RECORD,,
51
+ auto_editor-26.3.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
52
+ auto_editor-26.3.1.dist-info/METADATA,sha256=4r9rtkLWEDaVMcJauMcljdAw-cnz2h6xS9sixAnWVlQ,6111
53
+ auto_editor-26.3.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
54
+ auto_editor-26.3.1.dist-info/entry_points.txt,sha256=UAsTc7qJQbnAzHd7KWg-ALo_X9Hj2yDs3M9I2DV3eyI,212
55
+ auto_editor-26.3.1.dist-info/top_level.txt,sha256=jBV5zlbWRbKOa-xaWPvTD45QL7lGExx2BDzv-Ji4dTw,17
56
+ auto_editor-26.3.1.dist-info/RECORD,,