auto-editor 28.1.0__py3-none-any.whl → 29.0.1__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.
- auto_editor/__init__.py +3 -1
- auto_editor/__main__.py +31 -497
- auto_editor/cli.py +12 -0
- {auto_editor-28.1.0.dist-info → auto_editor-29.0.1.dist-info}/METADATA +5 -6
- auto_editor-29.0.1.dist-info/RECORD +9 -0
- auto_editor-29.0.1.dist-info/entry_points.txt +2 -0
- {auto_editor-28.1.0.dist-info → auto_editor-29.0.1.dist-info}/top_level.txt +0 -1
- auto_editor/analyze.py +0 -393
- auto_editor/cmds/__init__.py +0 -0
- auto_editor/cmds/cache.py +0 -69
- auto_editor/cmds/desc.py +0 -32
- auto_editor/cmds/info.py +0 -213
- auto_editor/cmds/levels.py +0 -199
- auto_editor/cmds/palet.py +0 -29
- auto_editor/cmds/repl.py +0 -113
- auto_editor/cmds/subdump.py +0 -72
- auto_editor/cmds/test.py +0 -816
- auto_editor/edit.py +0 -560
- auto_editor/exports/__init__.py +0 -0
- auto_editor/exports/fcp11.py +0 -195
- auto_editor/exports/fcp7.py +0 -313
- auto_editor/exports/json.py +0 -63
- auto_editor/exports/kdenlive.py +0 -322
- auto_editor/exports/shotcut.py +0 -147
- auto_editor/ffwrapper.py +0 -187
- auto_editor/help.py +0 -224
- auto_editor/imports/__init__.py +0 -0
- auto_editor/imports/fcp7.py +0 -275
- auto_editor/imports/json.py +0 -234
- auto_editor/json.py +0 -297
- auto_editor/lang/__init__.py +0 -0
- auto_editor/lang/libintrospection.py +0 -10
- auto_editor/lang/libmath.py +0 -23
- auto_editor/lang/palet.py +0 -724
- auto_editor/lang/stdenv.py +0 -1179
- auto_editor/lib/__init__.py +0 -0
- auto_editor/lib/contracts.py +0 -235
- auto_editor/lib/data_structs.py +0 -278
- auto_editor/lib/err.py +0 -2
- auto_editor/make_layers.py +0 -315
- auto_editor/preview.py +0 -93
- auto_editor/render/__init__.py +0 -0
- auto_editor/render/audio.py +0 -517
- auto_editor/render/subtitle.py +0 -205
- auto_editor/render/video.py +0 -307
- auto_editor/timeline.py +0 -331
- auto_editor/utils/__init__.py +0 -0
- auto_editor/utils/bar.py +0 -142
- auto_editor/utils/chunks.py +0 -2
- auto_editor/utils/cmdkw.py +0 -206
- auto_editor/utils/container.py +0 -101
- auto_editor/utils/func.py +0 -128
- auto_editor/utils/log.py +0 -126
- auto_editor/utils/types.py +0 -277
- auto_editor/vanparse.py +0 -313
- auto_editor-28.1.0.dist-info/RECORD +0 -57
- auto_editor-28.1.0.dist-info/entry_points.txt +0 -6
- docs/build.py +0 -70
- {auto_editor-28.1.0.dist-info → auto_editor-29.0.1.dist-info}/WHEEL +0 -0
- {auto_editor-28.1.0.dist-info → auto_editor-29.0.1.dist-info}/licenses/LICENSE +0 -0
auto_editor/timeline.py
DELETED
@@ -1,331 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from typing import TYPE_CHECKING
|
5
|
-
|
6
|
-
from auto_editor.ffwrapper import FileInfo, mux
|
7
|
-
from auto_editor.lib.contracts import *
|
8
|
-
from auto_editor.utils.cmdkw import Required, pAttr, pAttrs
|
9
|
-
from auto_editor.utils.types import CoerceError, natural, number, parse_color
|
10
|
-
|
11
|
-
if TYPE_CHECKING:
|
12
|
-
from collections.abc import Iterator
|
13
|
-
from fractions import Fraction
|
14
|
-
from pathlib import Path
|
15
|
-
|
16
|
-
from auto_editor.ffwrapper import FileInfo
|
17
|
-
from auto_editor.utils.chunks import Chunks
|
18
|
-
from auto_editor.utils.log import Log
|
19
|
-
|
20
|
-
|
21
|
-
@dataclass(slots=True)
|
22
|
-
class v1:
|
23
|
-
"""
|
24
|
-
v1 timeline constructor
|
25
|
-
timebase is always the source's average fps
|
26
|
-
|
27
|
-
"""
|
28
|
-
|
29
|
-
source: FileInfo
|
30
|
-
chunks: Chunks
|
31
|
-
|
32
|
-
def as_dict(self) -> dict:
|
33
|
-
return {
|
34
|
-
"version": "1",
|
35
|
-
"source": f"{self.source.path.resolve()}",
|
36
|
-
"chunks": self.chunks,
|
37
|
-
}
|
38
|
-
|
39
|
-
|
40
|
-
@dataclass(slots=True)
|
41
|
-
class Clip:
|
42
|
-
start: int
|
43
|
-
dur: int
|
44
|
-
src: FileInfo
|
45
|
-
offset: int
|
46
|
-
stream: int
|
47
|
-
|
48
|
-
speed: float = 1.0
|
49
|
-
volume: float = 1.0
|
50
|
-
|
51
|
-
def as_dict(self) -> dict:
|
52
|
-
return {
|
53
|
-
"name": "video",
|
54
|
-
"src": self.src,
|
55
|
-
"start": self.start,
|
56
|
-
"dur": self.dur,
|
57
|
-
"offset": self.offset,
|
58
|
-
"speed": self.speed,
|
59
|
-
"stream": self.stream,
|
60
|
-
}
|
61
|
-
|
62
|
-
|
63
|
-
@dataclass(slots=True)
|
64
|
-
class TlImage:
|
65
|
-
start: int
|
66
|
-
dur: int
|
67
|
-
src: FileInfo
|
68
|
-
x: int
|
69
|
-
y: int
|
70
|
-
width: int
|
71
|
-
opacity: float
|
72
|
-
|
73
|
-
def as_dict(self) -> dict:
|
74
|
-
return {
|
75
|
-
"name": "image",
|
76
|
-
"src": self.src,
|
77
|
-
"start": self.start,
|
78
|
-
"dur": self.dur,
|
79
|
-
"x": self.x,
|
80
|
-
"y": self.y,
|
81
|
-
"width": self.width,
|
82
|
-
"opacity": self.opacity,
|
83
|
-
}
|
84
|
-
|
85
|
-
|
86
|
-
@dataclass(slots=True)
|
87
|
-
class TlRect:
|
88
|
-
start: int
|
89
|
-
dur: int
|
90
|
-
x: int
|
91
|
-
y: int
|
92
|
-
width: int
|
93
|
-
height: int
|
94
|
-
fill: str
|
95
|
-
|
96
|
-
def as_dict(self) -> dict:
|
97
|
-
return {
|
98
|
-
"name": "rect",
|
99
|
-
"start": self.start,
|
100
|
-
"dur": self.dur,
|
101
|
-
"x": self.x,
|
102
|
-
"y": self.y,
|
103
|
-
"width": self.width,
|
104
|
-
"height": self.height,
|
105
|
-
"fill": self.fill,
|
106
|
-
}
|
107
|
-
|
108
|
-
|
109
|
-
def threshold(val: str | float) -> float:
|
110
|
-
num = number(val)
|
111
|
-
if num > 1 or num < 0:
|
112
|
-
raise CoerceError(f"'{val}': Threshold must be between 0 and 1 (0%-100%)")
|
113
|
-
return num
|
114
|
-
|
115
|
-
|
116
|
-
video_builder = pAttrs(
|
117
|
-
"video",
|
118
|
-
pAttr("start", Required, is_nat, natural),
|
119
|
-
pAttr("dur", Required, is_nat, natural),
|
120
|
-
pAttr("src", Required, is_str, "source"),
|
121
|
-
pAttr("offset", 0, is_int, natural),
|
122
|
-
pAttr("speed", 1, is_real, number),
|
123
|
-
pAttr("stream", 0, is_nat, natural),
|
124
|
-
)
|
125
|
-
audio_builder = pAttrs(
|
126
|
-
"audio",
|
127
|
-
pAttr("start", Required, is_nat, natural),
|
128
|
-
pAttr("dur", Required, is_nat, natural),
|
129
|
-
pAttr("src", Required, is_str, "source"),
|
130
|
-
pAttr("offset", 0, is_int, natural),
|
131
|
-
pAttr("speed", 1, is_real, number),
|
132
|
-
pAttr("volume", 1, is_threshold, threshold),
|
133
|
-
pAttr("stream", 0, is_nat, natural),
|
134
|
-
)
|
135
|
-
img_builder = pAttrs(
|
136
|
-
"image",
|
137
|
-
pAttr("start", Required, is_nat, natural),
|
138
|
-
pAttr("dur", Required, is_nat, natural),
|
139
|
-
pAttr("src", Required, is_str, "source"),
|
140
|
-
pAttr("x", Required, is_int, int),
|
141
|
-
pAttr("y", Required, is_int, int),
|
142
|
-
pAttr("width", 0, is_nat, natural),
|
143
|
-
pAttr("opacity", 1, is_threshold, threshold),
|
144
|
-
)
|
145
|
-
rect_builder = pAttrs(
|
146
|
-
"rect",
|
147
|
-
pAttr("start", Required, is_nat, natural),
|
148
|
-
pAttr("dur", Required, is_nat, natural),
|
149
|
-
pAttr("x", Required, is_int, int),
|
150
|
-
pAttr("y", Required, is_int, int),
|
151
|
-
pAttr("width", Required, is_int, int),
|
152
|
-
pAttr("height", Required, is_int, int),
|
153
|
-
pAttr("fill", "#c4c4c4", is_str, parse_color),
|
154
|
-
)
|
155
|
-
visual_objects = {
|
156
|
-
"rect": (TlRect, rect_builder),
|
157
|
-
"image": (TlImage, img_builder),
|
158
|
-
"video": (Clip, video_builder),
|
159
|
-
}
|
160
|
-
|
161
|
-
VLayer = list[Clip | TlImage | TlRect]
|
162
|
-
VSpace = list[VLayer]
|
163
|
-
ASpace = list[list[Clip]]
|
164
|
-
|
165
|
-
|
166
|
-
@dataclass(slots=True)
|
167
|
-
class AudioTemplate:
|
168
|
-
lang: str | None
|
169
|
-
|
170
|
-
|
171
|
-
@dataclass(slots=True)
|
172
|
-
class SubtitleTemplate:
|
173
|
-
lang: str | None
|
174
|
-
|
175
|
-
|
176
|
-
@dataclass(slots=True)
|
177
|
-
class Template:
|
178
|
-
sr: int
|
179
|
-
layout: str
|
180
|
-
res: tuple[int, int]
|
181
|
-
audios: list[AudioTemplate]
|
182
|
-
subtitles: list[SubtitleTemplate]
|
183
|
-
|
184
|
-
@classmethod
|
185
|
-
def init(
|
186
|
-
self,
|
187
|
-
src: FileInfo,
|
188
|
-
sr: int | None = None,
|
189
|
-
layout: str | None = None,
|
190
|
-
res: tuple[int, int] | None = None,
|
191
|
-
) -> Template:
|
192
|
-
alist = [AudioTemplate(x.lang) for x in src.audios]
|
193
|
-
slist = [SubtitleTemplate(x.lang) for x in src.subtitles]
|
194
|
-
|
195
|
-
if sr is None:
|
196
|
-
sr = src.get_sr()
|
197
|
-
|
198
|
-
if layout is None:
|
199
|
-
layout = "stereo" if not src.audios else src.audios[0].layout
|
200
|
-
|
201
|
-
if res is None:
|
202
|
-
res = src.get_res()
|
203
|
-
|
204
|
-
return Template(sr, layout, res, alist, slist)
|
205
|
-
|
206
|
-
|
207
|
-
@dataclass
|
208
|
-
class v3:
|
209
|
-
tb: Fraction
|
210
|
-
background: str
|
211
|
-
template: Template
|
212
|
-
v: VSpace
|
213
|
-
a: ASpace
|
214
|
-
v1: v1 | None # Is it v1 compatible (linear and only one source)?
|
215
|
-
|
216
|
-
def __str__(self) -> str:
|
217
|
-
result = f"""
|
218
|
-
global
|
219
|
-
timebase {self.tb}
|
220
|
-
samplerate {self.sr}
|
221
|
-
res {self.res[0]}x{self.res[1]}
|
222
|
-
|
223
|
-
video\n"""
|
224
|
-
|
225
|
-
for i, layer in enumerate(self.v):
|
226
|
-
result += f" v{i} "
|
227
|
-
for obj in layer:
|
228
|
-
if isinstance(obj, Clip):
|
229
|
-
result += (
|
230
|
-
f"[#:start {obj.start} #:dur {obj.dur} #:off {obj.offset}] "
|
231
|
-
)
|
232
|
-
else:
|
233
|
-
result += f"[#:start {obj.start} #:dur {obj.dur}] "
|
234
|
-
result += "\n"
|
235
|
-
|
236
|
-
result += "\naudio\n"
|
237
|
-
for i, alayer in enumerate(self.a):
|
238
|
-
result += f" a{i} "
|
239
|
-
for abj in alayer:
|
240
|
-
result += f"[#:start {abj.start} #:dur {abj.dur} #:off {abj.offset}] "
|
241
|
-
result += "\n"
|
242
|
-
return result
|
243
|
-
|
244
|
-
@property
|
245
|
-
def end(self) -> int:
|
246
|
-
end = 0
|
247
|
-
for vclips in self.v:
|
248
|
-
if vclips:
|
249
|
-
v = vclips[-1]
|
250
|
-
end = max(end, v.start + v.dur)
|
251
|
-
|
252
|
-
for aclips in self.a:
|
253
|
-
if aclips:
|
254
|
-
a = aclips[-1]
|
255
|
-
end = max(end, a.start + a.dur)
|
256
|
-
|
257
|
-
return end
|
258
|
-
|
259
|
-
@property
|
260
|
-
def sources(self) -> Iterator[FileInfo]:
|
261
|
-
for vclips in self.v:
|
262
|
-
for v in vclips:
|
263
|
-
if isinstance(v, Clip):
|
264
|
-
yield v.src
|
265
|
-
for aclips in self.a:
|
266
|
-
for a in aclips:
|
267
|
-
yield a.src
|
268
|
-
|
269
|
-
def unique_sources(self) -> Iterator[FileInfo]:
|
270
|
-
seen = set()
|
271
|
-
for source in self.sources:
|
272
|
-
if source.path not in seen:
|
273
|
-
seen.add(source.path)
|
274
|
-
yield source
|
275
|
-
|
276
|
-
def __len__(self) -> int:
|
277
|
-
result = 0
|
278
|
-
for clips in self.v + self.a:
|
279
|
-
if len(clips) > 0:
|
280
|
-
lastClip = clips[-1]
|
281
|
-
result = max(result, lastClip.start + lastClip.dur)
|
282
|
-
|
283
|
-
return result
|
284
|
-
|
285
|
-
@property
|
286
|
-
def T(self) -> Template:
|
287
|
-
return self.template
|
288
|
-
|
289
|
-
@property
|
290
|
-
def res(self) -> tuple[int, int]:
|
291
|
-
return self.T.res
|
292
|
-
|
293
|
-
@property
|
294
|
-
def sr(self) -> int:
|
295
|
-
return self.T.sr
|
296
|
-
|
297
|
-
|
298
|
-
def make_tracks_dir(tracks_dir: Path) -> None:
|
299
|
-
from os import mkdir
|
300
|
-
from shutil import rmtree
|
301
|
-
|
302
|
-
try:
|
303
|
-
mkdir(tracks_dir)
|
304
|
-
except OSError:
|
305
|
-
rmtree(tracks_dir)
|
306
|
-
mkdir(tracks_dir)
|
307
|
-
|
308
|
-
|
309
|
-
def set_stream_to_0(tl: v3, log: Log) -> None:
|
310
|
-
dir_exists = False
|
311
|
-
cache: dict[Path, FileInfo] = {}
|
312
|
-
|
313
|
-
def make_track(i: int, path: Path) -> FileInfo:
|
314
|
-
nonlocal dir_exists
|
315
|
-
|
316
|
-
fold = path.parent / f"{path.stem}_tracks"
|
317
|
-
if not dir_exists:
|
318
|
-
make_tracks_dir(fold)
|
319
|
-
dir_exists = True
|
320
|
-
|
321
|
-
newtrack = fold / f"{path.stem}_{i}.wav"
|
322
|
-
if newtrack not in cache:
|
323
|
-
mux(path, output=newtrack, stream=i)
|
324
|
-
cache[newtrack] = FileInfo.init(f"{newtrack}", log)
|
325
|
-
return cache[newtrack]
|
326
|
-
|
327
|
-
for alayer in tl.a:
|
328
|
-
for aobj in alayer:
|
329
|
-
if aobj.stream > 0:
|
330
|
-
aobj.src = make_track(aobj.stream, aobj.src.path)
|
331
|
-
aobj.stream = 0
|
auto_editor/utils/__init__.py
DELETED
File without changes
|
auto_editor/utils/bar.py
DELETED
@@ -1,142 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import sys
|
4
|
-
from dataclasses import dataclass
|
5
|
-
from math import floor
|
6
|
-
from shutil import get_terminal_size
|
7
|
-
from time import localtime, time
|
8
|
-
|
9
|
-
from .func import get_stdout_bytes
|
10
|
-
|
11
|
-
|
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
|
42
|
-
|
43
|
-
return Bar(icon, chars, brackets, machine, hide, part_width, ampm, [])
|
44
|
-
|
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]]
|
56
|
-
|
57
|
-
@staticmethod
|
58
|
-
def pretty_time(my_time: float, ampm: bool) -> str:
|
59
|
-
new_time = localtime(my_time)
|
60
|
-
|
61
|
-
hours = new_time.tm_hour
|
62
|
-
minutes = new_time.tm_min
|
63
|
-
|
64
|
-
if ampm:
|
65
|
-
if hours == 0:
|
66
|
-
hours = 12
|
67
|
-
if hours > 12:
|
68
|
-
hours -= 12
|
69
|
-
ampm_marker = "PM" if new_time.tm_hour >= 12 else "AM"
|
70
|
-
return f"{hours:02}:{minutes:02} {ampm_marker}"
|
71
|
-
return f"{hours:02}:{minutes:02}"
|
72
|
-
|
73
|
-
def tick(self, index: float) -> None:
|
74
|
-
if self.hide:
|
75
|
-
return
|
76
|
-
|
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
|
80
|
-
|
81
|
-
if self.machine:
|
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)
|
85
|
-
return
|
86
|
-
|
87
|
-
new_time = self.pretty_time(begin + rate, self.ampm)
|
88
|
-
|
89
|
-
percent = round(progress * 100, 1)
|
90
|
-
p_pad = " " * (4 - len(str(percent)))
|
91
|
-
columns = get_terminal_size().columns
|
92
|
-
bar_len = max(1, columns - len_title - 35)
|
93
|
-
bar_str = self._bar_str(progress, bar_len)
|
94
|
-
|
95
|
-
bar = f" {self.icon}{title} {bar_str} {p_pad}{percent}% ETA {new_time} \r"
|
96
|
-
sys.stdout.write(bar)
|
97
|
-
|
98
|
-
def start(self, total: float, title: str = "Please wait") -> None:
|
99
|
-
len_title = 0
|
100
|
-
in_escape = False
|
101
|
-
|
102
|
-
for char in title:
|
103
|
-
if not in_escape:
|
104
|
-
if char == "\033":
|
105
|
-
in_escape = True
|
106
|
-
else:
|
107
|
-
len_title += 1
|
108
|
-
elif char == "m":
|
109
|
-
in_escape = False
|
110
|
-
|
111
|
-
self.stack.append((title, len_title, total, time()))
|
112
|
-
|
113
|
-
try:
|
114
|
-
self.tick(0)
|
115
|
-
except UnicodeEncodeError:
|
116
|
-
self.icon = "& "
|
117
|
-
self.chars = ("-", "#")
|
118
|
-
self.brackets = ("[", "]")
|
119
|
-
self.part_width = 1
|
120
|
-
|
121
|
-
def _bar_str(self, progress: float, width: int) -> str:
|
122
|
-
whole_width = floor(progress * width)
|
123
|
-
remainder_width = (progress * width) % 1
|
124
|
-
part_width = floor(remainder_width * self.part_width)
|
125
|
-
part_char = self.chars[part_width]
|
126
|
-
|
127
|
-
if width - whole_width - 1 < 0:
|
128
|
-
part_char = ""
|
129
|
-
|
130
|
-
line = (
|
131
|
-
self.brackets[0]
|
132
|
-
+ self.chars[-1] * whole_width
|
133
|
-
+ part_char
|
134
|
-
+ self.chars[0] * (width - whole_width - 1)
|
135
|
-
+ self.brackets[1]
|
136
|
-
)
|
137
|
-
return line
|
138
|
-
|
139
|
-
def end(self) -> None:
|
140
|
-
sys.stdout.write(" " * (get_terminal_size().columns - 2) + "\r")
|
141
|
-
if self.stack:
|
142
|
-
self.stack.pop()
|
auto_editor/utils/chunks.py
DELETED
auto_editor/utils/cmdkw.py
DELETED
@@ -1,206 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from difflib import get_close_matches
|
5
|
-
from typing import TYPE_CHECKING
|
6
|
-
|
7
|
-
from auto_editor.lib.data_structs import Env
|
8
|
-
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
from collections.abc import Callable
|
11
|
-
from typing import Any, Literal
|
12
|
-
|
13
|
-
|
14
|
-
class ParserError(Exception):
|
15
|
-
pass
|
16
|
-
|
17
|
-
|
18
|
-
class Required:
|
19
|
-
pass
|
20
|
-
|
21
|
-
|
22
|
-
@dataclass(slots=True)
|
23
|
-
class pAttr:
|
24
|
-
n: str
|
25
|
-
default: Any
|
26
|
-
contract: Any
|
27
|
-
coerce: Callable[[Any], Any] | Literal["source"] | None = None
|
28
|
-
|
29
|
-
|
30
|
-
class pAttrs:
|
31
|
-
__slots__ = ("name", "attrs")
|
32
|
-
|
33
|
-
def __init__(self, name: str, *attrs: pAttr):
|
34
|
-
self.name = name
|
35
|
-
self.attrs = attrs
|
36
|
-
|
37
|
-
|
38
|
-
class PLexer:
|
39
|
-
__slots__ = ("text", "pos", "char")
|
40
|
-
|
41
|
-
def __init__(self, text: str):
|
42
|
-
self.text = text
|
43
|
-
self.pos: int = 0
|
44
|
-
self.char: str | None = self.text[self.pos] if text else None
|
45
|
-
|
46
|
-
def advance(self) -> None:
|
47
|
-
self.pos += 1
|
48
|
-
self.char = None if self.pos > len(self.text) - 1 else self.text[self.pos]
|
49
|
-
|
50
|
-
def string(self) -> str:
|
51
|
-
result = ""
|
52
|
-
while self.char is not None and self.char != '"':
|
53
|
-
if self.char == "\\":
|
54
|
-
self.advance()
|
55
|
-
if self.char is None:
|
56
|
-
raise ParserError(
|
57
|
-
"Expected character for escape sequence, got end of file."
|
58
|
-
)
|
59
|
-
result += f"\\{self.char}"
|
60
|
-
self.advance()
|
61
|
-
else:
|
62
|
-
result += self.char
|
63
|
-
self.advance()
|
64
|
-
|
65
|
-
self.advance()
|
66
|
-
return f'"{result}"'
|
67
|
-
|
68
|
-
def get_next_token(self) -> str | None:
|
69
|
-
while self.char is not None:
|
70
|
-
if self.char == '"':
|
71
|
-
self.advance()
|
72
|
-
return self.string()
|
73
|
-
|
74
|
-
result = ""
|
75
|
-
while self.char is not None and self.char not in ",":
|
76
|
-
result += self.char
|
77
|
-
self.advance()
|
78
|
-
|
79
|
-
self.advance()
|
80
|
-
return result
|
81
|
-
return None
|
82
|
-
|
83
|
-
|
84
|
-
def parse_with_palet(
|
85
|
-
text: str, build: pAttrs, _env: Env | dict[str, Any]
|
86
|
-
) -> dict[str, Any]:
|
87
|
-
from auto_editor.lang.palet import Lexer, Parser, interpret
|
88
|
-
from auto_editor.lib.data_structs import print_str
|
89
|
-
from auto_editor.lib.err import MyError
|
90
|
-
|
91
|
-
# Positional Arguments
|
92
|
-
# --option 0,end,10,20,20,30,#000, ...
|
93
|
-
# Keyword Arguments
|
94
|
-
# --option start=0,dur=end,x1=10, ...
|
95
|
-
|
96
|
-
KEYWORD_SEP = "="
|
97
|
-
kwargs: dict[str, Any] = {}
|
98
|
-
|
99
|
-
def _norm_name(s: str) -> str:
|
100
|
-
# Python does not allow - in variable names
|
101
|
-
return s.replace("-", "_")
|
102
|
-
|
103
|
-
def go(text: str, c: Any) -> Any:
|
104
|
-
try:
|
105
|
-
env = _env if isinstance(_env, Env) else Env(_env)
|
106
|
-
results = interpret(env, Parser(Lexer(build.name, text)))
|
107
|
-
except MyError as e:
|
108
|
-
raise ParserError(e)
|
109
|
-
|
110
|
-
if not results:
|
111
|
-
raise ParserError("Results must be of length > 0")
|
112
|
-
|
113
|
-
if c(results[-1]) is not True:
|
114
|
-
raise ParserError(
|
115
|
-
f"{build.name}: Expected {c.name}, got {print_str(results[-1])}"
|
116
|
-
)
|
117
|
-
|
118
|
-
return results[-1]
|
119
|
-
|
120
|
-
for attr in build.attrs:
|
121
|
-
kwargs[_norm_name(attr.n)] = attr.default
|
122
|
-
|
123
|
-
allow_positional_args = True
|
124
|
-
|
125
|
-
lexer = PLexer(text)
|
126
|
-
i = 0
|
127
|
-
while (arg := lexer.get_next_token()) is not None:
|
128
|
-
if not arg:
|
129
|
-
continue
|
130
|
-
|
131
|
-
if i + 1 > len(build.attrs):
|
132
|
-
raise ParserError(
|
133
|
-
f"{build.name} has too many arguments, starting with '{arg}'."
|
134
|
-
)
|
135
|
-
|
136
|
-
if KEYWORD_SEP in arg:
|
137
|
-
key, val = arg.split(KEYWORD_SEP, 1)
|
138
|
-
|
139
|
-
allow_positional_args = False
|
140
|
-
found = False
|
141
|
-
|
142
|
-
for attr in build.attrs:
|
143
|
-
if key == attr.n:
|
144
|
-
kwargs[_norm_name(attr.n)] = go(val, attr.contract)
|
145
|
-
found = True
|
146
|
-
break
|
147
|
-
|
148
|
-
if not found:
|
149
|
-
all_names = {attr.n for attr in build.attrs}
|
150
|
-
if matches := get_close_matches(key, all_names):
|
151
|
-
more = f"\n Did you mean:\n {', '.join(matches)}"
|
152
|
-
else:
|
153
|
-
more = (
|
154
|
-
f"\n attributes available:\n {', '.join(all_names)}"
|
155
|
-
)
|
156
|
-
|
157
|
-
raise ParserError(
|
158
|
-
f"{build.name} got an unexpected attribute '{key}'\n{more}"
|
159
|
-
)
|
160
|
-
|
161
|
-
elif allow_positional_args:
|
162
|
-
kwargs[_norm_name(build.attrs[i].n)] = go(arg, build.attrs[i].contract)
|
163
|
-
else:
|
164
|
-
raise ParserError(
|
165
|
-
f"{build.name} positional argument follows keyword argument."
|
166
|
-
)
|
167
|
-
i += 1
|
168
|
-
|
169
|
-
for k, v in kwargs.items():
|
170
|
-
if v is Required:
|
171
|
-
raise ParserError(f"'{k}' must be specified.")
|
172
|
-
|
173
|
-
return kwargs
|
174
|
-
|
175
|
-
|
176
|
-
def parse_method(name: str, text: str) -> tuple[str, list[Any], dict[str, Any]]:
|
177
|
-
from auto_editor.lang.palet import Lexer, Parser
|
178
|
-
|
179
|
-
# Positional Arguments
|
180
|
-
# audio:0.04,0,6,3
|
181
|
-
# Keyword Arguments
|
182
|
-
# audio:threshold=0.04,stream=0,mincut=6,minclip=3
|
183
|
-
|
184
|
-
args: list[Any] = []
|
185
|
-
kwargs: dict[str, Any] = {}
|
186
|
-
|
187
|
-
allow_positional_args = True
|
188
|
-
lexer = PLexer(text)
|
189
|
-
while (arg := lexer.get_next_token()) is not None:
|
190
|
-
if not arg:
|
191
|
-
continue
|
192
|
-
|
193
|
-
if "=" in arg:
|
194
|
-
key, val = arg.split("=", 1)
|
195
|
-
|
196
|
-
result = Parser(Lexer(name, val)).expr()
|
197
|
-
kwargs[key] = result
|
198
|
-
allow_positional_args = False
|
199
|
-
|
200
|
-
elif allow_positional_args:
|
201
|
-
result = Parser(Lexer(name, arg)).expr()
|
202
|
-
args.append(result)
|
203
|
-
else:
|
204
|
-
raise ParserError(f"{name} positional argument follows keyword argument.")
|
205
|
-
|
206
|
-
return name, args, kwargs
|