auto-editor 25.2.0__py3-none-any.whl → 25.3.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 +1 -1
- auto_editor/__main__.py +72 -6
- auto_editor/analyze.py +24 -26
- auto_editor/edit.py +37 -14
- auto_editor/ffwrapper.py +55 -67
- auto_editor/formats/fcp11.py +24 -27
- auto_editor/formats/utils.py +0 -18
- auto_editor/output.py +7 -7
- auto_editor/preview.py +2 -2
- auto_editor/render/audio.py +69 -38
- auto_editor/render/subtitle.py +5 -3
- auto_editor/render/video.py +1 -1
- auto_editor/subcommands/levels.py +2 -2
- auto_editor/subcommands/repl.py +2 -3
- auto_editor/subcommands/test.py +12 -4
- auto_editor/timeline.py +45 -0
- auto_editor/utils/bar.py +56 -49
- auto_editor/utils/func.py +0 -26
- auto_editor/utils/types.py +1 -0
- auto_editor/wavfile.py +25 -16
- {auto_editor-25.2.0.dist-info → auto_editor-25.3.1.dist-info}/METADATA +2 -2
- {auto_editor-25.2.0.dist-info → auto_editor-25.3.1.dist-info}/RECORD +27 -29
- {auto_editor-25.2.0.dist-info → auto_editor-25.3.1.dist-info}/WHEEL +1 -1
- docs/build.py +1 -0
- auto_editor/utils/subtitle_tools.py +0 -29
- auto_editor/validate_input.py +0 -88
- {auto_editor-25.2.0.dist-info → auto_editor-25.3.1.dist-info}/LICENSE +0 -0
- {auto_editor-25.2.0.dist-info → auto_editor-25.3.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-25.2.0.dist-info → auto_editor-25.3.1.dist-info}/top_level.txt +0 -0
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
|
-
ffmpeg.run_check_errors(cmd,
|
244
|
+
ffmpeg.run_check_errors(cmd, 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,67 @@ 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
|
+
|
184
|
+
graph = av.filter.Graph()
|
185
|
+
args = [graph.add_abuffer(template=input_stream)]
|
186
|
+
|
187
|
+
if clip.speed != 1:
|
188
|
+
if clip.speed > 10_000:
|
189
|
+
for _ in range(3):
|
190
|
+
args.append(graph.add("atempo", f"{clip.speed ** (1/3)}"))
|
191
|
+
elif clip.speed > 100:
|
192
|
+
for _ in range(2):
|
193
|
+
args.append(graph.add("atempo", f"{clip.speed ** 0.5}"))
|
194
|
+
elif clip.speed >= 0.5:
|
195
|
+
args.append(graph.add("atempo", f"{clip.speed}"))
|
196
|
+
else:
|
197
|
+
start = 0.5
|
198
|
+
while start * 0.5 > clip.speed:
|
199
|
+
start *= 0.5
|
200
|
+
args.append(graph.add("atempo", "0.5"))
|
201
|
+
args.append(graph.add("atempo", f"{clip.speed / start}"))
|
202
|
+
|
203
|
+
if clip.volume != 1:
|
204
|
+
args.append(graph.add("volume", f"{clip.volume}"))
|
205
|
+
|
206
|
+
args.append(graph.add("abuffersink"))
|
207
|
+
graph.link_nodes(*args).configure()
|
208
|
+
|
209
|
+
for frame in input_file.decode(input_stream):
|
210
|
+
graph.push(frame)
|
211
|
+
while True:
|
212
|
+
try:
|
213
|
+
aframe = graph.pull()
|
214
|
+
assert isinstance(aframe, av.AudioFrame)
|
215
|
+
for packet in output_stream.encode(aframe):
|
216
|
+
output_file.mux(packet)
|
217
|
+
except (av.BlockingIOError, av.EOFError):
|
218
|
+
break
|
219
|
+
|
220
|
+
# Flush the stream
|
221
|
+
for packet in output_stream.encode(None):
|
222
|
+
output_file.mux(packet)
|
223
|
+
|
224
|
+
input_file.close()
|
225
|
+
output_file.close()
|
226
|
+
|
227
|
+
output_bytes.seek(0)
|
228
|
+
return read(output_bytes)[1]
|
229
|
+
|
230
|
+
|
168
231
|
def make_new_audio(
|
169
232
|
tl: v3, ensure: Ensure, args: Args, ffmpeg: FFmpeg, bar: Bar, log: Log
|
170
233
|
) -> list[str]:
|
@@ -175,7 +238,6 @@ def make_new_audio(
|
|
175
238
|
|
176
239
|
norm = parse_norm(args.audio_normalize, log)
|
177
240
|
|
178
|
-
af_tick = 0
|
179
241
|
temp = log.temp
|
180
242
|
|
181
243
|
if not tl.a or not tl.a[0]:
|
@@ -191,7 +253,8 @@ def make_new_audio(
|
|
191
253
|
for c, clip in enumerate(layer):
|
192
254
|
if (clip.src, clip.stream) not in samples:
|
193
255
|
audio_path = ensure.audio(clip.src, clip.stream)
|
194
|
-
|
256
|
+
with open(audio_path, "rb") as file:
|
257
|
+
samples[(clip.src, clip.stream)] = read(file)[1]
|
195
258
|
|
196
259
|
if arr is None:
|
197
260
|
leng = max(round((layer[-1].start + layer[-1].dur) * sr / tb), sr // tb)
|
@@ -214,42 +277,10 @@ def make_new_audio(
|
|
214
277
|
if samp_end > len(samp_list):
|
215
278
|
samp_end = len(samp_list)
|
216
279
|
|
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]
|
280
|
+
if clip.speed != 1 or clip.volume != 1:
|
281
|
+
clip_arr = process_audio_clip(clip, samp_list, samp_start, samp_end, sr)
|
240
282
|
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]
|
283
|
+
clip_arr = samp_list[samp_start:samp_end]
|
253
284
|
|
254
285
|
# Mix numpy arrays
|
255
286
|
start = clip.start * sr // tb
|
auto_editor/render/subtitle.py
CHANGED
@@ -112,13 +112,15 @@ class SubtitleParser:
|
|
112
112
|
self.contents = new_content
|
113
113
|
|
114
114
|
def write(self, file_path: str) -> None:
|
115
|
+
codec = self.codec
|
115
116
|
with open(file_path, "w", encoding="utf-8") as file:
|
116
117
|
file.write(self.header)
|
117
118
|
for c in self.contents:
|
118
119
|
file.write(
|
119
|
-
f"{c.before}{to_timecode(c.start / self.tb,
|
120
|
-
f"{c.middle}{to_timecode(c.end / self.tb,
|
121
|
-
|
120
|
+
f"{c.before}{to_timecode(c.start / self.tb, codec)}"
|
121
|
+
+ f"{c.middle}{to_timecode(c.end / self.tb, codec)}"
|
122
|
+
+ c.after
|
123
|
+
+ ("\n" if codec == "webvtt" else "")
|
122
124
|
)
|
123
125
|
file.write(self.footer)
|
124
126
|
|
auto_editor/render/video.py
CHANGED
@@ -339,7 +339,7 @@ def render_av(
|
|
339
339
|
process2.wait()
|
340
340
|
except (OSError, BrokenPipeError):
|
341
341
|
bar.end()
|
342
|
-
ffmpeg.run_check_errors(cmd,
|
342
|
+
ffmpeg.run_check_errors(cmd, True)
|
343
343
|
log.error("FFmpeg Error!")
|
344
344
|
|
345
345
|
log.debug(f"Total frames saved seeking: {frames_saved}")
|
@@ -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
@@ -186,7 +186,7 @@ def main(sys_args: list[str] | None = None):
|
|
186
186
|
"wav/pcm-f32le.wav",
|
187
187
|
"wav/pcm-s32le.wav",
|
188
188
|
"multi-track.mov",
|
189
|
-
"
|
189
|
+
"mov_text.mp4",
|
190
190
|
"testsrc.mkv",
|
191
191
|
)
|
192
192
|
|
@@ -222,7 +222,8 @@ def main(sys_args: list[str] | None = None):
|
|
222
222
|
run.raw(["levels", "resources/new-commentary.mp3"])
|
223
223
|
|
224
224
|
def subdump():
|
225
|
-
run.raw(["subdump", "resources/
|
225
|
+
run.raw(["subdump", "resources/mov_text.mp4"])
|
226
|
+
run.raw(["subdump", "resources/webvtt.mkv"])
|
226
227
|
|
227
228
|
def desc():
|
228
229
|
run.raw(["desc", "example.mp4"])
|
@@ -357,7 +358,13 @@ def main(sys_args: list[str] | None = None):
|
|
357
358
|
run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
|
358
359
|
|
359
360
|
def export_subtitles():
|
360
|
-
cn = fileinfo(run.main(["resources/
|
361
|
+
cn = fileinfo(run.main(["resources/mov_text.mp4"], []))
|
362
|
+
|
363
|
+
assert len(cn.videos) == 1
|
364
|
+
assert len(cn.audios) == 1
|
365
|
+
assert len(cn.subtitles) == 1
|
366
|
+
|
367
|
+
cn = fileinfo(run.main(["resources/webvtt.mkv"], []))
|
361
368
|
|
362
369
|
assert len(cn.videos) == 1
|
363
370
|
assert len(cn.audios) == 1
|
@@ -405,7 +412,8 @@ def main(sys_args: list[str] | None = None):
|
|
405
412
|
test_file = f"resources/{test_name}"
|
406
413
|
results.add(run.main([test_file], []))
|
407
414
|
run.main([test_file], ["--edit", "none"])
|
408
|
-
results.add(run.main([test_file], ["-
|
415
|
+
results.add(run.main([test_file], ["--export", "final-cut-pro:version=10"]))
|
416
|
+
results.add(run.main([test_file], ["--export", "final-cut-pro:version=11"]))
|
409
417
|
results.add(run.main([test_file], ["-exs"]))
|
410
418
|
results.add(run.main([test_file], ["--export_as_clip_sequence"]))
|
411
419
|
run.main([test_file], ["--stats"])
|
auto_editor/timeline.py
CHANGED
@@ -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
|
auto_editor/utils/bar.py
CHANGED
@@ -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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
self.chars: tuple[str, ...] = (" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█")
|
18
|
-
self.brackets = ("|", "|")
|
43
|
+
return Bar(icon, chars, brackets, machine, hide, part_width, ampm, [])
|
19
44
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
66
|
-
|
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,
|
70
|
-
secs_til_eta = round(
|
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(
|
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 - (
|
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}{
|
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
|
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
|
-
|
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()
|
auto_editor/utils/func.py
CHANGED
@@ -10,8 +10,6 @@ if TYPE_CHECKING:
|
|
10
10
|
|
11
11
|
from numpy.typing import NDArray
|
12
12
|
|
13
|
-
from auto_editor.utils.log import Log
|
14
|
-
|
15
13
|
BoolList = NDArray[np.bool_]
|
16
14
|
BoolOperand = Callable[[BoolList, BoolList], BoolList]
|
17
15
|
|
@@ -135,30 +133,6 @@ def human_readable_time(time_in_secs: float) -> str:
|
|
135
133
|
return f"{time_in_secs} {units}"
|
136
134
|
|
137
135
|
|
138
|
-
def open_with_system_default(path: str, log: Log) -> None:
|
139
|
-
import sys
|
140
|
-
from subprocess import run
|
141
|
-
|
142
|
-
if sys.platform == "win32":
|
143
|
-
from os import startfile
|
144
|
-
|
145
|
-
try:
|
146
|
-
startfile(path)
|
147
|
-
except OSError:
|
148
|
-
log.warning("Could not find application to open file.")
|
149
|
-
else:
|
150
|
-
try: # MacOS case
|
151
|
-
run(["open", path])
|
152
|
-
except Exception:
|
153
|
-
try: # WSL2 case
|
154
|
-
run(["cmd.exe", "/C", "start", path])
|
155
|
-
except Exception:
|
156
|
-
try: # Linux case
|
157
|
-
run(["xdg-open", path])
|
158
|
-
except Exception:
|
159
|
-
log.warning("Could not open output file.")
|
160
|
-
|
161
|
-
|
162
136
|
def append_filename(path: str, val: str) -> str:
|
163
137
|
from os.path import splitext
|
164
138
|
|
auto_editor/utils/types.py
CHANGED
@@ -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)
|