auto-editor 25.2.0__tar.gz → 25.3.0__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-25.2.0 → auto_editor-25.3.0}/PKG-INFO +1 -1
  2. auto_editor-25.3.0/auto_editor/__init__.py +1 -0
  3. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/__main__.py +5 -0
  4. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/analyze.py +17 -16
  5. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/edit.py +16 -6
  6. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/ffwrapper.py +26 -1
  7. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/fcp11.py +24 -27
  8. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/utils.py +0 -18
  9. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/output.py +6 -6
  10. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/preview.py +2 -2
  11. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/render/audio.py +70 -38
  12. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/levels.py +2 -2
  13. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/repl.py +2 -3
  14. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/test.py +2 -1
  15. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/timeline.py +45 -0
  16. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/bar.py +56 -49
  17. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/types.py +1 -0
  18. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/wavfile.py +25 -16
  19. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/PKG-INFO +1 -1
  20. auto_editor-25.2.0/auto_editor/__init__.py +0 -1
  21. {auto_editor-25.2.0 → auto_editor-25.3.0}/LICENSE +0 -0
  22. {auto_editor-25.2.0 → auto_editor-25.3.0}/README.md +0 -0
  23. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/__init__.py +0 -0
  24. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/fcp7.py +0 -0
  25. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/json.py +0 -0
  26. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/shotcut.py +0 -0
  27. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/help.py +0 -0
  28. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/__init__.py +0 -0
  29. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/json.py +0 -0
  30. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/libintrospection.py +0 -0
  31. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/libmath.py +0 -0
  32. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/palet.py +0 -0
  33. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/stdenv.py +0 -0
  34. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lib/__init__.py +0 -0
  35. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lib/contracts.py +0 -0
  36. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lib/data_structs.py +0 -0
  37. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lib/err.py +0 -0
  38. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/make_layers.py +0 -0
  39. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/render/__init__.py +0 -0
  40. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/render/subtitle.py +0 -0
  41. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/render/video.py +0 -0
  42. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/__init__.py +0 -0
  43. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/desc.py +0 -0
  44. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/info.py +0 -0
  45. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/palet.py +0 -0
  46. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/subdump.py +0 -0
  47. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/__init__.py +0 -0
  48. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/chunks.py +0 -0
  49. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/cmdkw.py +0 -0
  50. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/container.py +0 -0
  51. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/encoder.py +0 -0
  52. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/func.py +0 -0
  53. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/log.py +0 -0
  54. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/subtitle_tools.py +0 -0
  55. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/validate_input.py +0 -0
  56. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/vanparse.py +0 -0
  57. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/SOURCES.txt +0 -0
  58. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/dependency_links.txt +0 -0
  59. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/entry_points.txt +0 -0
  60. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/requires.txt +0 -0
  61. {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/top_level.txt +0 -0
  62. {auto_editor-25.2.0 → auto_editor-25.3.0}/docs/build.py +0 -0
  63. {auto_editor-25.2.0 → auto_editor-25.3.0}/pyproject.toml +0 -0
  64. {auto_editor-25.2.0 → auto_editor-25.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 25.2.0
3
+ Version: 25.3.0
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
6
  License: Unlicense
@@ -0,0 +1 @@
1
+ __version__ = "25.3.0"
@@ -254,6 +254,11 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
254
254
  flag=True,
255
255
  help="Disable the inclusion of subtitle streams in the output file",
256
256
  )
257
+ parser.add_argument(
258
+ "-dn",
259
+ flag=True,
260
+ help="Disable the inclusion of data streams in the output file",
261
+ )
257
262
  parser.add_argument(
258
263
  "--extras",
259
264
  metavar="CMD",
@@ -19,6 +19,7 @@ from auto_editor.utils.subtitle_tools import convert_ass_to_text
19
19
  if TYPE_CHECKING:
20
20
  from collections.abc import Iterator
21
21
  from fractions import Fraction
22
+ from pathlib import Path
22
23
  from typing import Any
23
24
 
24
25
  from numpy.typing import NDArray
@@ -70,15 +71,6 @@ def mut_remove_large(
70
71
  active = False
71
72
 
72
73
 
73
- def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
74
- key = f"{tag}:{tb}:"
75
- for k, v in obj.items():
76
- key += f"{k}={v},"
77
-
78
- key = key[:-1] # remove unnecessary char
79
- return key
80
-
81
-
82
74
  def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[np.float32]:
83
75
  fifo = AudioFifo()
84
76
  try:
@@ -122,7 +114,7 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
122
114
 
123
115
  prev_frame = None
124
116
  current_frame = None
125
- total_pixels = src.videos[0].width * src.videos[0].height
117
+ total_pixels = None
126
118
  index = 0
127
119
  prev_index = -1
128
120
 
@@ -140,10 +132,13 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
140
132
  continue
141
133
 
142
134
  graph.push(unframe)
143
- frame = graph.pull()
135
+ frame = graph.vpull()
144
136
  assert frame.time is not None
145
137
  index = round(frame.time * tb)
146
138
 
139
+ if total_pixels is None:
140
+ total_pixels = frame.width * frame.height
141
+
147
142
  current_frame = frame.to_ndarray()
148
143
  if prev_frame is None:
149
144
  value = np.float32(0.0)
@@ -161,6 +156,12 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
161
156
  container.close()
162
157
 
163
158
 
159
+ def obj_tag(path: Path, kind: str, tb: Fraction, obj: dict[str, Any]) -> str:
160
+ mod_time = int(path.stat().st_mtime)
161
+ key = f"{path.name}:{mod_time:x}:{kind}:{tb}:"
162
+ return key + ",".join(f"{v}" for v in obj.values())
163
+
164
+
164
165
  @dataclass(slots=True)
165
166
  class Levels:
166
167
  src: FileInfo
@@ -201,7 +202,7 @@ class Levels:
201
202
  def all(self) -> NDArray[np.bool_]:
202
203
  return np.zeros(self.media_length, dtype=np.bool_)
203
204
 
204
- def read_cache(self, tag: str, obj: dict[str, Any]) -> None | np.ndarray:
205
+ def read_cache(self, kind: str, obj: dict[str, Any]) -> None | np.ndarray:
205
206
  if self.no_cache:
206
207
  return None
207
208
 
@@ -213,14 +214,14 @@ class Levels:
213
214
  self.log.debug(e)
214
215
  return None
215
216
 
216
- key = f"{self.src.path}:{obj_tag(tag, self.tb, obj)}"
217
+ key = obj_tag(self.src.path, kind, self.tb, obj)
217
218
  if key not in npzfile.files:
218
219
  return None
219
220
 
220
221
  self.log.debug("Using cache")
221
222
  return npzfile[key]
222
223
 
223
- def cache(self, arr: np.ndarray, tag: str, obj: dict[str, Any]) -> np.ndarray:
224
+ def cache(self, arr: np.ndarray, kind: str, obj: dict[str, Any]) -> np.ndarray:
224
225
  if self.no_cache:
225
226
  return arr
226
227
 
@@ -228,8 +229,8 @@ class Levels:
228
229
  if not os.path.exists(workdur):
229
230
  os.mkdir(workdur)
230
231
 
231
- tag = obj_tag(tag, self.tb, obj)
232
- np.savez(os.path.join(workdur, "cache.npz"), **{f"{self.src.path}:{tag}": arr})
232
+ key = obj_tag(self.src.path, kind, self.tb, obj)
233
+ np.savez(os.path.join(workdur, "cache.npz"), **{key: arr})
233
234
 
234
235
  return arr
235
236
 
@@ -11,7 +11,7 @@ from auto_editor.render.audio import make_new_audio
11
11
  from auto_editor.render.subtitle import make_new_subtitles
12
12
  from auto_editor.render.video import render_av
13
13
  from auto_editor.timeline import v1, v3
14
- from auto_editor.utils.bar import Bar
14
+ from auto_editor.utils.bar import initBar
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
@@ -125,7 +125,9 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
125
125
  "default": pAttrs("default"),
126
126
  "premiere": pAttrs("premiere", name_attr),
127
127
  "resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
128
- "final-cut-pro": pAttrs("final-cut-pro", name_attr),
128
+ "final-cut-pro": pAttrs(
129
+ "final-cut-pro", name_attr, pAttr("version", 11, is_int)
130
+ ),
129
131
  "resolve": pAttrs("resolve", name_attr),
130
132
  "shotcut": pAttrs("shotcut"),
131
133
  "json": pAttrs("json", pAttr("api", 3, is_int)),
@@ -146,7 +148,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
146
148
 
147
149
 
148
150
  def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
149
- bar = Bar(args.progress)
151
+ bar = initBar(args.progress)
150
152
  tl = None
151
153
 
152
154
  if paths:
@@ -232,11 +234,19 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
232
234
  fcp7_write_xml(export_ops["name"], output, is_resolve, tl, log)
233
235
  return
234
236
 
235
- if export in ("final-cut-pro", "resolve"):
237
+ if export == "final-cut-pro":
236
238
  from auto_editor.formats.fcp11 import fcp11_write_xml
237
239
 
238
- is_resolve = export.startswith("resolve")
239
- fcp11_write_xml(export_ops["name"], ffmpeg, output, is_resolve, tl, log)
240
+ ver = export_ops["version"]
241
+ fcp11_write_xml(export_ops["name"], ver, output, False, tl, log)
242
+ return
243
+
244
+ if export == "resolve":
245
+ from auto_editor.formats.fcp11 import fcp11_write_xml
246
+ from auto_editor.timeline import set_stream_to_0
247
+
248
+ set_stream_to_0(tl, log)
249
+ fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
240
250
  return
241
251
 
242
252
  if export == "shotcut":
@@ -127,6 +127,31 @@ class FFmpeg:
127
127
  return output
128
128
 
129
129
 
130
+ def mux(input: Path, output: Path, stream: int, codec: str | None = None) -> None:
131
+ input_container = av.open(input, "r")
132
+ output_container = av.open(output, "w")
133
+
134
+ input_audio_stream = input_container.streams.audio[stream]
135
+
136
+ if codec is None:
137
+ codec = "pcm_s16le"
138
+
139
+ output_audio_stream = output_container.add_stream(codec)
140
+ assert isinstance(output_audio_stream, av.audio.AudioStream)
141
+
142
+ for frame in input_container.decode(input_audio_stream):
143
+ packet = output_audio_stream.encode(frame)
144
+ if packet:
145
+ output_container.mux(packet)
146
+
147
+ packet = output_audio_stream.encode(None)
148
+ if packet:
149
+ output_container.mux(packet)
150
+
151
+ output_container.close()
152
+ input_container.close()
153
+
154
+
130
155
  @dataclass(slots=True, frozen=True)
131
156
  class VideoStream:
132
157
  width: int
@@ -269,7 +294,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
269
294
 
270
295
  desc = cont.metadata.get("description", None)
271
296
  bitrate = 0 if cont.bit_rate is None else cont.bit_rate
272
- dur = 0 if cont.duration is None else cont.duration / 1_000_000
297
+ dur = 0 if cont.duration is None else cont.duration / av.time_base
273
298
 
274
299
  cont.close()
275
300
 
@@ -3,17 +3,15 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any, cast
4
4
  from xml.etree.ElementTree import Element, ElementTree, SubElement, indent
5
5
 
6
- from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
7
-
8
- from .utils import make_tracks_dir
9
-
10
6
  if TYPE_CHECKING:
11
7
  from collections.abc import Sequence
12
8
  from fractions import Fraction
13
9
 
10
+ from auto_editor.ffwrapper import FileInfo
14
11
  from auto_editor.timeline import TlAudio, TlVideo, v3
15
12
  from auto_editor.utils.log import Log
16
13
 
14
+
17
15
  """
18
16
  Export a FCPXML 11 file readable with Final Cut Pro 10.6.8 or later.
19
17
 
@@ -54,7 +52,7 @@ def make_name(src: FileInfo, tb: Fraction) -> str:
54
52
 
55
53
 
56
54
  def fcp11_write_xml(
57
- group_name: str, ffmpeg: FFmpeg, output: str, resolve: bool, tl: v3, log: Log
55
+ group_name: str, version: int, output: str, resolve: bool, tl: v3, log: Log
58
56
  ) -> None:
59
57
  def fraction(val: int) -> str:
60
58
  if val == 0:
@@ -68,23 +66,17 @@ def fcp11_write_xml(
68
66
  src_dur = int(src.duration * tl.tb)
69
67
  tl_dur = src_dur if resolve else tl.out_len()
70
68
 
71
- all_srcs: list[FileInfo] = [src]
72
- all_refs: list[str] = ["r2"]
73
- if resolve and len(src.audios) > 1:
74
- fold = make_tracks_dir(src)
75
-
76
- for i in range(1, len(src.audios)):
77
- newtrack = fold / f"{i}.wav"
78
- ffmpeg.run(
79
- ["-i", f"{src.path.resolve()}", "-map", f"0:a:{i}", f"{newtrack}"]
80
- )
81
- all_srcs.append(initFileInfo(f"{newtrack}", log))
82
- all_refs.append(f"r{(i + 1) * 2}")
69
+ if version == 11:
70
+ ver_str = "1.11"
71
+ elif version == 10:
72
+ ver_str = "1.10"
73
+ else:
74
+ log.error(f"Unknown final cut pro version: {version}")
83
75
 
84
- fcpxml = Element("fcpxml", version="1.10" if resolve else "1.11")
76
+ fcpxml = Element("fcpxml", version=ver_str)
85
77
  resources = SubElement(fcpxml, "resources")
86
78
 
87
- for i, one_src in enumerate(all_srcs):
79
+ for i, one_src in enumerate(tl.unique_sources()):
88
80
  SubElement(
89
81
  resources,
90
82
  "format",
@@ -126,13 +118,6 @@ def fcp11_write_xml(
126
118
  )
127
119
  spine = SubElement(sequence, "spine")
128
120
 
129
- if tl.v and tl.v[0]:
130
- clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
131
- elif tl.a and tl.a[0]:
132
- clips = tl.a[0]
133
- else:
134
- clips = []
135
-
136
121
  def make_clip(ref: str, clip: TlVideo | TlAudio) -> None:
137
122
  clip_properties = {
138
123
  "name": proj_name,
@@ -157,7 +142,19 @@ def fcp11_write_xml(
157
142
  interp="smooth2",
158
143
  )
159
144
 
160
- for my_ref in all_refs:
145
+ if tl.v and tl.v[0]:
146
+ clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
147
+ elif tl.a and tl.a[0]:
148
+ clips = tl.a[0]
149
+ else:
150
+ clips = []
151
+
152
+ all_refs: list[str] = ["r2"]
153
+ if resolve:
154
+ for i in range(1, len(tl.a)):
155
+ all_refs.append(f"r{(i + 1) * 2}")
156
+
157
+ for my_ref in reversed(all_refs):
161
158
  for clip in clips:
162
159
  make_clip(my_ref, clip)
163
160
 
@@ -4,9 +4,6 @@ from typing import TYPE_CHECKING
4
4
  from xml.etree.ElementTree import Element
5
5
 
6
6
  if TYPE_CHECKING:
7
- from pathlib import Path
8
-
9
- from auto_editor.ffwrapper import FileInfo
10
7
  from auto_editor.utils.log import Log
11
8
 
12
9
 
@@ -19,21 +16,6 @@ def show(ele: Element, limit: int, depth: int = 0) -> None:
19
16
  show(child, limit, depth + 1)
20
17
 
21
18
 
22
- def make_tracks_dir(src: FileInfo) -> Path:
23
- from os import mkdir
24
- from shutil import rmtree
25
-
26
- fold = src.path.parent / f"{src.path.stem}_tracks"
27
-
28
- try:
29
- mkdir(fold)
30
- except OSError:
31
- rmtree(fold)
32
- mkdir(fold)
33
-
34
- return fold
35
-
36
-
37
19
  class Validator:
38
20
  def __init__(self, log: Log):
39
21
  self.log = log
@@ -46,9 +46,9 @@ class Ensure:
46
46
  astream = in_container.streams.audio[stream]
47
47
 
48
48
  if astream.duration is None or astream.time_base is None:
49
- dur = 1
49
+ dur = 1.0
50
50
  else:
51
- dur = int(astream.duration * astream.time_base)
51
+ dur = float(astream.duration * astream.time_base)
52
52
 
53
53
  bar.start(dur, "Extracting audio")
54
54
 
@@ -58,8 +58,8 @@ class Ensure:
58
58
 
59
59
  resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate)
60
60
  for i, frame in enumerate(in_container.decode(astream)):
61
- if i % 1500 == 0:
62
- bar.tick(0 if frame.time is None else frame.time)
61
+ if i % 1500 == 0 and frame.time is not None:
62
+ bar.tick(frame.time)
63
63
 
64
64
  for new_frame in resampler.resample(frame):
65
65
  for packet in output_astream.encode(new_frame):
@@ -237,8 +237,8 @@ def mux_quality_media(
237
237
  if s_tracks > 0:
238
238
  cmd.extend(["-map", "0:t?"]) # Add input attachments to output.
239
239
 
240
- # This was causing a crash for 'example.mp4 multi-track.mov'
241
- # cmd.extend(["-map", "0:d?"])
240
+ if not args.dn:
241
+ cmd.extend(["-map", "0:d?"])
242
242
 
243
243
  cmd.append(output_path)
244
244
  ffmpeg.run_check_errors(cmd, log, path=output_path)
@@ -7,7 +7,7 @@ from typing import TextIO
7
7
 
8
8
  from auto_editor.analyze import Levels
9
9
  from auto_editor.timeline import v3
10
- from auto_editor.utils.bar import Bar
10
+ from auto_editor.utils.bar import initBar
11
11
  from auto_editor.utils.func import to_timecode
12
12
  from auto_editor.utils.log import Log
13
13
 
@@ -65,7 +65,7 @@ def preview(tl: v3, log: Log) -> None:
65
65
 
66
66
  in_len = 0
67
67
  for src in all_sources:
68
- in_len += Levels(src, tb, Bar("none"), False, log, False).media_length
68
+ in_len += Levels(src, tb, initBar("none"), False, log, False).media_length
69
69
 
70
70
  out_len = tl.out_len()
71
71
 
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import io
3
4
  from pathlib import Path
4
5
  from platform import system
5
6
  from subprocess import PIPE
6
7
 
8
+ import av
7
9
  import numpy as np
8
10
 
9
11
  from auto_editor.ffwrapper import FFmpeg, FileInfo
@@ -12,7 +14,7 @@ from auto_editor.lang.palet import env
12
14
  from auto_editor.lib.contracts import andc, between_c, is_int_or_float
13
15
  from auto_editor.lib.err import MyError
14
16
  from auto_editor.output import Ensure
15
- from auto_editor.timeline import v3
17
+ from auto_editor.timeline import TlAudio, v3
16
18
  from auto_editor.utils.bar import Bar
17
19
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
18
20
  from auto_editor.utils.log import Log
@@ -165,6 +167,68 @@ def apply_audio_normalization(
165
167
  ffmpeg.run(["-i", f"{pre_master}"] + cmd + [f"{path}"])
166
168
 
167
169
 
170
+ def process_audio_clip(
171
+ clip: TlAudio, samp_list: AudioData, samp_start: int, samp_end: int, sr: int
172
+ ) -> AudioData:
173
+ input_buffer = io.BytesIO()
174
+ write(input_buffer, sr, samp_list[samp_start:samp_end])
175
+ input_buffer.seek(0)
176
+
177
+ input_file = av.open(input_buffer, "r")
178
+ input_stream = input_file.streams.audio[0]
179
+
180
+ output_bytes = io.BytesIO()
181
+ output_file = av.open(output_bytes, mode="w", format="wav")
182
+ output_stream = output_file.add_stream("pcm_s16le", rate=sr)
183
+ assert isinstance(output_stream, av.audio.AudioStream)
184
+
185
+ graph = av.filter.Graph()
186
+ args = [graph.add_abuffer(template=input_stream)]
187
+
188
+ if clip.speed != 1:
189
+ if clip.speed > 10_000:
190
+ for _ in range(3):
191
+ args.append(graph.add("atempo", f"{clip.speed ** (1/3)}"))
192
+ elif clip.speed > 100:
193
+ for _ in range(2):
194
+ args.append(graph.add("atempo", f"{clip.speed ** 0.5}"))
195
+ elif clip.speed >= 0.5:
196
+ args.append(graph.add("atempo", f"{clip.speed}"))
197
+ else:
198
+ start = 0.5
199
+ while start * 0.5 > clip.speed:
200
+ start *= 0.5
201
+ args.append(graph.add("atempo", "0.5"))
202
+ args.append(graph.add("atempo", f"{clip.speed / start}"))
203
+
204
+ if clip.volume != 1:
205
+ args.append(graph.add("volume", f"{clip.volume}"))
206
+
207
+ args.append(graph.add("abuffersink"))
208
+ graph.link_nodes(*args).configure()
209
+
210
+ for frame in input_file.decode(input_stream):
211
+ graph.push(frame)
212
+ while True:
213
+ try:
214
+ aframe = graph.pull()
215
+ assert isinstance(aframe, av.audio.AudioFrame)
216
+ for packet in output_stream.encode(aframe):
217
+ output_file.mux(packet)
218
+ except (av.BlockingIOError, av.EOFError):
219
+ break
220
+
221
+ # Flush the stream
222
+ for packet in output_stream.encode(None):
223
+ output_file.mux(packet)
224
+
225
+ input_file.close()
226
+ output_file.close()
227
+
228
+ output_bytes.seek(0)
229
+ return read(output_bytes)[1]
230
+
231
+
168
232
  def make_new_audio(
169
233
  tl: v3, ensure: Ensure, args: Args, ffmpeg: FFmpeg, bar: Bar, log: Log
170
234
  ) -> list[str]:
@@ -175,7 +239,6 @@ def make_new_audio(
175
239
 
176
240
  norm = parse_norm(args.audio_normalize, log)
177
241
 
178
- af_tick = 0
179
242
  temp = log.temp
180
243
 
181
244
  if not tl.a or not tl.a[0]:
@@ -191,7 +254,8 @@ def make_new_audio(
191
254
  for c, clip in enumerate(layer):
192
255
  if (clip.src, clip.stream) not in samples:
193
256
  audio_path = ensure.audio(clip.src, clip.stream)
194
- samples[(clip.src, clip.stream)] = read(audio_path)[1]
257
+ with open(audio_path, "rb") as file:
258
+ samples[(clip.src, clip.stream)] = read(file)[1]
195
259
 
196
260
  if arr is None:
197
261
  leng = max(round((layer[-1].start + layer[-1].dur) * sr / tb), sr // tb)
@@ -214,42 +278,10 @@ def make_new_audio(
214
278
  if samp_end > len(samp_list):
215
279
  samp_end = len(samp_list)
216
280
 
217
- filters: list[str] = []
218
-
219
- if clip.speed != 1:
220
- if clip.speed > 10_000:
221
- filters.extend([f"atempo={clip.speed}^.33333"] * 3)
222
- elif clip.speed > 100:
223
- filters.extend(
224
- [f"atempo=sqrt({clip.speed})", f"atempo=sqrt({clip.speed})"]
225
- )
226
- elif clip.speed >= 0.5:
227
- filters.append(f"atempo={clip.speed}")
228
- else:
229
- start = 0.5
230
- while start * 0.5 > clip.speed:
231
- start *= 0.5
232
- filters.append("atempo=0.5")
233
- filters.append(f"atempo={clip.speed / start}")
234
-
235
- if clip.volume != 1:
236
- filters.append(f"volume={clip.volume}")
237
-
238
- if not filters:
239
- clip_arr = samp_list[samp_start:samp_end]
281
+ if clip.speed != 1 or clip.volume != 1:
282
+ clip_arr = process_audio_clip(clip, samp_list, samp_start, samp_end, sr)
240
283
  else:
241
- af = Path(temp, f"af{af_tick}.wav")
242
- af_out = Path(temp, f"af{af_tick}_out.wav")
243
-
244
- # Windows can't replace a file that's already in use, so we have to
245
- # cycle through file names.
246
- af_tick = (af_tick + 1) % 3
247
-
248
- with open(af, "wb") as fid:
249
- write(fid, sr, samp_list[samp_start:samp_end])
250
-
251
- ffmpeg.run(["-i", f"{af}", "-af", ",".join(filters), f"{af_out}"])
252
- clip_arr = read(f"{af_out}")[1]
284
+ clip_arr = samp_list[samp_start:samp_end]
253
285
 
254
286
  # Mix numpy arrays
255
287
  start = clip.start * sr // tb
@@ -11,7 +11,7 @@ from auto_editor.analyze import LevelError, Levels, iter_audio, iter_motion
11
11
  from auto_editor.ffwrapper import initFileInfo
12
12
  from auto_editor.lang.palet import env
13
13
  from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
14
- from auto_editor.utils.bar import Bar
14
+ from auto_editor.utils.bar import initBar
15
15
  from auto_editor.utils.cmdkw import (
16
16
  ParserError,
17
17
  Required,
@@ -83,7 +83,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
83
83
  parser = levels_options(ArgumentParser("levels"))
84
84
  args = parser.parse_args(LevelArgs, sys_args)
85
85
 
86
- bar = Bar("none")
86
+ bar = initBar("none")
87
87
  log = Log(quiet=True)
88
88
 
89
89
  sources = [initFileInfo(path, log) for path in args.input]
@@ -11,7 +11,7 @@ from auto_editor.lang.palet import ClosingError, Lexer, Parser, env, interpret
11
11
  from auto_editor.lang.stdenv import make_standard_env
12
12
  from auto_editor.lib.data_structs import print_str
13
13
  from auto_editor.lib.err import MyError
14
- from auto_editor.utils.bar import Bar
14
+ from auto_editor.utils.bar import initBar
15
15
  from auto_editor.utils.log import Log
16
16
  from auto_editor.utils.types import frame_rate
17
17
  from auto_editor.vanparse import ArgumentParser
@@ -64,9 +64,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
64
64
  sources = [initFileInfo(path, log) for path in args.input]
65
65
  src = sources[0]
66
66
  tb = src.get_fps() if args.timebase is None else args.timebase
67
- bar = Bar("modern")
68
67
  env["timebase"] = tb
69
- env["@levels"] = Levels(src, tb, bar, False, log, strict)
68
+ env["@levels"] = Levels(src, tb, initBar("modern"), False, log, strict)
70
69
 
71
70
  env.update(make_standard_env())
72
71
  print(f"Auto-Editor {auto_editor.__version__}")
@@ -405,7 +405,8 @@ def main(sys_args: list[str] | None = None):
405
405
  test_file = f"resources/{test_name}"
406
406
  results.add(run.main([test_file], []))
407
407
  run.main([test_file], ["--edit", "none"])
408
- results.add(run.main([test_file], ["-exf"]))
408
+ results.add(run.main([test_file], ["--export", "final-cut-pro:version=10"]))
409
+ results.add(run.main([test_file], ["--export", "final-cut-pro:version=11"]))
409
410
  results.add(run.main([test_file], ["-exs"]))
410
411
  results.add(run.main([test_file], ["--export_as_clip_sequence"]))
411
412
  run.main([test_file], ["--stats"])
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from auto_editor.ffwrapper import initFileInfo, mux
6
7
  from auto_editor.lib.contracts import *
7
8
  from auto_editor.utils.cmdkw import Required, pAttr, pAttrs
8
9
  from auto_editor.utils.types import color, natural, number, threshold
@@ -10,10 +11,12 @@ from auto_editor.utils.types import color, natural, number, threshold
10
11
  if TYPE_CHECKING:
11
12
  from collections.abc import Iterator
12
13
  from fractions import Fraction
14
+ from pathlib import Path
13
15
  from typing import Any
14
16
 
15
17
  from auto_editor.ffwrapper import FileInfo
16
18
  from auto_editor.utils.chunks import Chunks
19
+ from auto_editor.utils.log import Log
17
20
 
18
21
 
19
22
  @dataclass(slots=True)
@@ -241,6 +244,13 @@ video\n"""
241
244
  for a in aclips:
242
245
  yield a.src
243
246
 
247
+ def unique_sources(self) -> Iterator[FileInfo]:
248
+ seen = set()
249
+ for source in self.sources:
250
+ if source.path not in seen:
251
+ seen.add(source.path)
252
+ yield source
253
+
244
254
  def _duration(self, layer: Any) -> int:
245
255
  total_dur = 0
246
256
  for clips in layer:
@@ -276,3 +286,38 @@ video\n"""
276
286
  "v": v,
277
287
  "a": a,
278
288
  }
289
+
290
+
291
+ def make_tracks_dir(path: Path) -> Path:
292
+ from os import mkdir
293
+ from shutil import rmtree
294
+
295
+ tracks_dir = path.parent / f"{path.stem}_tracks"
296
+
297
+ try:
298
+ mkdir(tracks_dir)
299
+ except OSError:
300
+ rmtree(tracks_dir)
301
+ mkdir(tracks_dir)
302
+
303
+ return tracks_dir
304
+
305
+
306
+ def set_stream_to_0(tl: v3, log: Log) -> None:
307
+ src = tl.src
308
+ assert src is not None
309
+ fold = make_tracks_dir(src.path)
310
+ cache: dict[Path, FileInfo] = {}
311
+
312
+ def make_track(i: int, path: Path) -> FileInfo:
313
+ newtrack = fold / f"{path.stem}_{i}.wav"
314
+ if newtrack not in cache:
315
+ mux(path, output=newtrack, stream=i)
316
+ cache[newtrack] = initFileInfo(f"{newtrack}", log)
317
+ return cache[newtrack]
318
+
319
+ for alayer in tl.a:
320
+ for aobj in alayer:
321
+ if aobj.stream > 0:
322
+ aobj.src = make_track(aobj.stream, aobj.src.path)
323
+ aobj.stream = 0
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
+ from dataclasses import dataclass
4
5
  from math import floor
5
6
  from shutil import get_terminal_size
6
7
  from time import localtime, time
@@ -8,39 +9,50 @@ from time import localtime, time
8
9
  from .func import get_stdout_bytes
9
10
 
10
11
 
11
- class Bar:
12
- def __init__(self, bar_type: str) -> None:
13
- self.machine = False
14
- self.hide = False
12
+ def initBar(bar_type: str) -> Bar:
13
+ icon = "⏳"
14
+ chars: tuple[str, ...] = (" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█")
15
+ brackets = ("|", "|")
16
+ machine = hide = False
17
+
18
+ if bar_type == "classic":
19
+ icon = "⏳"
20
+ chars = ("░", "█")
21
+ brackets = ("[", "]")
22
+ if bar_type == "ascii":
23
+ icon = "& "
24
+ chars = ("-", "#")
25
+ brackets = ("[", "]")
26
+ if bar_type == "machine":
27
+ machine = True
28
+ if bar_type == "none":
29
+ hide = True
30
+
31
+ part_width = len(chars) - 1
32
+
33
+ ampm = True
34
+ if sys.platform == "darwin" and bar_type in ("modern", "classic", "ascii"):
35
+ try:
36
+ date_format = get_stdout_bytes(
37
+ ["defaults", "read", "com.apple.menuextra.clock", "Show24Hour"]
38
+ )
39
+ ampm = date_format == b"0\n"
40
+ except FileNotFoundError:
41
+ pass
15
42
 
16
- self.icon = "⏳"
17
- self.chars: tuple[str, ...] = (" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█")
18
- self.brackets = ("|", "|")
43
+ return Bar(icon, chars, brackets, machine, hide, part_width, ampm, [])
19
44
 
20
- if bar_type == "classic":
21
- self.icon = "⏳"
22
- self.chars = ("░", "█")
23
- self.brackets = ("[", "]")
24
- if bar_type == "ascii":
25
- self.icon = "& "
26
- self.chars = ("-", "#")
27
- self.brackets = ("[", "]")
28
- if bar_type == "machine":
29
- self.machine = True
30
- if bar_type == "none":
31
- self.hide = True
32
-
33
- self.part_width = len(self.chars) - 1
34
-
35
- self.ampm = True
36
- if sys.platform == "darwin" and bar_type in ("modern", "classic", "ascii"):
37
- try:
38
- date_format = get_stdout_bytes(
39
- ["defaults", "read", "com.apple.menuextra.clock", "Show24Hour"]
40
- )
41
- self.ampm = date_format == b"0\n"
42
- except FileNotFoundError:
43
- pass
45
+
46
+ @dataclass(slots=True)
47
+ class Bar:
48
+ icon: str
49
+ chars: tuple[str, ...]
50
+ brackets: tuple[str, str]
51
+ machine: bool
52
+ hide: bool
53
+ part_width: int
54
+ ampm: bool
55
+ stack: list[tuple[str, int, float, float]]
44
56
 
45
57
  @staticmethod
46
58
  def pretty_time(my_time: float, ampm: bool) -> str:
@@ -62,28 +74,25 @@ class Bar:
62
74
  if self.hide:
63
75
  return
64
76
 
65
- progress = 0.0 if self.total == 0 else min(1, max(0, index / self.total))
66
- rate = 0.0 if progress == 0 else (time() - self.begin_time) / progress
77
+ title, len_title, total, begin = self.stack[-1]
78
+ progress = 0.0 if total == 0 else min(1, max(0, index / total))
79
+ rate = 0.0 if progress == 0 else (time() - begin) / progress
67
80
 
68
81
  if self.machine:
69
- index = min(index, self.total)
70
- secs_til_eta = round(self.begin_time + rate - time(), 2)
71
- print(
72
- f"{self.title}~{index}~{self.total}~{secs_til_eta}",
73
- end="\r",
74
- flush=True,
75
- )
82
+ index = min(index, total)
83
+ secs_til_eta = round(begin + rate - time(), 2)
84
+ print(f"{title}~{index}~{total}~{secs_til_eta}", end="\r", flush=True)
76
85
  return
77
86
 
78
- new_time = self.pretty_time(self.begin_time + rate, self.ampm)
87
+ new_time = self.pretty_time(begin + rate, self.ampm)
79
88
 
80
89
  percent = round(progress * 100, 1)
81
90
  p_pad = " " * (4 - len(str(percent)))
82
91
  columns = get_terminal_size().columns
83
- bar_len = max(1, columns - (self.len_title + 32))
92
+ bar_len = max(1, columns - (len_title + 32))
84
93
  bar_str = self._bar_str(progress, bar_len)
85
94
 
86
- bar = f" {self.icon}{self.title} {bar_str} {p_pad}{percent}% ETA {new_time}"
95
+ bar = f" {self.icon}{title} {bar_str} {p_pad}{percent}% ETA {new_time}"
87
96
 
88
97
  if len(bar) > columns - 2:
89
98
  bar = bar[: columns - 2]
@@ -93,10 +102,7 @@ class Bar:
93
102
  sys.stdout.write(bar + "\r")
94
103
 
95
104
  def start(self, total: float, title: str = "Please wait") -> None:
96
- self.title = title
97
- self.len_title = len(title)
98
- self.total = total
99
- self.begin_time = time()
105
+ self.stack.append((title, len(title), total, time()))
100
106
 
101
107
  try:
102
108
  self.tick(0)
@@ -124,6 +130,7 @@ class Bar:
124
130
  )
125
131
  return line
126
132
 
127
- @staticmethod
128
- def end() -> None:
133
+ def end(self) -> None:
129
134
  sys.stdout.write(" " * (get_terminal_size().columns - 2) + "\r")
135
+ if self.stack:
136
+ self.stack.pop()
@@ -224,6 +224,7 @@ class Args:
224
224
  scale: float = 1.0
225
225
  extras: str | None = None
226
226
  sn: bool = False
227
+ dn: bool = False
227
228
  no_seek: bool = False
228
229
  cut_out: list[tuple[str, str]] = field(default_factory=list)
229
230
  add_in: list[tuple[str, str]] = field(default_factory=list)
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import io
4
4
  import struct
5
5
  import sys
6
- from typing import Literal
6
+ from typing import TYPE_CHECKING, Literal
7
7
 
8
8
  import numpy as np
9
9
 
@@ -15,13 +15,17 @@ AudioData = np.memmap | np.ndarray
15
15
  Endian = Literal[">", "<"] # Big Endian, Little Endian
16
16
  ByteOrd = Literal["big", "little"]
17
17
 
18
+ if TYPE_CHECKING:
19
+ Reader = io.BufferedReader | io.BytesIO
20
+ Writer = io.BufferedWriter | io.BytesIO
21
+
18
22
 
19
23
  class WavError(Exception):
20
24
  pass
21
25
 
22
26
 
23
27
  def _read_fmt_chunk(
24
- fid: io.BufferedReader, bytes_order: ByteOrd
28
+ fid: Reader, bytes_order: ByteOrd
25
29
  ) -> tuple[int, int, int, int, int]:
26
30
  size = int.from_bytes(fid.read(4), bytes_order)
27
31
 
@@ -69,7 +73,7 @@ def _read_fmt_chunk(
69
73
 
70
74
 
71
75
  def _read_data_chunk(
72
- fid: io.BufferedReader,
76
+ fid: Reader,
73
77
  format_tag: int,
74
78
  channels: int,
75
79
  bit_depth: int,
@@ -114,16 +118,22 @@ def _read_data_chunk(
114
118
  else:
115
119
  n_samples = (size - 1) // block_align
116
120
 
117
- data = np.memmap(
118
- fid, dtype=dtype, mode="c", offset=fid.tell(), shape=(n_samples, channels)
119
- )
120
- fid.seek(size, 1)
121
+ if isinstance(fid, io.BufferedReader):
122
+ data: AudioData = np.memmap(
123
+ fid, dtype=dtype, mode="c", offset=fid.tell(), shape=(n_samples, channels)
124
+ )
125
+ fid.seek(size, 1)
126
+ else:
127
+ bytes_per_sample = np.dtype(dtype).itemsize
128
+ buffer = fid.read(n_samples * channels * bytes_per_sample)
129
+ data = np.frombuffer(buffer, dtype=dtype).reshape((n_samples, channels))
130
+
121
131
  _handle_pad_byte(fid, size)
122
132
 
123
133
  return data
124
134
 
125
135
 
126
- def _skip_unknown_chunk(fid: io.BufferedReader, en: Endian) -> None:
136
+ def _skip_unknown_chunk(fid: Reader, en: Endian) -> None:
127
137
  data = fid.read(4)
128
138
 
129
139
  if len(data) == 4:
@@ -140,7 +150,7 @@ def _skip_unknown_chunk(fid: io.BufferedReader, en: Endian) -> None:
140
150
  )
141
151
 
142
152
 
143
- def _read_rf64_chunk(fid: io.BufferedReader) -> tuple[int, int, Endian]:
153
+ def _read_rf64_chunk(fid: Reader) -> tuple[int, int, Endian]:
144
154
  # https://tech.ebu.ch/docs/tech/tech3306v1_0.pdf
145
155
  # https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
146
156
 
@@ -171,7 +181,7 @@ def _read_rf64_chunk(fid: io.BufferedReader) -> tuple[int, int, Endian]:
171
181
  return data_size, file_size, en
172
182
 
173
183
 
174
- def _read_riff_chunk(sig: bytes, fid: io.BufferedReader) -> tuple[None, int, Endian]:
184
+ def _read_riff_chunk(sig: bytes, fid: Reader) -> tuple[None, int, Endian]:
175
185
  en: Endian = "<" if sig == b"RIFF" else ">"
176
186
  bytes_order: ByteOrd = "big" if en == ">" else "little"
177
187
 
@@ -184,14 +194,12 @@ def _read_riff_chunk(sig: bytes, fid: io.BufferedReader) -> tuple[None, int, End
184
194
  return None, file_size, en
185
195
 
186
196
 
187
- def _handle_pad_byte(fid: io.BufferedReader, size: int) -> None:
197
+ def _handle_pad_byte(fid: Reader, size: int) -> None:
188
198
  if size % 2 == 1:
189
199
  fid.seek(1, 1)
190
200
 
191
201
 
192
- def read(filename: str) -> tuple[int, AudioData]:
193
- fid = open(filename, "rb")
194
-
202
+ def read(fid: Reader) -> tuple[int, AudioData]:
195
203
  file_sig = fid.read(4)
196
204
  if file_sig in (b"RIFF", b"RIFX"):
197
205
  data_size, file_size, en = _read_riff_chunk(file_sig, fid)
@@ -241,7 +249,7 @@ def read(filename: str) -> tuple[int, AudioData]:
241
249
  raise WavError("Found no data")
242
250
 
243
251
 
244
- def write(fid: io.BufferedWriter, sr: int, arr: np.ndarray) -> None:
252
+ def write(fid: Writer, sr: int, arr: np.ndarray) -> None:
245
253
  channels = 1 if arr.ndim == 1 else arr.shape[1]
246
254
  bit_depth = arr.dtype.itemsize * 8
247
255
  block_align = channels * (bit_depth // 8)
@@ -290,7 +298,8 @@ def main() -> None:
290
298
  with open("test.wav", "wb") as file:
291
299
  write(file, 48_000, data)
292
300
 
293
- read_sr, read_data = read("test.wav")
301
+ with open("test.wav", "rb") as file:
302
+ read_sr, read_data = read(file)
294
303
 
295
304
  assert read_sr == 48_000
296
305
  assert np.array_equal(data, read_data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: auto-editor
3
- Version: 25.2.0
3
+ Version: 25.3.0
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
6
  License: Unlicense
@@ -1 +0,0 @@
1
- __version__ = "25.2.0"
File without changes
File without changes
File without changes
File without changes