auto-editor 24.24.1__tar.gz → 24.27.1__tar.gz

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.
Files changed (64) hide show
  1. {auto_editor-24.24.1/auto_editor.egg-info → auto_editor-24.27.1}/PKG-INFO +6 -8
  2. {auto_editor-24.24.1 → auto_editor-24.27.1}/README.md +4 -6
  3. auto_editor-24.27.1/auto_editor/__init__.py +2 -0
  4. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/__main__.py +29 -26
  5. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/analyze.py +54 -19
  6. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/edit.py +7 -17
  7. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/ffwrapper.py +6 -2
  8. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/fcp7.py +3 -3
  9. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/shotcut.py +1 -2
  10. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/make_layers.py +46 -8
  11. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/output.py +50 -14
  12. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/render/subtitle.py +7 -13
  13. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/render/video.py +9 -9
  14. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/info.py +1 -1
  15. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/levels.py +1 -1
  16. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/repl.py +1 -1
  17. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/test.py +12 -5
  18. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/log.py +33 -34
  19. auto_editor-24.27.1/auto_editor/utils/subtitle_tools.py +29 -0
  20. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/validate_input.py +9 -12
  21. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/vanparse.py +26 -27
  22. {auto_editor-24.24.1 → auto_editor-24.27.1/auto_editor.egg-info}/PKG-INFO +6 -8
  23. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/SOURCES.txt +1 -0
  24. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/requires.txt +1 -1
  25. {auto_editor-24.24.1 → auto_editor-24.27.1}/pyproject.toml +1 -1
  26. auto_editor-24.24.1/auto_editor/__init__.py +0 -2
  27. {auto_editor-24.24.1 → auto_editor-24.27.1}/LICENSE +0 -0
  28. {auto_editor-24.24.1 → auto_editor-24.27.1}/ae-ffmpeg/ae_ffmpeg/__init__.py +0 -0
  29. {auto_editor-24.24.1 → auto_editor-24.27.1}/ae-ffmpeg/ae_ffmpeg/py.typed +0 -0
  30. {auto_editor-24.24.1 → auto_editor-24.27.1}/ae-ffmpeg/setup.py +0 -0
  31. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/__init__.py +0 -0
  32. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/fcp11.py +0 -0
  33. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/json.py +0 -0
  34. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/utils.py +0 -0
  35. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/help.py +0 -0
  36. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lang/__init__.py +0 -0
  37. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lang/json.py +0 -0
  38. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lang/libmath.py +0 -0
  39. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lang/palet.py +0 -0
  40. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lib/__init__.py +0 -0
  41. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lib/contracts.py +0 -0
  42. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lib/data_structs.py +0 -0
  43. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lib/err.py +0 -0
  44. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/preview.py +0 -0
  45. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/render/__init__.py +0 -0
  46. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/render/audio.py +0 -0
  47. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/__init__.py +0 -0
  48. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/desc.py +0 -0
  49. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/palet.py +0 -0
  50. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/subdump.py +0 -0
  51. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/timeline.py +0 -0
  52. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/__init__.py +0 -0
  53. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/bar.py +0 -0
  54. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/chunks.py +0 -0
  55. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/cmdkw.py +0 -0
  56. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/container.py +0 -0
  57. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/encoder.py +0 -0
  58. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/func.py +0 -0
  59. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/types.py +0 -0
  60. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/wavfile.py +0 -0
  61. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/dependency_links.txt +0 -0
  62. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/entry_points.txt +0 -0
  63. {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/top_level.txt +0 -0
  64. {auto_editor-24.24.1 → auto_editor-24.27.1}/setup.cfg +0 -0
@@ -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
@@ -88,16 +88,14 @@ Auto-Editor can also export to:
88
88
  - Individual media clips with `--export clip-sequence`
89
89
 
90
90
  ### Naming Timelines
91
- By default, auto-editor will name the timeline to "Auto-Editor Media Group" if the export supports naming.
91
+ 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.
92
92
 
93
93
  ```
94
+ # for POSIX shells
94
95
  auto-editor example.mp4 --export 'premiere:name="Your name here"'
95
96
 
96
- auto-editor example.mp4 --export 'resolve:name="Your name here"'
97
-
98
- auto-editor example.mp4 --export 'final-cut-pro:name="Your name here"'
99
-
100
- # No other export options support naming
97
+ # for Powershell
98
+ auto-editor example.mp4 --export 'premiere:name=""Your name here""'
101
99
  ```
102
100
 
103
101
  ### Split by Clip
@@ -0,0 +1,2 @@
1
+ __version__ = "24.27.1"
2
+ version = "24w27a"
@@ -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()
@@ -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
 
@@ -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:
@@ -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
@@ -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)