auto-editor 24.19.1__tar.gz → 24.25.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.19.1/auto_editor.egg-info → auto_editor-24.25.1}/PKG-INFO +6 -8
  2. {auto_editor-24.19.1 → auto_editor-24.25.1}/README.md +4 -6
  3. auto_editor-24.25.1/auto_editor/__init__.py +2 -0
  4. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/__main__.py +25 -21
  5. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/analyze.py +55 -29
  6. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/edit.py +4 -15
  7. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/ffwrapper.py +0 -2
  8. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/fcp7.py +1 -1
  9. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/json.py +6 -3
  10. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/make_layers.py +79 -41
  11. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/output.py +11 -10
  12. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/render/subtitle.py +7 -13
  13. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/render/video.py +0 -3
  14. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/info.py +10 -1
  15. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/subdump.py +7 -7
  16. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/bar.py +2 -2
  17. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/log.py +36 -34
  18. auto_editor-24.25.1/auto_editor/utils/subtitle_tools.py +29 -0
  19. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/types.py +5 -4
  20. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/validate_input.py +1 -1
  21. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/vanparse.py +23 -24
  22. {auto_editor-24.19.1 → auto_editor-24.25.1/auto_editor.egg-info}/PKG-INFO +6 -8
  23. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/SOURCES.txt +1 -0
  24. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/requires.txt +1 -1
  25. {auto_editor-24.19.1 → auto_editor-24.25.1}/pyproject.toml +1 -1
  26. auto_editor-24.19.1/auto_editor/__init__.py +0 -2
  27. {auto_editor-24.19.1 → auto_editor-24.25.1}/LICENSE +0 -0
  28. {auto_editor-24.19.1 → auto_editor-24.25.1}/ae-ffmpeg/ae_ffmpeg/__init__.py +0 -0
  29. {auto_editor-24.19.1 → auto_editor-24.25.1}/ae-ffmpeg/ae_ffmpeg/py.typed +0 -0
  30. {auto_editor-24.19.1 → auto_editor-24.25.1}/ae-ffmpeg/setup.py +0 -0
  31. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/__init__.py +0 -0
  32. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/fcp11.py +0 -0
  33. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/shotcut.py +0 -0
  34. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/formats/utils.py +0 -0
  35. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/help.py +0 -0
  36. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lang/__init__.py +0 -0
  37. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lang/json.py +0 -0
  38. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lang/libmath.py +0 -0
  39. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lang/palet.py +0 -0
  40. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lib/__init__.py +0 -0
  41. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lib/contracts.py +0 -0
  42. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lib/data_structs.py +0 -0
  43. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/lib/err.py +0 -0
  44. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/preview.py +0 -0
  45. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/render/__init__.py +0 -0
  46. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/render/audio.py +0 -0
  47. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/__init__.py +0 -0
  48. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/desc.py +0 -0
  49. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/levels.py +0 -0
  50. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/palet.py +0 -0
  51. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/repl.py +0 -0
  52. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/subcommands/test.py +0 -0
  53. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/timeline.py +0 -0
  54. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/__init__.py +0 -0
  55. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/chunks.py +0 -0
  56. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/cmdkw.py +0 -0
  57. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/container.py +0 -0
  58. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/encoder.py +0 -0
  59. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/utils/func.py +0 -0
  60. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor/wavfile.py +0 -0
  61. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/dependency_links.txt +0 -0
  62. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/entry_points.txt +0 -0
  63. {auto_editor-24.19.1 → auto_editor-24.25.1}/auto_editor.egg-info/top_level.txt +0 -0
  64. {auto_editor-24.19.1 → auto_editor-24.25.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 24.19.1
3
+ Version: 24.25.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.0.5
15
+ Requires-Dist: pyav==12.1.0
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.25.1"
2
+ version = "24w25a"
@@ -1,8 +1,11 @@
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
10
  from auto_editor.utils.log import Log
8
11
  from auto_editor.utils.types import (
@@ -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,36 +304,32 @@ 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
- is64bit = "64-bit" if sys.maxsize > 2**32 else "32-bit"
316
- print(f"Python Version: {plat.python_version()} {is64bit}")
312
+ import av
313
+
314
+ print(f"Python Version: {plat.python_version()}")
317
315
  print(f"Platform: {plat.system()} {plat.release()} {plat.machine().lower()}")
318
- print(f"FFmpeg Version: {ffmpeg.version}\nFFmpeg Path: {ffmpeg.path}")
316
+ print(f"PyAV Version: {av.__version__}")
319
317
  print(f"Auto-Editor Version: {auto_editor.version}")
320
- sys.exit()
318
+ return
321
319
 
322
- if args.input == []:
320
+ if not args.input:
323
321
  log.error("You need to give auto-editor an input file.")
324
322
 
325
- temp = setup_tempdir(args.temp_dir, Log())
326
- log = Log(args.debug, args.quiet, temp=temp)
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
334
 
331
335
  try:
@@ -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:
@@ -170,8 +171,6 @@ class Levels:
170
171
  # If there's no audio, get length in video metadata.
171
172
  import av
172
173
 
173
- av.logging.set_level(av.logging.PANIC)
174
-
175
174
  with av.open(f"{self.src.path}") as cn:
176
175
  if len(cn.streams.video) < 1:
177
176
  self.log.error("Could not get media duration")
@@ -227,11 +226,7 @@ class Levels:
227
226
  except Exception:
228
227
  json_object = {}
229
228
 
230
- entry = {
231
- "type": str(arr.dtype),
232
- "arr": arr.tolist(),
233
- }
234
-
229
+ entry = {"type": str(arr.dtype), "arr": arr.tolist()}
235
230
  src_key = f"{self.src.path}"
236
231
 
237
232
  if src_key in json_object:
@@ -313,44 +308,75 @@ class Levels:
313
308
  except re.error as e:
314
309
  self.log.error(e)
315
310
 
316
- sub_file = self.ensure.subtitle(self.src, stream)
317
- parser = SubtitleParser(self.tb)
311
+ import av
312
+ from av.subtitles.subtitle import AssSubtitle, TextSubtitle
318
313
 
319
- with open(sub_file, encoding="utf-8") as file:
320
- 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)
321
320
 
322
- # stackoverflow.com/questions/9662346/python-code-to-remove-html-tags-from-a-string
323
- def cleanhtml(raw_html: str) -> str:
324
- cleanr = re.compile("<.*?>")
325
- 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)
326
331
 
