auto-editor 28.0.2__py3-none-any.whl → 29.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {auto_editor-28.0.2.dist-info → auto_editor-29.0.0.dist-info}/METADATA +5 -4
  2. auto_editor-29.0.0.dist-info/RECORD +5 -0
  3. auto_editor-29.0.0.dist-info/top_level.txt +1 -0
  4. auto_editor/__init__.py +0 -1
  5. auto_editor/__main__.py +0 -503
  6. auto_editor/analyze.py +0 -393
  7. auto_editor/cmds/__init__.py +0 -0
  8. auto_editor/cmds/cache.py +0 -69
  9. auto_editor/cmds/desc.py +0 -32
  10. auto_editor/cmds/info.py +0 -213
  11. auto_editor/cmds/levels.py +0 -199
  12. auto_editor/cmds/palet.py +0 -29
  13. auto_editor/cmds/repl.py +0 -113
  14. auto_editor/cmds/subdump.py +0 -72
  15. auto_editor/cmds/test.py +0 -812
  16. auto_editor/edit.py +0 -548
  17. auto_editor/exports/__init__.py +0 -0
  18. auto_editor/exports/fcp11.py +0 -195
  19. auto_editor/exports/fcp7.py +0 -313
  20. auto_editor/exports/json.py +0 -63
  21. auto_editor/exports/shotcut.py +0 -147
  22. auto_editor/ffwrapper.py +0 -187
  23. auto_editor/help.py +0 -223
  24. auto_editor/imports/__init__.py +0 -0
  25. auto_editor/imports/fcp7.py +0 -275
  26. auto_editor/imports/json.py +0 -234
  27. auto_editor/json.py +0 -297
  28. auto_editor/lang/__init__.py +0 -0
  29. auto_editor/lang/libintrospection.py +0 -10
  30. auto_editor/lang/libmath.py +0 -23
  31. auto_editor/lang/palet.py +0 -724
  32. auto_editor/lang/stdenv.py +0 -1184
  33. auto_editor/lib/__init__.py +0 -0
  34. auto_editor/lib/contracts.py +0 -235
  35. auto_editor/lib/data_structs.py +0 -278
  36. auto_editor/lib/err.py +0 -2
  37. auto_editor/make_layers.py +0 -315
  38. auto_editor/preview.py +0 -93
  39. auto_editor/render/__init__.py +0 -0
  40. auto_editor/render/audio.py +0 -517
  41. auto_editor/render/subtitle.py +0 -205
  42. auto_editor/render/video.py +0 -312
  43. auto_editor/timeline.py +0 -331
  44. auto_editor/utils/__init__.py +0 -0
  45. auto_editor/utils/bar.py +0 -142
  46. auto_editor/utils/chunks.py +0 -2
  47. auto_editor/utils/cmdkw.py +0 -206
  48. auto_editor/utils/container.py +0 -102
  49. auto_editor/utils/func.py +0 -128
  50. auto_editor/utils/log.py +0 -124
  51. auto_editor/utils/types.py +0 -277
  52. auto_editor/vanparse.py +0 -313
  53. auto_editor-28.0.2.dist-info/RECORD +0 -56
  54. auto_editor-28.0.2.dist-info/entry_points.txt +0 -6
  55. auto_editor-28.0.2.dist-info/top_level.txt +0 -2
  56. docs/build.py +0 -70
  57. {auto_editor-28.0.2.dist-info → auto_editor-29.0.0.dist-info}/WHEEL +0 -0
  58. {auto_editor-28.0.2.dist-info → auto_editor-29.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,205 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import io
4
- import os
5
- import re
6
- from dataclasses import dataclass
7
- from typing import TYPE_CHECKING
8
-
9
- import bv
10
-
11
- from auto_editor.utils.func import to_timecode
12
-
13
- if TYPE_CHECKING:
14
- from fractions import Fraction
15
-
16
- from auto_editor.timeline import v3
17
- from auto_editor.utils.chunks import Chunks
18
- from auto_editor.utils.log import Log
19
-
20
- Input = bv.container.InputContainer
21
-
22
-
23
- @dataclass(slots=True)
24
- class SerialSub:
25
- start: int
26
- end: int
27
- before: str
28
- middle: str
29
- after: str
30
-
31
-
32
- class SubtitleParser:
33
- def __init__(self, tb: Fraction) -> None:
34
- self.tb = tb
35
- self.contents: list[SerialSub] = []
36
- self.header = ""
37
- self.footer = ""
38
-
39
- @staticmethod
40
- def to_tick(text: str, codec: str, tb: Fraction) -> int:
41
- boxes = text.replace(",", ".").split(":")
42
- assert len(boxes) < 4
43
-
44
- boxes.reverse()
45
- multiply = (1, 60, 3600)
46
- seconds = 0.0
47
- for box, mul in zip(boxes, multiply):
48
- seconds += float(box) * mul
49
-
50
- return round(seconds * tb)
51
-
52
- def parse(self, text: str, codec: str) -> None:
53
- self.codec = codec
54
- self.contents = []
55
-
56
- if codec in {"ass", "ssa"}:
57
- time_code = re.compile(r"(.*)(\d+:\d+:[\d.]+)(.*)(\d+:\d+:[\d.]+)(.*)")
58
- elif codec == "webvtt":
59
- time_code = re.compile(r"()(\d+:[\d.]+)( --> )(\d+:[\d.]+)(\n.*)")
60
- elif codec == "mov_text":
61
- time_code = re.compile(r"()(\d+:\d+:[\d,]+)( --> )(\d+:\d+:[\d,]+)(\n.*)")
62
- else:
63
- raise ValueError(f"codec {codec} not supported.")
64
-
65
- i = 0
66
- for reg in re.finditer(time_code, text):
67
- i += 1
68
- if i == 1:
69
- self.header = text[: reg.span()[0]]
70
-
71
- self.contents.append(
72
- SerialSub(
73
- self.to_tick(reg.group(2), self.codec, self.tb),
74
- self.to_tick(reg.group(4), self.codec, self.tb),
75
- reg.group(1),
76
- reg.group(3),
77
- f"{reg.group(5)}\n",
78
- )
79
- )
80
-
81
- if i == 0:
82
- self.header = ""
83
- self.footer = ""
84
- else:
85
- self.footer = text[reg.span()[1] :]
86
-
87
- def edit(self, chunks: Chunks) -> None:
88
- for cut in reversed(chunks):
89
- the_speed = cut[2]
90
- speed_factor = (
91
- 1 if (the_speed == 0 or the_speed >= 99999) else 1 - (1 / the_speed)
92
- )
93
-
94
- new_content = []
95
- for content in self.contents:
96
- if cut[0] <= content.end and cut[1] > content.start:
97
- diff = int(
98
- (min(cut[1], content.end) - max(cut[0], content.start))
99
- * speed_factor
100
- )
101
- if content.start > cut[0]:
102
- content.start -= diff
103
- content.end -= diff
104
-
105
- content.end -= diff
106
-
107
- elif content.start >= cut[0]:
108
- diff = int((cut[1] - cut[0]) * speed_factor)
109
-
110
- content.start -= diff
111
- content.end -= diff
112
-
113
- if content.start != content.end:
114
- new_content.append(content)
115
-
116
- self.contents = new_content
117
-
118
- def write(self, file_path: str) -> None:
119
- codec = self.codec
120
- with open(file_path, "w", encoding="utf-8") as file:
121
- file.write(self.header)
122
- for c in self.contents:
123
- file.write(
124
- f"{c.before}{to_timecode(c.start / self.tb, codec)}"
125
- + f"{c.middle}{to_timecode(c.end / self.tb, codec)}"
126
- + c.after
127
- + ("\n" if codec == "webvtt" else "")
128
- )
129
- file.write(self.footer)
130
-
131
-
132
- def make_srt(input_: Input, stream: int) -> str:
133
- output_bytes = io.StringIO()
134
- input_stream = input_.streams.subtitles[stream]
135
- assert input_stream.time_base is not None
136
- s = 1
137
- for packet in input_.demux(input_stream):
138
- if packet.dts is None or packet.pts is None or packet.duration is None:
139
- continue
140
-
141
- start_num = packet.pts * input_stream.time_base
142
- start = to_timecode(start_num, "srt")
143
- end = to_timecode(start_num + packet.duration * input_stream.time_base, "srt")
144
-
145
- for sub in packet.decode():
146
- assert isinstance(sub, bv.subtitles.subtitle.AssSubtitle)
147
-
148
- output_bytes.write(f"{s}\n{start} --> {end}\n")
149
- output_bytes.write(sub.dialogue.decode("utf-8", errors="ignore") + "\n\n")
150
- s += 1
151
-
152
- output_bytes.seek(0)
153
- return output_bytes.getvalue()
154
-
155
-
156
- def _ensure(input_: Input, format: str, stream: int) -> str:
157
- output_bytes = io.BytesIO()
158
- output = bv.open(output_bytes, "w", format=format)
159
-
160
- in_stream = input_.streams.subtitles[stream]
161
- out_stream = output.add_stream_from_template(in_stream)
162
-
163
- for packet in input_.demux(in_stream):
164
- if packet.dts is None:
165
- continue
166
- packet.stream = out_stream
167
- output.mux(packet)
168
-
169
- output.close()
170
- output_bytes.seek(0)
171
- return output_bytes.getvalue().decode("utf-8", errors="ignore")
172
-
173
-
174
- def make_new_subtitles(tl: v3, log: Log) -> list[str]:
175
- if tl.v1 is None:
176
- return []
177
-
178
- input_ = bv.open(tl.v1.source.path)
179
- new_paths = []
180
-
181
- for s, sub in enumerate(tl.v1.source.subtitles):
182
- if sub.codec == "mov_text":
183
- continue
184
-
185
- parser = SubtitleParser(tl.tb)
186
- if sub.codec == "ssa":
187
- format = "ass"
188
- elif sub.codec in {"webvtt", "ass"}:
189
- format = sub.codec
190
- else:
191
- log.error(f"Unknown subtitle codec: {sub.codec}")
192
-
193
- if sub.codec == "mov_text":
194
- ret = make_srt(input_, s)
195
- else:
196
- ret = _ensure(input_, format, s)
197
- parser.parse(ret, format)
198
- parser.edit(tl.v1.chunks)
199
-
200
- new_path = os.path.join(log.temp, f"new{s}s.{sub.ext}")
201
- parser.write(new_path)
202
- new_paths.append(new_path)
203
-
204
- input_.close()
205
- return new_paths
@@ -1,312 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING
5
-
6
- import bv
7
- import numpy as np
8
-
9
- from auto_editor.timeline import Clip, TlImage, TlRect
10
- from auto_editor.utils.func import parse_bitrate
11
-
12
- if TYPE_CHECKING:
13
- from collections.abc import Iterator
14
-
15
- from auto_editor.__main__ import Args
16
- from auto_editor.ffwrapper import FileInfo
17
- from auto_editor.timeline import v3
18
- from auto_editor.utils.log import Log
19
-
20
-
21
- @dataclass(slots=True)
22
- class VideoFrame:
23
- index: int
24
- src: FileInfo
25
-
26
-
27
- def make_solid(width: int, height: int, pix_fmt: str, bg: str) -> bv.VideoFrame:
28
- hex_color = bg.lstrip("#").upper()
29
- rgb_color = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
30
-
31
- rgb_array = np.full((height, width, 3), rgb_color, dtype=np.uint8)
32
- rgb_frame = bv.VideoFrame.from_ndarray(rgb_array, format="rgb24")
33
- return rgb_frame.reformat(format=pix_fmt)
34
-
35
-
36
- def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
37
- img_cache = {}
38
- for clip in tl.v:
39
- for obj in clip:
40
- if isinstance(obj, TlImage) and (obj.src, obj.width) not in img_cache:
41
- with bv.open(obj.src.path) as cn:
42
- my_stream = cn.streams.video[0]
43
- for frame in cn.decode(my_stream):
44
- if obj.width != 0:
45
- graph = bv.filter.Graph()
46
- graph.link_nodes(
47
- graph.add_buffer(template=my_stream),
48
- graph.add("scale", f"{obj.width}:-1"),
49
- graph.add("buffersink"),
50
- ).vpush(frame)
51
- frame = graph.vpull()
52
- img_cache[(obj.src, obj.width)] = frame.to_ndarray(
53
- format="rgb24"
54
- )
55
- break
56
- return img_cache
57
-
58
-
59
- def render_av(
60
- output: bv.container.OutputContainer, tl: v3, args: Args, log: Log
61
- ) -> Iterator[tuple[int, bv.VideoFrame]]:
62
- from_ndarray = bv.VideoFrame.from_ndarray
63
-
64
- cns: dict[FileInfo, bv.container.InputContainer] = {}
65
- decoders: dict[FileInfo, Iterator[bv.VideoFrame]] = {}
66
- seek_cost: dict[FileInfo, int] = {}
67
- tous: dict[FileInfo, int] = {}
68
-
69
- pix_fmt = "yuv420p" # Reasonable default
70
- target_fps = tl.tb # Always constant
71
- img_cache = make_image_cache(tl)
72
-
73
- first_src: FileInfo | None = None
74
- for src in tl.sources:
75
- if first_src is None:
76
- first_src = src
77
-
78
- if src not in cns:
79
- cns[src] = bv.open(f"{src.path}")
80
-
81
- for src, cn in cns.items():
82
- if len(cn.streams.video) > 0:
83
- stream = cn.streams.video[0]
84
- stream.thread_type = "AUTO"
85
-
86
- if args.no_seek or stream.average_rate is None or stream.time_base is None:
87
- sc_val = 4294967295 # 2 ** 32 - 1
88
- tou = 0
89
- else:
90
- # Keyframes are usually spread out every 5 seconds or less.
91
- sc_val = int(stream.average_rate * 5)
92
- tou = int(stream.time_base.denominator / stream.average_rate)
93
-
94
- seek_cost[src] = sc_val
95
- tous[src] = tou
96
- decoders[src] = cn.decode(stream)
97
-
98
- if src == first_src and stream.pix_fmt is not None:
99
- pix_fmt = stream.pix_fmt
100
-
101
- log.debug(f"Tous: {tous}")
102
- log.debug(f"Clips: {tl.v}")
103
-
104
- codec = bv.Codec(args.video_codec, "w")
105
-
106
- need_valid_fmt = True
107
- if codec.video_formats is not None:
108
- for video_format in codec.video_formats:
109
- if pix_fmt == video_format.name:
110
- need_valid_fmt = False
111
- break
112
-
113
- if need_valid_fmt:
114
- if codec.canonical_name == "gif":
115
- pix_fmt = "rgb8"
116
- elif codec.canonical_name == "prores":
117
- pix_fmt = "yuv422p10le"
118
- else:
119
- pix_fmt = "yuv420p"
120
-
121
- del codec
122
- output_stream = output.add_stream(args.video_codec, rate=target_fps)
123
- output_stream.options = {"x265-params": "log-level=error"} # type: ignore
124
-
125
- cc = output_stream.codec_context
126
- if args.vprofile is not None:
127
- if args.vprofile.title() not in cc.profiles:
128
- b = " ".join([f'"{x.lower()}"' for x in cc.profiles])
129
- log.error(
130
- f"`{args.vprofile}` is not a valid profile.\nprofiles supported: {b}"
131
- )
132
-
133
- cc.profile = args.vprofile.title()
134
-
135
- yield output_stream # type: ignore
136
- if not isinstance(output_stream, bv.VideoStream):
137
- log.error(f"Not a known video codec: {args.video_codec}")
138
- if src.videos and src.videos[0].lang is not None:
139
- output_stream.metadata["language"] = src.videos[0].lang
140
-
141
- if args.scale == 1.0:
142
- target_width, target_height = tl.res
143
- scale_graph = None
144
- else:
145
- target_width = max(round(tl.res[0] * args.scale), 2)
146
- target_height = max(round(tl.res[1] * args.scale), 2)
147
- scale_graph = bv.filter.Graph()
148
- scale_graph.link_nodes(
149
- scale_graph.add(
150
- "buffer", video_size="1x1", time_base="1/1", pix_fmt=pix_fmt
151
- ),
152
- scale_graph.add("scale", f"{target_width}:{target_height}"),
153
- scale_graph.add("buffersink"),
154
- )
155
-
156
- output_stream.width = target_width
157
- output_stream.height = target_height
158
- output_stream.pix_fmt = pix_fmt
159
- output_stream.framerate = target_fps
160
-
161
- color_range = src.videos[0].color_range
162
- colorspace = src.videos[0].color_space
163
- color_prim = src.videos[0].color_primaries
164
- color_trc = src.videos[0].color_transfer
165
-
166
- if color_range in {1, 2}:
167
- output_stream.color_range = color_range
168
- if colorspace in {0, 1} or (colorspace >= 3 and colorspace < 16):
169
- output_stream.colorspace = colorspace
170
- if color_prim == 1 or (color_prim >= 4 and color_prim < 17):
171
- output_stream.color_primaries = color_prim
172
- if color_trc == 1 or (color_trc >= 4 and color_trc < 22):
173
- output_stream.color_trc = color_trc
174
-
175
- if args.video_bitrate != "auto":
176
- output_stream.bit_rate = parse_bitrate(args.video_bitrate, log)
177
- log.debug(f"video bitrate: {output_stream.bit_rate}")
178
- else:
179
- log.debug(f"[auto] video bitrate: {output_stream.bit_rate}")
180
-
181
- if src is not None and src.videos and (sar := src.videos[0].sar) is not None:
182
- output_stream.sample_aspect_ratio = sar
183
-
184
- # First few frames can have an abnormal keyframe count, so never seek there.
185
- seek = 10
186
- seek_frame = None
187
- frames_saved = 0
188
-
189
- bg = args.background
190
- null_frame = make_solid(target_width, target_height, pix_fmt, bg)
191
- frame_index = -1
192
-
193
- for index in range(tl.end):
194
- obj_list: list[VideoFrame | TlRect | TlImage] = []
195
- for layer in tl.v:
196
- for lobj in layer:
197
- if isinstance(lobj, Clip):
198
- if index >= lobj.start and index < (lobj.start + lobj.dur):
199
- _i = round((lobj.offset + index - lobj.start) * lobj.speed)
200
- obj_list.append(VideoFrame(_i, lobj.src))
201
- elif index >= lobj.start and index < lobj.start + lobj.dur:
202
- obj_list.append(lobj)
203
-
204
- if tl.v1 is not None:
205
- # When there can be valid gaps in the timeline.
206
- frame = null_frame
207
- # else, use the last frame
208
-
209
- for obj in obj_list:
210
- if isinstance(obj, VideoFrame):
211
- my_stream = cns[obj.src].streams.video[0]
212
- if frame_index > obj.index:
213
- log.debug(f"Seek: {frame_index} -> 0")
214
- cns[obj.src].seek(0)
215
- try:
216
- frame = next(decoders[obj.src])
217
- frame_index = round(frame.time * tl.tb)
218
- except StopIteration:
219
- pass
220
-
221
- while frame_index < obj.index:
222
- # Check if skipping ahead is worth it.
223
- if (
224
- obj.index - frame_index > seek_cost[obj.src]
225
- and frame_index > seek
226
- ):
227
- seek = frame_index + (seek_cost[obj.src] // 2)
228
- seek_frame = frame_index
229
- log.debug(f"Seek: {frame_index} -> {obj.index}")
230
- cns[obj.src].seek(obj.index * tous[obj.src], stream=my_stream)
231
-
232
- try:
233
- frame = next(decoders[obj.src])
234
- frame_index = round(frame.time * tl.tb)
235
- except StopIteration:
236
- log.debug(f"No source frame at {index=}. Using null frame")
237
- frame = null_frame
238
- break
239
-
240
- if seek_frame is not None:
241
- log.debug(f"Skipped {frame_index - seek_frame} frame indexes")
242
- frames_saved += frame_index - seek_frame
243
- seek_frame = None
244
- if frame.key_frame:
245
- log.debug(f"Keyframe {frame_index} {frame.pts}")
246
-
247
- if (frame.width, frame.height) != tl.res:
248
- width, height = tl.res
249
- graph = bv.filter.Graph()
250
- graph.link_nodes(
251
- graph.add_buffer(template=my_stream),
252
- graph.add(
253
- "scale",
254
- f"{width}:{height}:force_original_aspect_ratio=decrease:eval=frame",
255
- ),
256
- graph.add("pad", f"{width}:{height}:-1:-1:color={bg}"),
257
- graph.add("buffersink"),
258
- ).vpush(frame)
259
- frame = graph.vpull()
260
- elif isinstance(obj, TlRect):
261
- graph = bv.filter.Graph()
262
- x, y = obj.x, obj.y
263
- graph.link_nodes(
264
- graph.add_buffer(template=my_stream),
265
- graph.add(
266
- "drawbox",
267
- f"x={x}:y={y}:w={obj.width}:h={obj.height}:color={obj.fill}:t=fill",
268
- ),
269
- graph.add("buffersink"),
270
- ).vpush(frame)
271
- frame = graph.vpull()
272
- elif isinstance(obj, TlImage):
273
- img = img_cache[(obj.src, obj.width)]
274
- array = frame.to_ndarray(format="rgb24")
275
-
276
- overlay_h, overlay_w, _ = img.shape
277
- x_pos, y_pos = obj.x, obj.y
278
-
279
- x_start = max(x_pos, 0)
280
- y_start = max(y_pos, 0)
281
- x_end = min(x_pos + overlay_w, frame.width)
282
- y_end = min(y_pos + overlay_h, frame.height)
283
-
284
- # Clip the overlay image to fit into the frame
285
- overlay_x_start = max(-x_pos, 0)
286
- overlay_y_start = max(-y_pos, 0)
287
- overlay_x_end = overlay_w - max((x_pos + overlay_w) - frame.width, 0)
288
- overlay_y_end = overlay_h - max((y_pos + overlay_h) - frame.height, 0)
289
- clipped_overlay = img[
290
- overlay_y_start:overlay_y_end, overlay_x_start:overlay_x_end
291
- ]
292
-
293
- # Create a region of interest (ROI) on the video frame
294
- roi = array[y_start:y_end, x_start:x_end]
295
-
296
- # Blend the overlay image with the ROI based on the opacity
297
- roi = (1 - obj.opacity) * roi + obj.opacity * clipped_overlay # type: ignore
298
- array[y_start:y_end, x_start:x_end] = roi
299
- array = np.clip(array, 0, 255).astype(np.uint8)
300
-
301
- frame = from_ndarray(array, format="rgb24")
302
-
303
- if scale_graph is not None and frame.width != target_width:
304
- scale_graph.vpush(frame)
305
- frame = scale_graph.vpull()
306
-
307
- frame = frame.reformat(format=pix_fmt)
308
- frame.pts = None # type: ignore
309
- frame.time_base = 0 # type: ignore
310
- yield (index, frame)
311
-
312
- log.debug(f"Total frames saved seeking: {frames_saved}")