auto-editor 24.24.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.24.1/auto_editor.egg-info → auto_editor-24.25.1}/PKG-INFO +5 -7
  2. {auto_editor-24.24.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.24.1 → auto_editor-24.25.1}/auto_editor/__main__.py +26 -23
  5. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/analyze.py +54 -19
  6. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/edit.py +4 -14
  7. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/formats/fcp7.py +1 -1
  8. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/make_layers.py +45 -7
  9. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/output.py +11 -10
  10. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/render/subtitle.py +7 -13
  11. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/subcommands/info.py +1 -1
  12. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/log.py +33 -34
  13. auto_editor-24.25.1/auto_editor/utils/subtitle_tools.py +29 -0
  14. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/validate_input.py +1 -1
  15. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/vanparse.py +23 -24
  16. {auto_editor-24.24.1 → auto_editor-24.25.1/auto_editor.egg-info}/PKG-INFO +5 -7
  17. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor.egg-info/SOURCES.txt +1 -0
  18. auto_editor-24.24.1/auto_editor/__init__.py +0 -2
  19. {auto_editor-24.24.1 → auto_editor-24.25.1}/LICENSE +0 -0
  20. {auto_editor-24.24.1 → auto_editor-24.25.1}/ae-ffmpeg/ae_ffmpeg/__init__.py +0 -0
  21. {auto_editor-24.24.1 → auto_editor-24.25.1}/ae-ffmpeg/ae_ffmpeg/py.typed +0 -0
  22. {auto_editor-24.24.1 → auto_editor-24.25.1}/ae-ffmpeg/setup.py +0 -0
  23. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/ffwrapper.py +0 -0
  24. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/formats/__init__.py +0 -0
  25. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/formats/fcp11.py +0 -0
  26. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/formats/json.py +0 -0
  27. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/formats/shotcut.py +0 -0
  28. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/formats/utils.py +0 -0
  29. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/help.py +0 -0
  30. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/lang/__init__.py +0 -0
  31. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/lang/json.py +0 -0
  32. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/lang/libmath.py +0 -0
  33. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/lang/palet.py +0 -0
  34. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/lib/__init__.py +0 -0
  35. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/lib/contracts.py +0 -0
  36. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/lib/data_structs.py +0 -0
  37. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/lib/err.py +0 -0
  38. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/preview.py +0 -0
  39. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/render/__init__.py +0 -0
  40. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/render/audio.py +0 -0
  41. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/render/video.py +0 -0
  42. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/subcommands/__init__.py +0 -0
  43. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/subcommands/desc.py +0 -0
  44. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/subcommands/levels.py +0 -0
  45. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/subcommands/palet.py +0 -0
  46. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/subcommands/repl.py +0 -0
  47. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/subcommands/subdump.py +0 -0
  48. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/subcommands/test.py +0 -0
  49. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/timeline.py +0 -0
  50. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/__init__.py +0 -0
  51. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/bar.py +0 -0
  52. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/chunks.py +0 -0
  53. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/cmdkw.py +0 -0
  54. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/container.py +0 -0
  55. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/encoder.py +0 -0
  56. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/func.py +0 -0
  57. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/utils/types.py +0 -0
  58. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor/wavfile.py +0 -0
  59. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor.egg-info/dependency_links.txt +0 -0
  60. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor.egg-info/entry_points.txt +0 -0
  61. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor.egg-info/requires.txt +0 -0
  62. {auto_editor-24.24.1 → auto_editor-24.25.1}/auto_editor.egg-info/top_level.txt +0 -0
  63. {auto_editor-24.24.1 → auto_editor-24.25.1}/pyproject.toml +0 -0
  64. {auto_editor-24.24.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.24.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
@@ -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,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
 
312
+ import av
313
+
315
314
  print(f"Python Version: {plat.python_version()}")
316
315
  print(f"Platform: {plat.system()} {plat.release()} {plat.machine().lower()}")
317
- print(f"FFmpeg Version: {ffmpeg.version}\nFFmpeg Path: {ffmpeg.path}")
316
+ print(f"PyAV Version: {av.__version__}")
318
317
  print(f"Auto-Editor Version: {auto_editor.version}")
319
- sys.exit()
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
@@ -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")
@@ -213,15 +212,6 @@ def edit_media(
213
212
  ensure = Ensure(ffmpeg, 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:
@@ -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
 
@@ -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
@@ -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
@@ -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)
@@ -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":
@@ -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
@@ -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.24.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
@@ -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,2 +0,0 @@
1
- __version__ = "24.24.1"
2
- version = "24w24a"
File without changes
File without changes