327
- if not parser.contents:
328
- self.log.error("subtitle has no valid entries")
332
+ end = round((start + dur) * self.tb)
333
+ sub_length = max(sub_length, end)
329
334
 
330
- result = np.zeros((parser.contents[-1].end), dtype=np.bool_)
335
+ result = np.zeros((sub_length), dtype=np.bool_)
336
+ del sub_length
331
337
 
332
338
  count = 0
333
- for content in parser.contents:
334
- 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:
335
345
  break
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)
336
356
 
337
- line = cleanhtml(content.after.strip())
338
- if line and re.search(pattern, line):
339
- result[content.start : content.end] = 1
340
- count += 1
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()
341
370
 
342
371
  return result
343
372
 
344
373
  def motion(self, s: int, blur: int, width: int) -> NDArray[np.float64]:
345
374
  import av
346
375
 
347
- av.logging.set_level(av.logging.PANIC)
348
-
349
- mobj = {"stream": s, "width": width, "blur": blur}
350
-
351
376
  if s >= len(self.src.videos):
352
377
  raise LevelError(f"motion: video stream '{s}' does not exist.")
353
378
 
379
+ mobj = {"stream": s, "width": width, "blur": blur}
354
380
  if (arr := self.read_cache("motion", mobj)) is not None:
355
381
  return arr
356
382
 
@@ -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
 
