auto-editor 25.1.0__py3-none-any.whl → 25.3.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.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +5 -0
- auto_editor/analyze.py +17 -16
- auto_editor/edit.py +16 -6
- auto_editor/ffwrapper.py +26 -1
- auto_editor/formats/fcp11.py +24 -27
- auto_editor/formats/utils.py +0 -18
- auto_editor/lang/palet.py +118 -57
- auto_editor/lang/stdenv.py +56 -5
- auto_editor/lib/contracts.py +4 -0
- auto_editor/lib/data_structs.py +4 -2
- auto_editor/output.py +6 -6
- auto_editor/preview.py +2 -2
- auto_editor/render/audio.py +70 -38
- auto_editor/render/video.py +2 -19
- auto_editor/subcommands/levels.py +2 -2
- auto_editor/subcommands/repl.py +2 -3
- auto_editor/subcommands/test.py +2 -1
- auto_editor/timeline.py +46 -7
- auto_editor/utils/bar.py +56 -49
- auto_editor/utils/cmdkw.py +5 -11
- auto_editor/utils/types.py +1 -7
- auto_editor/wavfile.py +25 -16
- {auto_editor-25.1.0.dist-info → auto_editor-25.3.0.dist-info}/METADATA +4 -4
- {auto_editor-25.1.0.dist-info → auto_editor-25.3.0.dist-info}/RECORD +29 -29
- {auto_editor-25.1.0.dist-info → auto_editor-25.3.0.dist-info}/WHEEL +1 -1
- {auto_editor-25.1.0.dist-info → auto_editor-25.3.0.dist-info}/LICENSE +0 -0
- {auto_editor-25.1.0.dist-info → auto_editor-25.3.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-25.1.0.dist-info → auto_editor-25.3.0.dist-info}/top_level.txt +0 -0
auto_editor/lang/stdenv.py
CHANGED
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
|
|
20
20
|
|
21
21
|
|
22
22
|
def make_standard_env() -> dict[str, Any]:
|
23
|
+
import os.path
|
23
24
|
from cmath import sqrt as complex_sqrt
|
24
25
|
from functools import reduce
|
25
26
|
from operator import add, ge, gt, is_, le, lt, mod, mul
|
@@ -135,6 +136,22 @@ def make_standard_env() -> dict[str, Any]:
|
|
135
136
|
Sym("float64"): np.float64,
|
136
137
|
}
|
137
138
|
|
139
|
+
@dataclass(slots=True)
|
140
|
+
class InputPort:
|
141
|
+
name: str
|
142
|
+
port: Any
|
143
|
+
closed: bool
|
144
|
+
|
145
|
+
def close(self) -> None:
|
146
|
+
if not self.closed:
|
147
|
+
self.closed = True
|
148
|
+
self.port.close()
|
149
|
+
|
150
|
+
def __str__(self) -> str:
|
151
|
+
return f"#<input-port:{self.name}>"
|
152
|
+
|
153
|
+
__repr__ = __str__
|
154
|
+
|
138
155
|
@dataclass(slots=True)
|
139
156
|
class OutputPort:
|
140
157
|
name: str
|
@@ -152,6 +169,13 @@ def make_standard_env() -> dict[str, Any]:
|
|
152
169
|
|
153
170
|
__repr__ = __str__
|
154
171
|
|
172
|
+
def initInPort(name: str) -> InputPort | Literal[False]:
|
173
|
+
try:
|
174
|
+
port = open(name, encoding="utf-8")
|
175
|
+
except Exception:
|
176
|
+
return False
|
177
|
+
return InputPort(name, port, False)
|
178
|
+
|
155
179
|
def initOutPort(name: str) -> OutputPort | Literal[False]:
|
156
180
|
try:
|
157
181
|
port = open(name, "w", encoding="utf-8")
|
@@ -198,14 +222,20 @@ def make_standard_env() -> dict[str, Any]:
|
|
198
222
|
parms: list[str] = []
|
199
223
|
for item in node[1]:
|
200
224
|
if type(item) is not Sym:
|
201
|
-
raise MyError(f"{node[0]}: must be an identifier")
|
225
|
+
raise MyError(f"{node[0]}: must be an identifier, got: {item} {type(item)}")
|
202
226
|
|
203
227
|
parms.append(f"{item}")
|
204
228
|
|
205
229
|
return UserProc(env, "", parms, (), node[2:])
|
206
230
|
|
207
231
|
def syn_define(env: Env, node: Node) -> None:
|
232
|
+
if len(node) < 2:
|
233
|
+
raise MyError(f"{node[0]}: too few terms")
|
208
234
|
if len(node) < 3:
|
235
|
+
if type(node[1]) is Sym:
|
236
|
+
raise MyError(f"{node[0]}: what should `{node[1]}` be defined as?")
|
237
|
+
elif type(node[1]) is tuple and len(node[1]) > 0:
|
238
|
+
raise MyError(f"{node[0]}: function `{node[1][0]}` needs a body.")
|
209
239
|
raise MyError(f"{node[0]}: too few terms")
|
210
240
|
|
211
241
|
if type(node[1]) is tuple:
|
@@ -437,6 +467,11 @@ def make_standard_env() -> dict[str, Any]:
|
|
437
467
|
raise MyError(f"{node[0]}: Expected string? got: {print_str(num)}")
|
438
468
|
env[name] += num
|
439
469
|
|
470
|
+
def syn_while(env: Env, node: Node) -> None:
|
471
|
+
while my_eval(env, node[1]) == True:
|
472
|
+
for c in node[2:]:
|
473
|
+
my_eval(env, c)
|
474
|
+
|
440
475
|
def syn_for(env: Env, node: Node) -> None:
|
441
476
|
var, my_iter = check_for_syntax(env, node)
|
442
477
|
|
@@ -808,6 +843,12 @@ def make_standard_env() -> dict[str, Any]:
|
|
808
843
|
return f"{val.real}{join}{val.imag}i"
|
809
844
|
return f"{val}"
|
810
845
|
|
846
|
+
def string_to_number(val) -> float:
|
847
|
+
try:
|
848
|
+
return float(val)
|
849
|
+
except Exception:
|
850
|
+
raise MyError(f"failed to convert {val} to number")
|
851
|
+
|
811
852
|
def palet_join(v: Any, s: str) -> str:
|
812
853
|
try:
|
813
854
|
return s.join(v)
|
@@ -943,6 +984,7 @@ def make_standard_env() -> dict[str, Any]:
|
|
943
984
|
# loops
|
944
985
|
"for": Syntax(syn_for),
|
945
986
|
"for-items": Syntax(syn_for_items),
|
987
|
+
"while": Syntax(syn_while),
|
946
988
|
# contracts
|
947
989
|
"number?": is_num,
|
948
990
|
"real?": is_real,
|
@@ -1102,10 +1144,20 @@ def make_standard_env() -> dict[str, Any]:
|
|
1102
1144
|
),
|
1103
1145
|
),
|
1104
1146
|
# i/o
|
1147
|
+
"file-exists?": Proc("file-exists", os.path.isfile, (1, 1), is_str),
|
1148
|
+
"open-input-file": Proc("open-input-file", initInPort, (1, 1), is_str),
|
1149
|
+
"input-port?": (ip := Contract("input-port?", lambda v: type(v) is InputPort)),
|
1150
|
+
"read-line": Proc("read-line",
|
1151
|
+
lambda f: (r := f.port.readline(), None if r == "" else r.rstrip())[-1],
|
1152
|
+
(1, 1), ip),
|
1105
1153
|
"open-output-file": Proc("open-output-file", initOutPort, (1, 1), is_str),
|
1106
1154
|
"output-port?": (op := Contract("output-port?", lambda v: type(v) is OutputPort)),
|
1107
|
-
"
|
1108
|
-
"
|
1155
|
+
"port?": (port := Contract("port?", orc(ip, op))),
|
1156
|
+
"close-port": Proc("close-port", lambda p: p.close, (1, 1), port),
|
1157
|
+
"closed?": Proc("closed?", lambda o: o.closed, (1, 1), port),
|
1158
|
+
# subprocess
|
1159
|
+
"system": Proc("system", palet_system, (1, 1), is_str),
|
1160
|
+
"system*": Proc("system*", palet_system_star, (1, None), is_str),
|
1109
1161
|
# printing
|
1110
1162
|
"display": Proc("display",
|
1111
1163
|
lambda v, f=None: print(display_str(v), end="", file=f), (1, 2), any_p, op),
|
@@ -1119,10 +1171,9 @@ def make_standard_env() -> dict[str, Any]:
|
|
1119
1171
|
"assert": Proc("assert", palet_assert, (1, 2), any_p, orc(is_str, False)),
|
1120
1172
|
"error": Proc("error", raise_, (1, 1), is_str),
|
1121
1173
|
"sleep": Proc("sleep", sleep, (1, 1), is_int_or_float),
|
1122
|
-
"system": Proc("system", palet_system, (1, 1), is_str),
|
1123
|
-
"system*": Proc("system*", palet_system_star, (1, None), is_str),
|
1124
1174
|
# conversions
|
1125
1175
|
"number->string": Proc("number->string", number_to_string, (1, 1), is_num),
|
1176
|
+
"string->number": Proc("string->number", string_to_number, (1, 1), is_str),
|
1126
1177
|
"string->vector": Proc(
|
1127
1178
|
"string->vector", lambda s: [Char(c) for c in s], (1, 1), is_str
|
1128
1179
|
),
|
auto_editor/lib/contracts.py
CHANGED
@@ -118,6 +118,10 @@ class Proc:
|
|
118
118
|
|
119
119
|
if kws is not None:
|
120
120
|
for key, val in kwargs.items():
|
121
|
+
if key not in kws:
|
122
|
+
raise MyError(
|
123
|
+
f"{self.name} got an unexpected keyword argument: {key}"
|
124
|
+
)
|
121
125
|
check = cont[-1] if kws[key] >= len(cont) else cont[kws[key]]
|
122
126
|
if not check_contract(check, val):
|
123
127
|
exp = f"{check}" if callable(check) else print_str(check)
|
auto_editor/lib/data_structs.py
CHANGED
@@ -54,12 +54,14 @@ class Env:
|
|
54
54
|
|
55
55
|
|
56
56
|
class Sym:
|
57
|
-
__slots__ = ("val", "hash")
|
57
|
+
__slots__ = ("val", "hash", "lineno", "column")
|
58
58
|
|
59
|
-
def __init__(self, val: str):
|
59
|
+
def __init__(self, val: str, lineno: int = -1, column: int = -1):
|
60
60
|
assert isinstance(val, str)
|
61
61
|
self.val = val
|
62
62
|
self.hash = hash(val)
|
63
|
+
self.lineno = lineno
|
64
|
+
self.column = column
|
63
65
|
|
64
66
|
def __str__(self) -> str:
|
65
67
|
return self.val
|
auto_editor/output.py
CHANGED
@@ -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 =
|
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(
|
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
|
-
|
241
|
-
|
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)
|
auto_editor/preview.py
CHANGED
@@ -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
|
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,
|
68
|
+
in_len += Levels(src, tb, initBar("none"), False, log, False).media_length
|
69
69
|
|
70
70
|
out_len = tl.out_len()
|
71
71
|
|
auto_editor/render/audio.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
auto_editor/render/video.py
CHANGED
@@ -57,21 +57,6 @@ allowed_pix_fmt = {
|
|
57
57
|
}
|
58
58
|
|
59
59
|
|
60
|
-
def apply_anchor(x: int, y: int, w: int, h: int, anchor: str) -> tuple[int, int]:
|
61
|
-
if anchor == "ce":
|
62
|
-
x = (x * 2 - w) // 2
|
63
|
-
y = (y * 2 - h) // 2
|
64
|
-
if anchor == "tr":
|
65
|
-
x -= w
|
66
|
-
if anchor == "bl":
|
67
|
-
y -= h
|
68
|
-
if anchor == "br":
|
69
|
-
x -= w
|
70
|
-
y -= h
|
71
|
-
# Use 'tl' by default
|
72
|
-
return x, y
|
73
|
-
|
74
|
-
|
75
60
|
def make_solid(width: int, height: int, pix_fmt: str, bg: str) -> av.VideoFrame:
|
76
61
|
hex_color = bg.lstrip("#").upper()
|
77
62
|
rgb_color = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
@@ -292,7 +277,7 @@ def render_av(
|
|
292
277
|
frame = graph.vpull()
|
293
278
|
elif isinstance(obj, TlRect):
|
294
279
|
graph = av.filter.Graph()
|
295
|
-
x, y =
|
280
|
+
x, y = obj.x, obj.y
|
296
281
|
graph.link_nodes(
|
297
282
|
graph.add_buffer(template=my_stream),
|
298
283
|
graph.add(
|
@@ -307,9 +292,7 @@ def render_av(
|
|
307
292
|
array = frame.to_ndarray(format="rgb24")
|
308
293
|
|
309
294
|
overlay_h, overlay_w, _ = img.shape
|
310
|
-
x_pos, y_pos =
|
311
|
-
obj.x, obj.y, overlay_w, overlay_h, obj.anchor
|
312
|
-
)
|
295
|
+
x_pos, y_pos = obj.x, obj.y
|
313
296
|
|
314
297
|
x_start = max(x_pos, 0)
|
315
298
|
y_start = max(y_pos, 0)
|
@@ -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
|
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 =
|
86
|
+
bar = initBar("none")
|
87
87
|
log = Log(quiet=True)
|
88
88
|
|
89
89
|
sources = [initFileInfo(path, log) for path in args.input]
|
auto_editor/subcommands/repl.py
CHANGED
@@ -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
|
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,
|
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__}")
|
auto_editor/subcommands/test.py
CHANGED
@@ -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], ["-
|
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"])
|
auto_editor/timeline.py
CHANGED
@@ -3,17 +3,20 @@ 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
|
-
from auto_editor.utils.types import
|
9
|
+
from auto_editor.utils.types import color, natural, number, threshold
|
9
10
|
|
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)
|
@@ -88,7 +91,6 @@ class TlImage:
|
|
88
91
|
y: int
|
89
92
|
width: int
|
90
93
|
opacity: float
|
91
|
-
anchor: str
|
92
94
|
|
93
95
|
def as_dict(self) -> dict:
|
94
96
|
return {
|
@@ -100,7 +102,6 @@ class TlImage:
|
|
100
102
|
"y": self.y,
|
101
103
|
"width": self.width,
|
102
104
|
"opacity": self.opacity,
|
103
|
-
"anchor": self.anchor,
|
104
105
|
}
|
105
106
|
|
106
107
|
|
@@ -112,7 +113,6 @@ class TlRect:
|
|
112
113
|
y: int
|
113
114
|
width: int
|
114
115
|
height: int
|
115
|
-
anchor: str
|
116
116
|
fill: str
|
117
117
|
|
118
118
|
def as_dict(self) -> dict:
|
@@ -124,7 +124,6 @@ class TlRect:
|
|
124
124
|
"y": self.y,
|
125
125
|
"width": self.width,
|
126
126
|
"height": self.height,
|
127
|
-
"anchor": self.anchor,
|
128
127
|
"fill": self.fill,
|
129
128
|
}
|
130
129
|
|
@@ -157,7 +156,6 @@ img_builder = pAttrs(
|
|
157
156
|
pAttr("y", Required, is_int, int),
|
158
157
|
pAttr("width", 0, is_nat, natural),
|
159
158
|
pAttr("opacity", 1, is_threshold, threshold),
|
160
|
-
pAttr("anchor", "ce", is_str, anchor),
|
161
159
|
)
|
162
160
|
rect_builder = pAttrs(
|
163
161
|
"rect",
|
@@ -167,7 +165,6 @@ rect_builder = pAttrs(
|
|
167
165
|
pAttr("y", Required, is_int, int),
|
168
166
|
pAttr("width", Required, is_int, int),
|
169
167
|
pAttr("height", Required, is_int, int),
|
170
|
-
pAttr("anchor", "ce", is_str, anchor),
|
171
168
|
pAttr("fill", "#c4c4c4", is_str, color),
|
172
169
|
)
|
173
170
|
visual_objects = {
|
@@ -247,6 +244,13 @@ video\n"""
|
|
247
244
|
for a in aclips:
|
248
245
|
yield a.src
|
249
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
|
+
|
250
254
|
def _duration(self, layer: Any) -> int:
|
251
255
|
total_dur = 0
|
252
256
|
for clips in layer:
|
@@ -282,3 +286,38 @@ video\n"""
|
|
282
286
|
"v": v,
|
283
287
|
"a": a,
|
284
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
|