@@ -152,7 +152,6 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
152
152
  def edit_media(
153
153
  paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str, log: Log
154
154
  ) -> None:
155
- timer = Timer(args.quiet)
156
155
  bar = Bar(args.progress)
157
156
  tl = None
158
157
 
@@ -191,7 +190,6 @@ def edit_media(
191
190
 
192
191
  if export["export"] == "timeline":
193
192
  log.quiet = True
194
- timer.quiet = True
195
193
 
196
194
  if not args.preview:
197
195
  log.conwrite("Starting")
@@ -214,16 +212,7 @@ def edit_media(
214
212
  ensure = Ensure(ffmpeg, samplerate, temp, log)
215
213
 
216
214
  if tl is None:
217
- # Extract subtitles in their native format.
218
- if src is not None and len(src.subtitles) > 0 and not args.sn:
219
- cmd = ["-i", f"{src.path}", "-hide_banner"]
220
- for s, sub in enumerate(src.subtitles):
221
- cmd.extend(["-map", f"0:s:{s}"])
222
- for s, sub in enumerate(src.subtitles):
223
- cmd.extend([os.path.join(temp, f"{s}s.{sub.ext}")])
224
- ffmpeg.run(cmd)
225
-
226
- tl = make_timeline(sources, ffmpeg, ensure, args, samplerate, bar, temp, log)
215
+ tl = make_timeline(sources, ensure, args, samplerate, bar, temp, log)
227
216
 
228
217
  if export["export"] == "timeline":
229
218
  from auto_editor.formats.json import make_json_timeline
@@ -284,7 +273,7 @@ def edit_media(
284
273
  apply_later = False
285
274
 
286
275
  if ctr.allow_subtitle and not args.sn:
287
- sub_output = make_new_subtitles(tl, ffmpeg, temp, log)
276
+ sub_output = make_new_subtitles(tl, ensure, temp)
288
277
 
289
278
  if ctr.allow_audio:
290
279
  audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, temp, log)
@@ -358,7 +347,7 @@ def edit_media(
358
347
  else:
359
348
  make_media(tl, output)
360
349
 
361
- timer.stop()
350
+ log.stop_timer()
362
351
 
363
352
  if not args.no_open and export["export"] in ("default", "audio", "clip-sequence"):
364
353
  if args.player is None:
@@ -192,8 +192,6 @@ class FileInfo:
192
192
  def initFileInfo(path: str, log: Log) -> FileInfo:
193
193
  import av
194
194
 
195
- av.logging.set_level(av.logging.PANIC)
196
-
197
195
  try:
198
196
  cont = av.open(path, "r")
199
197
  except av.error.InvalidDataError:
@@ -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
 
@@ -181,11 +181,11 @@ def read_v1(tl: Any, log: Log) -> v3:
181
181
  for i, chunk in enumerate(chunks):
182
182
  if type(chunk) is not list or len(chunk) != 3:
183
183
  log.error(f"Invalid chunk at chunk {i}")
184
- if type(chunk[0]) is not int or chunk[0] < 0:
184
+ if type(chunk[0]) not in (int, float) or chunk[0] < 0:
185
185
  log.error(f"Invalid start at chunk {i}")
186
- if type(chunk[1]) is not int or chunk[1] <= chunk[0]:
186
+ if type(chunk[1]) not in (int, float) or chunk[1] <= chunk[0]:
187
187
  log.error(f"Invalid end at chunk {i}")
188
- if type(chunk[2]) is not float or chunk[2] < 0.0 or chunk[2] > 99999.0:
188
+ if type(chunk[2]) not in (int, float) or chunk[2] < 0.0 or chunk[2] > 99999.0:
189
189
  log.error(f"Invalid speed at chunk {i}")
190
190
 
191
191
  if i == 0 and chunk[0] != 0:
@@ -194,6 +194,9 @@ def read_v1(tl: Any, log: Log) -> v3:
194
194
  log.error(f"Invalid start at chunk {i}")
195
195
  last_end = chunk[1]
196
196
 
197
+ if type(chunk[0]) is float or type(chunk[1]) is float or type(chunk[2]) is int:
198
+ chunks[i] = (int(chunk[0]), int(chunk[1]), float(chunk[2]))
199
+
197
200
  for c in clipify(chunks, src):
198
201
  if src.videos:
199
202
  if len(vtl) == 0:
@@ -1,12 +1,13 @@
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
7
8
 
8
9
  from auto_editor.analyze import FileSetup, Levels
9
- from auto_editor.ffwrapper import FFmpeg, FileInfo
10
+ from auto_editor.ffwrapper import FileInfo
10
11
  from auto_editor.lang.palet import Lexer, Parser, env, interpret, is_boolarr
11
12
  from auto_editor.lib.data_structs import print_str
12
13
  from auto_editor.lib.err import MyError
@@ -110,9 +111,34 @@ def run_interpreter_for_edit_option(
110
111
  return result
111
112
 
112
113
 
114
+ def make_sane_timebase(fps: Fraction) -> Fraction:
115
+ tb = round(fps, 2)
116
+
117
+ ntsc_60 = Fraction(60_000, 1001)
118
+ ntsc = Fraction(30_000, 1001)
119
+ film_ntsc = Fraction(24_000, 1001)
120
+
121
+ if tb == round(ntsc_60, 2):
122
+ return ntsc_60
123
+ if tb == round(ntsc, 2):
124
+ return ntsc
125
+ if tb == round(film_ntsc, 2):
126
+ return film_ntsc
127
+ return tb
128
+
129
+
130
+ def parse_time(val: str, arr: NDArray, tb: Fraction) -> int: # raises: `CoerceError`
131
+ if val == "start":
132
+ return 0
133
+ if val == "end":
134
+ return len(arr)
135
+
136
+ num = time(val, tb)
137
+ return num if num >= 0 else num + len(arr)
138
+
139
+
113
140
  def make_timeline(
114
141
  sources: list[FileInfo],
115
- ffmpeg: FFmpeg,
116
142
  ensure: Ensure,
117
143
  args: Args,
118
144
  sr: int,
@@ -125,14 +151,9 @@ def make_timeline(
125
151
  if inp is None:
126
152
  tb, res = Fraction(30), (1920, 1080)
127
153
  else:
128
- tb = round(inp.get_fps() if args.frame_rate is None else args.frame_rate, 2)
129
- ntsc = Fraction(30_000, 1001)
130
- film_ntsc = Fraction(24_000, 1001)
131
- if tb == round(ntsc, 2):
132
- tb = ntsc
133
- elif tb == round(film_ntsc, 2):
134
- tb = film_ntsc
135
-
154
+ tb = make_sane_timebase(
155
+ inp.get_fps() if args.frame_rate is None else args.frame_rate
156
+ )
136
157
  res = inp.get_res() if args.resolution is None else args.resolution
137
158
 
138
159
  try:
@@ -171,36 +192,21 @@ def make_timeline(
171
192
  speed_hash[len(speed_map) - 1] = speed
172
193
  return len(speed_map) - 1
173
194
 
174
- def parse_time(val: str, arr: NDArray) -> int:
175
- if val == "start":
176
- return 0
177
- if val == "end":
178
- return len(arr)
179
- try:
180
- num = time(val, tb)
181
- return num if num >= 0 else num + len(arr)
182
- except CoerceError as e:
183
- log.error(e)
184
-
185
- def mut_set_range(arr: NDArray, _ranges: list[list[str]], index: float) -> None:
186
- for _range in _ranges:
187
- assert len(_range) == 2
188
- pair = [parse_time(val, arr) for val in _range]
189
- arr[pair[0] : pair[1]] = index
190
-
191
195
  try:
192
- if len(args.cut_out) > 0:
196
+ for _range in args.cut_out:
193
197
  # always cut out even if 'silent_speed' is not 99,999
194
- mut_set_range(speed_index, args.cut_out, get_speed_index(99_999))
198
+ pair = [parse_time(val, speed_index, tb) for val in _range]
199
+ speed_index[pair[0] : pair[1]] = get_speed_index(99_999)
195
200
 
196
- if len(args.add_in) > 0:
201
+ for _range in args.add_in:
197
202
  # set to 'video_speed' index
198
- mut_set_range(speed_index, args.add_in, 1.0)
203
+ pair = [parse_time(val, speed_index, tb) for val in _range]
204
+ speed_index[pair[0] : pair[1]] = 1
199
205
 
200
206
  for speed_range in args.set_speed_for_range:
201
- speed = speed_range[0]
202
- _range = list(speed_range[1:])
203
- mut_set_range(speed_index, [_range], get_speed_index(speed))
207
+ start_in = parse_time(speed_range[1], speed_index, tb)
208
+ end_in = parse_time(speed_range[2], speed_index, tb)
209
+ speed_index[start_in:end_in] = get_speed_index(speed_range[0])
204
210
  except CoerceError as e:
205
211
  log.error(e)
206
212
 
@@ -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
@@ -16,21 +16,22 @@ class Ensure:
16
16
  _sr: int
17
17
  temp: str
18
18
  log: Log
19
- labels: list[tuple[FileInfo, int]] = field(default_factory=list)
20
- sub_labels: list[tuple[FileInfo, int]] = field(default_factory=list)
19
+ _audios: list[tuple[FileInfo, int]] = field(default_factory=list)
20
+ _subtitles: list[tuple[FileInfo, int, str]] = field(default_factory=list)
21
21
 
22
22
  def audio(self, src: FileInfo, stream: int) -> str:
23
23
  try:
24
- label = self.labels.index((src, stream))
24
+ label = self._audios.index((src, stream))
25
25
  first_time = False
26
26
  except ValueError:
27
- self.labels.append((src, stream))
28
- label = len(self.labels) - 1
27
+ self._audios.append((src, stream))
28
+ label = len(self._audios) - 1
29
29
  first_time = True
30
30
 
31
31
  out_path = os.path.join(self.temp, f"{label:x}.wav")
32
32
 
33
33
  if first_time:
34
+ self.log.debug(f"Making external audio: {out_path}")
34
35
  self.log.conwrite("Extracting audio")
35
36
 
36
37
  cmd = ["-i", f"{src.path}", "-map", f"0:a:{stream}"]
@@ -39,18 +40,18 @@ class Ensure:
39
40
 
40
41
  return out_path
41
42
 
42
- def subtitle(self, src: FileInfo, stream: int) -> str:
43
+ def subtitle(self, src: FileInfo, stream: int, ext: str) -> str:
43
44
  try:
44
- label = self.sub_labels.index((src, stream))
45
+ self._subtitles.index((src, stream, ext))
45
46
  first_time = False
46
47
  except ValueError:
47
- self.sub_labels.append((src, stream))
48
- label = len(self.sub_labels) - 1
48
+ self._subtitles.append((src, stream, ext))
49
49
  first_time = True
50
50
 
51
- out_path = os.path.join(self.temp, f"{label:x}.vtt")
51
+ out_path = os.path.join(self.temp, f"{stream}s.{ext}")
52
52
 
53
53
  if first_time:
54
+ self.log.debug(f"Making external subtitle: {out_path}")
54
55
  self.log.conwrite("Extracting subtitle")
55
56
  self._ffmpeg.run(["-i", f"{src.path}", "-map", f"0:s:{stream}", out_path])
56
57
 
@@ -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)
@@ -27,9 +27,6 @@ if TYPE_CHECKING:
27
27
  from auto_editor.utils.types import Args
28
28
 
29
29
 
30
- av.logging.set_level(av.logging.PANIC)
31
-
32
-
33
30
  @dataclass(slots=True)
34
31
  class VideoFrame:
35
32
  index: int
@@ -7,6 +7,7 @@ from typing import Any, Literal, TypedDict
7
7
 
8
8
  from auto_editor.ffwrapper import initFileInfo
9
9
  from auto_editor.lang.json import dump
10
+ from auto_editor.make_layers import make_sane_timebase
10
11
  from auto_editor.timeline import v3
11
12
  from auto_editor.utils.func import aspect_ratio
12
13
  from auto_editor.utils.log import Log
@@ -68,6 +69,7 @@ class MediaJson(TypedDict, total=False):
68
69
  subtitle: list[SubtitleJson]
69
70
  container: ContainerJson
70
71
  type: Literal["media", "timeline", "unknown"]
72
+ recommendedTimebase: str
71
73
  version: Literal["v1", "v3"]
72
74
  clips: int
73
75
 
@@ -81,7 +83,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
81
83
 
82
84
  for file in args.input:
83
85
  if not os.path.isfile(file):
84
- log.nofile(file)
86
+ log.error(f"Could not find '{file}'")
85
87
 
86
88
  ext = os.path.splitext(file)[1]
87
89
  if ext == ".json":
@@ -108,6 +110,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
108
110
 
109
111
  file_info[file] = {
110
112
  "type": "media",
113
+ "recommendedTimebase": "30/1",
111
114
  "video": [],
112
115
  "audio": [],
113
116
  "subtitle": [],
@@ -117,6 +120,12 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
117
120
  },
118
121
  }
119
122
 
123
+ if src.videos:
124
+ recTb = make_sane_timebase(src.videos[0].fps)
125
+ file_info[file]["recommendedTimebase"] = (
126
+ f"{recTb.numerator}/{recTb.denominator}"
127
+ )
128
+
120
129
  for track, v in enumerate(src.videos):
121
130
  w, h = v.width, v.height
122
131
 
@@ -1,7 +1,7 @@
1
1
  import sys
2
2
 
3
3
  import av
4
- from av.subtitles.subtitle import SubtitleSet
4
+ from av.subtitles.subtitle import AssSubtitle, TextSubtitle
5
5
 
6
6
 
7
7
  def main(sys_args: list[str] = sys.argv[1:]) -> None:
@@ -10,12 +10,12 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
10
10
  for s in range(len(container.streams.subtitles)):
11
11
  print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
12
12
  for packet in container.demux(subtitles=s):
13
- for item in packet.decode():
14
- if type(item) is SubtitleSet and item:
15
- if item[0].type == b"ass":
16
- print(item[0].ass.decode("utf-8"))
17
- elif item[0].type == b"text":
18
- print(item[0].text)
13
+ for subset in packet.decode():
14
+ for sub in subset.rects:
15
+ if isinstance(sub, AssSubtitle):
16
+ print(sub.ass.decode("utf-8", errors="ignore"))
17
+ elif isinstance(sub, TextSubtitle):
18
+ print(sub.text.decode("utf-8", errors="ignore"))
19
19
  print("------")
20
20
 
21
21
 
@@ -67,9 +67,9 @@ class Bar:
67
67
 
68
68
  if self.machine:
69
69
  index = min(index, self.total)
70
- raw = int(self.begin_time + rate)
70
+ secs_til_eta = round(self.begin_time + rate - time(), 2)
71
71
  print(
72
- f"{self.title}~{index}~{self.total}~{self.begin_time}~{raw}",
72
+ f"{self.title}~{index}~{self.total}~{secs_til_eta}",
73
73
  end="\r",
74
74
  flush=True,
75
75
  )
@@ -2,36 +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")
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
24
+ self.machine = machine
25
+ self.no_color = no_color
26
+ self.start_time = 0 if self.quiet or self.machine else perf_counter()
35
27
 
36
28
  def debug(self, message: object) -> None:
37
29
  if self.is_debug:
@@ -55,17 +47,40 @@ class Log:
55
47
  self.debug(f"Failed to delete temp dir:\n{e}")
56
48
 
57
49
  def conwrite(self, message: str) -> None:
58
- if not self.quiet:
50
+ if self.machine:
51
+ print(message, flush=True)
52
+ elif not self.quiet:
59
53
  buffer = " " * (get_terminal_size().columns - len(message) - 3)
60
54
  sys.stdout.write(f" {message}{buffer}\r")
61
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
+
62
73
  def error(self, message: str | Exception) -> NoReturn:
63
74
  if self.is_debug and isinstance(message, Exception):
64
75
  self.cleanup()
65
76
  raise message
66
77
 
67
78
  self.conwrite("")
68
- 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
+
69
84
  self.cleanup()
70
85
  from platform import system
71
86
 
@@ -78,16 +93,3 @@ class Log:
78
93
  import os
79
94
 
80
95
  os._exit(1)
81
-
82
- def nofile(self, path: str | Path) -> NoReturn:
83
- self.error(f"Could not find '{path}'")
84
-
85
- def warning(self, message: str) -> None:
86
- if not self.quiet:
87
- self.conwrite("")
88
- sys.stderr.write(f"Warning! {message}\n")
89
-
90
- def print(self, message: str) -> None:
91
- if not self.quiet:
92
- self.conwrite("")
93
- 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
@@ -164,8 +164,9 @@ def margin(val: str) -> tuple[str, str]:
164
164
  return vals[0], vals[1]
165
165
 
166
166
 
167
- def time_range(val: str) -> list[str]:
168
- return _comma_coerce("time_range", val, 2)
167
+ def time_range(val: str) -> tuple[str, str]:
168
+ a = _comma_coerce("time_range", val, 2)
169
+ return a[0], a[1]
169
170
 
170
171
 
171
172
  def speed_range(val: str) -> tuple[float, str, str]:
@@ -231,8 +232,8 @@ class Args:
231
232
  extras: str | None = None
232
233
  sn: bool = False
233
234
  no_seek: bool = False
234
- cut_out: list[list[str]] = field(default_factory=list)
235
- add_in: list[list[str]] = field(default_factory=list)
235
+ cut_out: list[tuple[str, str]] = field(default_factory=list)
236
+ add_in: list[tuple[str, str]] = field(default_factory=list)
236
237
  set_speed_for_range: list[tuple[float, str, str]] = field(default_factory=list)
237
238
  frame_rate: Fraction | None = None
238
239
  sample_rate: int | None = None
@@ -81,6 +81,6 @@ def valid_input(inputs: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> list
81
81
  else:
82
82
  if os.path.isdir(my_input):
83
83
  log.error("Input must be a file or a URL, not a directory.")
84
- log.nofile(my_input)
84
+ log.error(f"Could not find '{my_input}'")
85
85
 
86
86
  return new_inputs
@@ -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.19.1
3
+ Version: 24.25.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.0.5
15
+ Requires-Dist: pyav==12.1.0
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
@@ -57,4 +57,5 @@ auto_editor/utils/container.py
57
57
  auto_editor/utils/encoder.py
58
58
  auto_editor/utils/func.py
59
59
  auto_editor/utils/log.py
60
+ auto_editor/utils/subtitle_tools.py
60
61
  auto_editor/utils/types.py
@@ -1,3 +1,3 @@
1
1
  numpy>=1.22.0
2
- pyav==12.0.5
2
+ pyav==12.1.0
3
3
  ae-ffmpeg==1.2.*
@@ -10,7 +10,7 @@ authors = [{ name = "WyattBlue", email = "wyattblue@auto-editor.com" }]
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "numpy>=1.22.0",
13
- "pyav==12.0.5",
13
+ "pyav==12.1.0",
14
14
  "ae-ffmpeg==1.2.*",
15
15
  ]
16
16
  keywords = [
@@ -1,2 +0,0 @@
1
- __version__ = "24.19.1"
2
- version = "24w19a"
File without changes
File without changes