auto-editor 26.0.1__py3-none-any.whl → 26.1.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 +30 -32
- auto_editor/analyze.py +101 -68
- auto_editor/edit.py +5 -5
- auto_editor/ffwrapper.py +0 -28
- auto_editor/help.py +14 -16
- auto_editor/make_layers.py +3 -1
- auto_editor/render/audio.py +13 -45
- auto_editor/render/subtitle.py +1 -1
- auto_editor/subcommands/cache.py +69 -0
- auto_editor/subcommands/info.py +2 -0
- auto_editor/subcommands/levels.py +14 -3
- auto_editor/utils/log.py +8 -1
- auto_editor/utils/types.py +1 -2
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.1.dist-info}/METADATA +3 -3
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.1.dist-info}/RECORD +21 -20
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.1.dist-info}/WHEEL +1 -1
- docs/build.py +11 -4
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.1.dist-info}/LICENSE +0 -0
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.0.1.dist-info → auto_editor-26.1.1.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "26.
|
1
|
+
__version__ = "26.1.1"
|
auto_editor/__main__.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
|
3
|
+
import platform as plat
|
3
4
|
import re
|
4
5
|
import sys
|
5
6
|
from os import environ
|
@@ -7,8 +8,6 @@ from os.path import exists, isdir, isfile, lexists, splitext
|
|
7
8
|
from subprocess import run
|
8
9
|
|
9
10
|
import auto_editor
|
10
|
-
from auto_editor.edit import edit_media
|
11
|
-
from auto_editor.ffwrapper import FFmpeg
|
12
11
|
from auto_editor.utils.func import get_stdout
|
13
12
|
from auto_editor.utils.log import Log
|
14
13
|
from auto_editor.utils.types import (
|
@@ -34,13 +33,12 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
34
33
|
"-m",
|
35
34
|
type=margin,
|
36
35
|
metavar="LENGTH",
|
37
|
-
help='Set sections near "loud" as "loud" too if section is less than LENGTH away
|
36
|
+
help='Set sections near "loud" as "loud" too if section is less than LENGTH away',
|
38
37
|
)
|
39
38
|
parser.add_argument(
|
40
|
-
"--edit-based-on",
|
41
39
|
"--edit",
|
42
40
|
metavar="METHOD",
|
43
|
-
help="
|
41
|
+
help="Set an expression which determines how to make auto edits",
|
44
42
|
)
|
45
43
|
parser.add_argument(
|
46
44
|
"--silent-speed",
|
@@ -148,7 +146,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
148
146
|
"--output",
|
149
147
|
"-o",
|
150
148
|
metavar="FILE",
|
151
|
-
help="Set the name/path of the new output file
|
149
|
+
help="Set the name/path of the new output file",
|
152
150
|
)
|
153
151
|
parser.add_argument(
|
154
152
|
"--player", "-p", metavar="CMD", help="Set player to open output media files"
|
@@ -161,11 +159,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
161
159
|
metavar="PATH",
|
162
160
|
help="Set where the temporary directory is located",
|
163
161
|
)
|
164
|
-
parser.add_argument(
|
165
|
-
"--ffmpeg-location",
|
166
|
-
metavar="PATH",
|
167
|
-
help="Set a custom path to the ffmpeg location",
|
168
|
-
)
|
169
162
|
parser.add_text("Display Options:")
|
170
163
|
parser.add_argument(
|
171
164
|
"--progress",
|
@@ -241,11 +234,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
241
234
|
flag=True,
|
242
235
|
help="Disable the inclusion of data streams in the output file",
|
243
236
|
)
|
244
|
-
parser.add_argument(
|
245
|
-
"--extras",
|
246
|
-
metavar="CMD",
|
247
|
-
help="Add extra options for ffmpeg. Must be in quotes",
|
248
|
-
)
|
249
237
|
parser.add_argument(
|
250
238
|
"--config", flag=True, help="When set, look for `config.pal` and run it"
|
251
239
|
)
|
@@ -256,7 +244,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
256
244
|
return parser
|
257
245
|
|
258
246
|
|
259
|
-
def download_video(my_input: str, args: Args,
|
247
|
+
def download_video(my_input: str, args: Args, log: Log) -> str:
|
260
248
|
log.conwrite("Downloading video...")
|
261
249
|
|
262
250
|
def get_domain(url: str) -> str:
|
@@ -272,18 +260,15 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
|
272
260
|
else:
|
273
261
|
output_format = args.output_format
|
274
262
|
|
275
|
-
|
276
|
-
|
277
|
-
cmd = ["--ffmpeg-location", ffmpeg.get_path("yt-dlp", log)]
|
278
|
-
|
263
|
+
cmd = []
|
279
264
|
if download_format is not None:
|
280
265
|
cmd.extend(["-f", download_format])
|
281
266
|
|
282
267
|
cmd.extend(["-o", output_format, my_input])
|
283
|
-
|
284
268
|
if args.yt_dlp_extras is not None:
|
285
269
|
cmd.extend(args.yt_dlp_extras.split(" "))
|
286
270
|
|
271
|
+
yt_dlp_path = args.yt_dlp_location
|
287
272
|
try:
|
288
273
|
location = get_stdout(
|
289
274
|
[yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
|
@@ -301,7 +286,16 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
|
301
286
|
|
302
287
|
|
303
288
|
def main() -> None:
|
304
|
-
subcommands = (
|
289
|
+
subcommands = (
|
290
|
+
"test",
|
291
|
+
"info",
|
292
|
+
"levels",
|
293
|
+
"subdump",
|
294
|
+
"desc",
|
295
|
+
"repl",
|
296
|
+
"palet",
|
297
|
+
"cache",
|
298
|
+
)
|
305
299
|
|
306
300
|
if len(sys.argv) > 1 and sys.argv[1] in subcommands:
|
307
301
|
obj = __import__(
|
@@ -326,6 +320,7 @@ def main() -> None:
|
|
326
320
|
({"--export-as-json"}, ["--export", "json"]),
|
327
321
|
({"--export-as-clip-sequence", "-excs"}, ["--export", "clip-sequence"]),
|
328
322
|
({"--keep-tracks-seperate"}, ["--keep-tracks-separate"]),
|
323
|
+
({"--edit-based-on"}, ["--edit"]),
|
329
324
|
],
|
330
325
|
)
|
331
326
|
|
@@ -334,15 +329,17 @@ def main() -> None:
|
|
334
329
|
return
|
335
330
|
|
336
331
|
if args.debug and not args.input:
|
337
|
-
|
332
|
+
print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
|
333
|
+
print(f"Python: {plat.python_version()}")
|
338
334
|
|
339
|
-
|
335
|
+
try:
|
336
|
+
import av
|
340
337
|
|
341
|
-
|
338
|
+
license = av._core.library_meta["libavcodec"]["license"]
|
339
|
+
print(f"PyAV: {av.__version__} ({license})")
|
340
|
+
except (ModuleNotFoundError, ImportError):
|
341
|
+
print("PyAV: error")
|
342
342
|
|
343
|
-
print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
|
344
|
-
print(f"Python: {plat.python_version()}")
|
345
|
-
print(f"PyAV: {av.__version__} ({license})")
|
346
343
|
print(f"Auto-Editor: {auto_editor.__version__}")
|
347
344
|
return
|
348
345
|
|
@@ -352,11 +349,10 @@ def main() -> None:
|
|
352
349
|
is_machine = args.progress == "machine"
|
353
350
|
log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
|
354
351
|
|
355
|
-
ffmpeg = FFmpeg(args.ffmpeg_location)
|
356
352
|
paths = []
|
357
353
|
for my_input in args.input:
|
358
354
|
if my_input.startswith("http://") or my_input.startswith("https://"):
|
359
|
-
paths.append(download_video(my_input, args,
|
355
|
+
paths.append(download_video(my_input, args, log))
|
360
356
|
else:
|
361
357
|
if not splitext(my_input)[1]:
|
362
358
|
if isdir(my_input):
|
@@ -369,8 +365,10 @@ def main() -> None:
|
|
369
365
|
log.error(f"Option/Input file doesn't exist: {my_input}")
|
370
366
|
paths.append(my_input)
|
371
367
|
|
368
|
+
from auto_editor.edit import edit_media
|
369
|
+
|
372
370
|
try:
|
373
|
-
edit_media(paths,
|
371
|
+
edit_media(paths, args, log)
|
374
372
|
except KeyboardInterrupt:
|
375
373
|
log.error("Keyboard Interrupt")
|
376
374
|
log.cleanup()
|
auto_editor/analyze.py
CHANGED
@@ -4,6 +4,7 @@ import os
|
|
4
4
|
import re
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from fractions import Fraction
|
7
|
+
from hashlib import sha1
|
7
8
|
from math import ceil
|
8
9
|
from tempfile import gettempdir
|
9
10
|
from typing import TYPE_CHECKING
|
@@ -27,6 +28,9 @@ if TYPE_CHECKING:
|
|
27
28
|
from auto_editor.utils.log import Log
|
28
29
|
|
29
30
|
|
31
|
+
__all__ = ("LevelError", "Levels", "iter_audio", "iter_motion")
|
32
|
+
|
33
|
+
|
30
34
|
class LevelError(Exception):
|
31
35
|
pass
|
32
36
|
|
@@ -69,45 +73,39 @@ def mut_remove_large(
|
|
69
73
|
active = False
|
70
74
|
|
71
75
|
|
72
|
-
def iter_audio(
|
76
|
+
def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float32]:
|
73
77
|
fifo = AudioFifo()
|
74
|
-
|
75
|
-
container = av.open(src.path, "r")
|
76
|
-
audio_stream = container.streams.audio[stream]
|
77
|
-
sample_rate = audio_stream.rate
|
78
|
+
sr = audio_stream.rate
|
78
79
|
|
79
|
-
|
80
|
-
|
80
|
+
exact_size = (1 / tb) * sr
|
81
|
+
accumulated_error = Fraction(0)
|
81
82
|
|
82
|
-
|
83
|
-
|
84
|
-
av.AudioFormat("flt"), audio_stream.layout, sample_rate
|
85
|
-
)
|
83
|
+
# Resample so that audio data is between [-1, 1]
|
84
|
+
resampler = av.AudioResampler(av.AudioFormat("flt"), audio_stream.layout, sr)
|
86
85
|
|
87
|
-
|
88
|
-
|
86
|
+
container = audio_stream.container
|
87
|
+
assert isinstance(container, av.container.InputContainer)
|
89
88
|
|
90
|
-
|
91
|
-
|
89
|
+
for frame in container.decode(audio_stream):
|
90
|
+
frame.pts = None # Skip time checks
|
92
91
|
|
93
|
-
|
94
|
-
|
95
|
-
current_size = round(size_with_error)
|
96
|
-
accumulated_error = size_with_error - current_size
|
92
|
+
for reframe in resampler.resample(frame):
|
93
|
+
fifo.write(reframe)
|
97
94
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
95
|
+
while fifo.samples >= ceil(exact_size):
|
96
|
+
size_with_error = exact_size + accumulated_error
|
97
|
+
current_size = round(size_with_error)
|
98
|
+
accumulated_error = size_with_error - current_size
|
102
99
|
|
103
|
-
|
104
|
-
|
100
|
+
audio_chunk = fifo.read(current_size)
|
101
|
+
assert audio_chunk is not None
|
102
|
+
arr = audio_chunk.to_ndarray().flatten()
|
103
|
+
yield np.max(np.abs(arr))
|
105
104
|
|
106
105
|
|
107
|
-
def iter_motion(
|
108
|
-
|
109
|
-
|
110
|
-
video = container.streams.video[stream]
|
106
|
+
def iter_motion(
|
107
|
+
video: av.VideoStream, tb: Fraction, blur: int, width: int
|
108
|
+
) -> Iterator[np.float32]:
|
111
109
|
video.thread_type = "AUTO"
|
112
110
|
|
113
111
|
prev_frame = None
|
@@ -125,6 +123,9 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
125
123
|
graph.add("buffersink"),
|
126
124
|
).configure()
|
127
125
|
|
126
|
+
container = video.container
|
127
|
+
assert isinstance(container, av.container.InputContainer)
|
128
|
+
|
128
129
|
for unframe in container.decode(video):
|
129
130
|
if unframe.pts is None:
|
130
131
|
continue
|
@@ -151,13 +152,13 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
151
152
|
prev_frame = current_frame
|
152
153
|
prev_index = index
|
153
154
|
|
154
|
-
container.close()
|
155
|
-
|
156
155
|
|
157
156
|
def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
|
158
157
|
mod_time = int(path.stat().st_mtime)
|
159
|
-
key = f"{path
|
160
|
-
|
158
|
+
key = f"{path}:{mod_time:x}:{tb}:" + ",".join(f"{v}" for v in obj)
|
159
|
+
part1 = sha1(key.encode()).hexdigest()[:16]
|
160
|
+
|
161
|
+
return f"{part1}{kind}"
|
161
162
|
|
162
163
|
|
163
164
|
@dataclass(slots=True)
|
@@ -175,7 +176,11 @@ class Levels:
|
|
175
176
|
if (arr := self.read_cache("audio", (0,))) is not None:
|
176
177
|
return len(arr)
|
177
178
|
|
178
|
-
|
179
|
+
with av.open(self.src.path, "r") as container:
|
180
|
+
audio_stream = container.streams.audio[0]
|
181
|
+
self.log.experimental(audio_stream.codec)
|
182
|
+
result = sum(1 for _ in iter_audio(audio_stream, self.tb))
|
183
|
+
|
179
184
|
self.log.debug(f"Audio Length: {result}")
|
180
185
|
return result
|
181
186
|
|
@@ -204,31 +209,47 @@ class Levels:
|
|
204
209
|
if self.no_cache:
|
205
210
|
return None
|
206
211
|
|
207
|
-
|
212
|
+
key = obj_tag(self.src.path, kind, self.tb, obj)
|
213
|
+
cache_file = os.path.join(gettempdir(), f"ae-{__version__}", f"{key}.npz")
|
208
214
|
|
209
215
|
try:
|
210
|
-
|
216
|
+
with np.load(cache_file, allow_pickle=False) as npzfile:
|
217
|
+
return npzfile["data"]
|
211
218
|
except Exception as e:
|
212
219
|
self.log.debug(e)
|
213
220
|
return None
|
214
221
|
|
215
|
-
key = obj_tag(self.src.path, kind, self.tb, obj)
|
216
|
-
if key not in npzfile.files:
|
217
|
-
return None
|
218
|
-
|
219
|
-
self.log.debug("Using cache")
|
220
|
-
return npzfile[key]
|
221
|
-
|
222
222
|
def cache(self, arr: np.ndarray, kind: str, obj: Sequence[object]) -> np.ndarray:
|
223
223
|
if self.no_cache:
|
224
224
|
return arr
|
225
225
|
|
226
|
-
|
227
|
-
if not os.path.exists(
|
228
|
-
os.mkdir(
|
226
|
+
workdir = os.path.join(gettempdir(), f"ae-{__version__}")
|
227
|
+
if not os.path.exists(workdir):
|
228
|
+
os.mkdir(workdir)
|
229
229
|
|
230
230
|
key = obj_tag(self.src.path, kind, self.tb, obj)
|
231
|
-
|
231
|
+
cache_file = os.path.join(workdir, f"{key}.npz")
|
232
|
+
|
233
|
+
try:
|
234
|
+
np.savez(cache_file, data=arr)
|
235
|
+
except Exception as e:
|
236
|
+
self.log.warning(f"Cache write failed: {e}")
|
237
|
+
|
238
|
+
cache_entries = []
|
239
|
+
with os.scandir(workdir) as entries:
|
240
|
+
for entry in entries:
|
241
|
+
if entry.name.endswith(".npz"):
|
242
|
+
cache_entries.append((entry.path, entry.stat().st_mtime))
|
243
|
+
|
244
|
+
if len(cache_entries) > 10:
|
245
|
+
# Sort by modification time, oldest first
|
246
|
+
cache_entries.sort(key=lambda x: x[1])
|
247
|
+
# Remove oldest files until we're back to 10
|
248
|
+
for filepath, _ in cache_entries[:-10]:
|
249
|
+
try:
|
250
|
+
os.remove(filepath)
|
251
|
+
except OSError:
|
252
|
+
pass
|
232
253
|
|
233
254
|
return arr
|
234
255
|
|
@@ -239,30 +260,37 @@ class Levels:
|
|
239
260
|
if (arr := self.read_cache("audio", (stream,))) is not None:
|
240
261
|
return arr
|
241
262
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
263
|
+
container = av.open(self.src.path, "r")
|
264
|
+
audio = container.streams.audio[stream]
|
265
|
+
|
266
|
+
if audio.codec.experimental:
|
267
|
+
self.log.error(f"`{audio.codec.name}` is an experimental codec")
|
268
|
+
|
269
|
+
if audio.duration is not None and audio.time_base is not None:
|
270
|
+
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
|
271
|
+
elif container.duration is not None:
|
272
|
+
inaccurate_dur = int(container.duration / av.time_base * self.tb)
|
273
|
+
else:
|
274
|
+
inaccurate_dur = 1024
|
250
275
|
|
251
276
|
bar = self.bar
|
252
277
|
bar.start(inaccurate_dur, "Analyzing audio volume")
|
253
278
|
|
254
|
-
result = np.zeros(
|
279
|
+
result: NDArray[np.float32] = np.zeros(inaccurate_dur, dtype=np.float32)
|
255
280
|
index = 0
|
256
|
-
|
281
|
+
|
282
|
+
for value in iter_audio(audio, self.tb):
|
257
283
|
if index > len(result) - 1:
|
258
284
|
result = np.concatenate(
|
259
|
-
(result, np.zeros(
|
285
|
+
(result, np.zeros(len(result), dtype=np.float32))
|
260
286
|
)
|
287
|
+
|
261
288
|
result[index] = value
|
262
289
|
bar.tick(index)
|
263
290
|
index += 1
|
264
291
|
|
265
292
|
bar.end()
|
293
|
+
assert len(result) > 0
|
266
294
|
return self.cache(result[:index], "audio", (stream,))
|
267
295
|
|
268
296
|
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
|
@@ -273,23 +301,28 @@ class Levels:
|
|
273
301
|
if (arr := self.read_cache("motion", mobj)) is not None:
|
274
302
|
return arr
|
275
303
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
304
|
+
container = av.open(self.src.path, "r")
|
305
|
+
video = container.streams.video[stream]
|
306
|
+
|
307
|
+
if video.codec.experimental:
|
308
|
+
self.log.experimental(video.codec)
|
309
|
+
|
310
|
+
inaccurate_dur = (
|
311
|
+
1024
|
312
|
+
if video.duration is None or video.time_base is None
|
313
|
+
else int(video.duration * video.time_base * self.tb)
|
314
|
+
)
|
283
315
|
|
284
316
|
bar = self.bar
|
285
317
|
bar.start(inaccurate_dur, "Analyzing motion")
|
286
318
|
|
287
|
-
result = np.zeros(
|
319
|
+
result: NDArray[np.float32] = np.zeros(inaccurate_dur, dtype=np.float32)
|
288
320
|
index = 0
|
289
|
-
|
321
|
+
|
322
|
+
for value in iter_motion(video, self.tb, blur, width):
|
290
323
|
if index > len(result) - 1:
|
291
324
|
result = np.concatenate(
|
292
|
-
(result, np.zeros(
|
325
|
+
(result, np.zeros(len(result), dtype=np.float32))
|
293
326
|
)
|
294
327
|
result[index] = value
|
295
328
|
bar.tick(index)
|
auto_editor/edit.py
CHANGED
@@ -10,7 +10,7 @@ from typing import Any
|
|
10
10
|
import av
|
11
11
|
from av import AudioResampler
|
12
12
|
|
13
|
-
from auto_editor.ffwrapper import
|
13
|
+
from auto_editor.ffwrapper import FileInfo, initFileInfo
|
14
14
|
from auto_editor.lib.contracts import is_int, is_str
|
15
15
|
from auto_editor.make_layers import clipify, make_av, make_timeline
|
16
16
|
from auto_editor.output import Ensure, parse_bitrate
|
@@ -160,7 +160,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
160
160
|
log.error(f"'{name}': Export must be [{', '.join([s for s in parsing.keys()])}]")
|
161
161
|
|
162
162
|
|
163
|
-
def edit_media(paths: list[str],
|
163
|
+
def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
164
164
|
bar = initBar(args.progress)
|
165
165
|
tl = None
|
166
166
|
|
@@ -294,7 +294,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
294
294
|
|
295
295
|
if ctr.default_aud != "none":
|
296
296
|
ensure = Ensure(bar, samplerate, log)
|
297
|
-
audio_paths = make_new_audio(tl, ctr, ensure, args,
|
297
|
+
audio_paths = make_new_audio(tl, ctr, ensure, args, bar, log)
|
298
298
|
else:
|
299
299
|
audio_paths = []
|
300
300
|
|
@@ -343,8 +343,8 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
343
343
|
for i, sub_path in enumerate(sub_paths):
|
344
344
|
subtitle_input = av.open(sub_path)
|
345
345
|
subtitle_inputs.append(subtitle_input)
|
346
|
-
subtitle_stream = output.
|
347
|
-
|
346
|
+
subtitle_stream = output.add_stream_from_template(
|
347
|
+
subtitle_input.streams.subtitles[0]
|
348
348
|
)
|
349
349
|
if i < len(src.subtitles) and src.subtitles[i].lang is not None:
|
350
350
|
subtitle_stream.metadata["language"] = src.subtitles[i].lang # type: ignore
|
auto_editor/ffwrapper.py
CHANGED
@@ -3,40 +3,12 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from fractions import Fraction
|
5
5
|
from pathlib import Path
|
6
|
-
from shutil import which
|
7
|
-
from subprocess import PIPE, Popen
|
8
6
|
|
9
7
|
import av
|
10
8
|
|
11
9
|
from auto_editor.utils.log import Log
|
12
10
|
|
13
11
|
|
14
|
-
def _get_ffmpeg(reason: str, ffloc: str | None, log: Log) -> str:
|
15
|
-
program = "ffmpeg" if ffloc is None else ffloc
|
16
|
-
if (path := which(program)) is None:
|
17
|
-
log.error(f"{reason} needs ffmpeg cli but couldn't find ffmpeg on PATH.")
|
18
|
-
return path
|
19
|
-
|
20
|
-
|
21
|
-
@dataclass(slots=True)
|
22
|
-
class FFmpeg:
|
23
|
-
ffmpeg_location: str | None
|
24
|
-
path: str | None = None
|
25
|
-
|
26
|
-
def get_path(self, reason: str, log: Log) -> str:
|
27
|
-
if self.path is not None:
|
28
|
-
return self.path
|
29
|
-
|
30
|
-
self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
|
31
|
-
return self.path
|
32
|
-
|
33
|
-
def Popen(self, reason: str, cmd: list[str], log: Log) -> Popen:
|
34
|
-
if self.path is None:
|
35
|
-
self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
|
36
|
-
|
37
|
-
return Popen([self.path] + cmd, stdout=PIPE, stderr=PIPE)
|
38
|
-
|
39
|
-
|
40
12
|
def mux(input: Path, output: Path, stream: int) -> None:
|
41
13
|
input_container = av.open(input, "r")
|
42
14
|
output_container = av.open(output, "w")
|
auto_editor/help.py
CHANGED
@@ -24,10 +24,23 @@ example:
|
|
24
24
|
will set the speed from 400 ticks to 800 ticks to 2.5x
|
25
25
|
If timebase is 30, 400 ticks to 800 means 13.33 to 26.66 seconds
|
26
26
|
""".strip(),
|
27
|
-
"--edit
|
27
|
+
"--edit": """
|
28
28
|
Evaluates a palet expression that returns a bool-array?. The array is then used for
|
29
29
|
editing.
|
30
30
|
|
31
|
+
Examples:
|
32
|
+
--edit audio
|
33
|
+
--edit audio:0.03 ; Change the threshold. Can be a value between 0-1.
|
34
|
+
--edit audio:3% ; You can also use the `%` macro.
|
35
|
+
--edit audio:0.03,stream=0 ; Only consider the first stream for editing.
|
36
|
+
--edit audio:stream=1,threshold=0.05 ; Here's how you use keyword arguments.
|
37
|
+
--edit (or audio:0.04,stream=0 audio:0.08,stream=1) ; Consider both streams for editing (merge with logical or), but with different thresholds.
|
38
|
+
--edit motion
|
39
|
+
--edit motion:0.02,blur=3
|
40
|
+
--edit (or audio:0.04 motion:0.02,blur=3)
|
41
|
+
--edit none
|
42
|
+
--edit all/e
|
43
|
+
|
31
44
|
Editing Methods:
|
32
45
|
- audio ; Audio silence/loudness detection
|
33
46
|
- threshold threshold? : 4%
|
@@ -52,19 +65,6 @@ Editing Methods:
|
|
52
65
|
|
53
66
|
- none ; Do not modify the media in anyway; mark all sections as "loud" (1).
|
54
67
|
- all/e ; Cut out everything out; mark all sections as "silent" (0).
|
55
|
-
|
56
|
-
|
57
|
-
Command-line Examples:
|
58
|
-
--edit audio
|
59
|
-
--edit audio:threshold=4%
|
60
|
-
--edit audio:threshold=0.03
|
61
|
-
--edit audio:stream=1
|
62
|
-
--edit (or audio:4%,stream=0 audio:8%,stream=1) ; `threshold` is first
|
63
|
-
--edit motion
|
64
|
-
--edit motion:threshold=2%,blur=3
|
65
|
-
--edit (or audio:4% motion:2%,blur=3)
|
66
|
-
--edit none
|
67
|
-
--edit all/e
|
68
68
|
""".strip(),
|
69
69
|
"--export": """
|
70
70
|
This option controls how timelines are exported.
|
@@ -144,8 +144,6 @@ If not set, tempdir will be set with Python's tempfile module
|
|
144
144
|
The directory doesn't have to exist beforehand, however, the root path must be valid.
|
145
145
|
Beware that the temp directory can get quite big.
|
146
146
|
""".strip(),
|
147
|
-
"--ffmpeg-location": "This takes precedence over `--my-ffmpeg`.",
|
148
|
-
"--my-ffmpeg": "This is equivalent to `--ffmpeg-location ffmpeg`.",
|
149
147
|
"--audio-bitrate": """
|
150
148
|
`--audio-bitrate` sets the target bitrate for the audio encoder.
|
151
149
|
By default, the value is `auto` (let the encoder decide).
|
auto_editor/make_layers.py
CHANGED
@@ -139,7 +139,7 @@ def make_timeline(
|
|
139
139
|
|
140
140
|
for i, src in enumerate(sources):
|
141
141
|
try:
|
142
|
-
parser = Parser(Lexer("`--edit`", args.
|
142
|
+
parser = Parser(Lexer("`--edit`", args.edit))
|
143
143
|
if log.is_debug:
|
144
144
|
log.debug(f"edit: {parser}")
|
145
145
|
|
@@ -169,6 +169,8 @@ def make_timeline(
|
|
169
169
|
has_loud = concat((has_loud, result))
|
170
170
|
src_index = concat((src_index, np.full(len(result), i, dtype=np.int32)))
|
171
171
|
|
172
|
+
assert len(has_loud) > 0
|
173
|
+
|
172
174
|
# Setup for handling custom speeds
|
173
175
|
speed_index = has_loud.astype(np.uint)
|
174
176
|
speed_map = [args.silent_speed, args.video_speed]
|
auto_editor/render/audio.py
CHANGED
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import io
|
4
4
|
from pathlib import Path
|
5
|
-
from platform import system
|
6
5
|
|
7
6
|
import av
|
8
7
|
import numpy as np
|
8
|
+
from av.filter.loudnorm import stats
|
9
9
|
|
10
|
-
from auto_editor.ffwrapper import
|
10
|
+
from auto_editor.ffwrapper import FileInfo
|
11
11
|
from auto_editor.lang.json import Lexer, Parser
|
12
12
|
from auto_editor.lang.palet import env
|
13
13
|
from auto_editor.lib.contracts import andc, between_c, is_int_or_float
|
@@ -56,25 +56,11 @@ def parse_norm(norm: str, log: Log) -> dict | None:
|
|
56
56
|
log.error(e)
|
57
57
|
|
58
58
|
|
59
|
-
def parse_ebu_bytes(norm: dict,
|
60
|
-
start = end = 0
|
61
|
-
lines = stderr.splitlines()
|
62
|
-
|
63
|
-
for index, line in enumerate(lines):
|
64
|
-
if line.startswith(b"[Parsed_loudnorm"):
|
65
|
-
start = index + 1
|
66
|
-
continue
|
67
|
-
if start != 0 and line.startswith(b"}"):
|
68
|
-
end = index + 1
|
69
|
-
break
|
70
|
-
|
71
|
-
if start == 0 or end == 0:
|
72
|
-
log.error(f"Invalid loudnorm stats.\n{stderr!r}")
|
73
|
-
|
59
|
+
def parse_ebu_bytes(norm: dict, stat: bytes, log: Log) -> tuple[str, str]:
|
74
60
|
try:
|
75
|
-
parsed = Parser(Lexer("loudnorm",
|
61
|
+
parsed = Parser(Lexer("loudnorm", stat)).expr()
|
76
62
|
except MyError:
|
77
|
-
log.error(f"Invalid loudnorm stats.\n{
|
63
|
+
log.error(f"Invalid loudnorm stats.\n{stat!r}")
|
78
64
|
|
79
65
|
for key in ("input_i", "input_tp", "input_lra", "input_thresh", "target_offset"):
|
80
66
|
val = float(parsed[key])
|
@@ -101,29 +87,17 @@ def parse_ebu_bytes(norm: dict, stderr: bytes, log: Log) -> tuple[str, str]:
|
|
101
87
|
|
102
88
|
|
103
89
|
def apply_audio_normalization(
|
104
|
-
|
90
|
+
norm: dict, pre_master: Path, path: Path, log: Log
|
105
91
|
) -> None:
|
106
92
|
if norm["tag"] == "ebu":
|
107
93
|
first_pass = (
|
108
|
-
f"
|
109
|
-
f"offset={norm['gain']}:print_format=json"
|
94
|
+
f"i={norm['i']}:lra={norm['lra']}:tp={norm['tp']}:" f"offset={norm['gain']}"
|
110
95
|
)
|
111
96
|
log.debug(f"audio norm first pass: {first_pass}")
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
f"{pre_master}",
|
117
|
-
"-af",
|
118
|
-
first_pass,
|
119
|
-
"-vn",
|
120
|
-
"-sn",
|
121
|
-
"-f",
|
122
|
-
"null",
|
123
|
-
file_null,
|
124
|
-
]
|
125
|
-
stderr = ffmpeg.Popen("EBU", cmd, log).communicate()[1]
|
126
|
-
name, filter_args = parse_ebu_bytes(norm, stderr, log)
|
97
|
+
with av.open(f"{pre_master}") as container:
|
98
|
+
stats_ = stats(first_pass, container.streams.audio[0])
|
99
|
+
|
100
|
+
name, filter_args = parse_ebu_bytes(norm, stats_, log)
|
127
101
|
else:
|
128
102
|
assert "t" in norm
|
129
103
|
|
@@ -310,13 +284,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
|
310
284
|
|
311
285
|
|
312
286
|
def make_new_audio(
|
313
|
-
tl: v3,
|
314
|
-
ctr: Container,
|
315
|
-
ensure: Ensure,
|
316
|
-
args: Args,
|
317
|
-
ffmpeg: FFmpeg,
|
318
|
-
bar: Bar,
|
319
|
-
log: Log,
|
287
|
+
tl: v3, ctr: Container, ensure: Ensure, args: Args, bar: Bar, log: Log
|
320
288
|
) -> list[str]:
|
321
289
|
sr = tl.sr
|
322
290
|
tb = tl.tb
|
@@ -390,7 +358,7 @@ def make_new_audio(
|
|
390
358
|
with open(pre_master, "wb") as fid:
|
391
359
|
write(fid, sr, arr)
|
392
360
|
|
393
|
-
apply_audio_normalization(
|
361
|
+
apply_audio_normalization(norm, pre_master, path, log)
|
394
362
|
|
395
363
|
bar.end()
|
396
364
|
|
auto_editor/render/subtitle.py
CHANGED
@@ -162,7 +162,7 @@ def _ensure(input_: Input, format: str, stream: int) -> str:
|
|
162
162
|
output = av.open(output_bytes, "w", format=format)
|
163
163
|
|
164
164
|
in_stream = input_.streams.subtitles[stream]
|
165
|
-
out_stream = output.
|
165
|
+
out_stream = output.add_stream_from_template(in_stream)
|
166
166
|
|
167
167
|
for packet in input_.demux(in_stream):
|
168
168
|
if packet.dts is None:
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import glob
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
from shutil import rmtree
|
5
|
+
from tempfile import gettempdir
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
from auto_editor import __version__
|
10
|
+
|
11
|
+
|
12
|
+
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
13
|
+
cache_dir = os.path.join(gettempdir(), f"ae-{__version__}")
|
14
|
+
|
15
|
+
if sys_args and sys_args[0] in ("clean", "clear"):
|
16
|
+
rmtree(cache_dir, ignore_errors=True)
|
17
|
+
return
|
18
|
+
|
19
|
+
if not os.path.exists(cache_dir):
|
20
|
+
print("Empty cache")
|
21
|
+
return
|
22
|
+
|
23
|
+
cache_files = glob.glob(os.path.join(cache_dir, "*.npz"))
|
24
|
+
if not cache_files:
|
25
|
+
print("Empty cache")
|
26
|
+
return
|
27
|
+
|
28
|
+
def format_bytes(size: float) -> str:
|
29
|
+
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
30
|
+
if size < 1024:
|
31
|
+
return f"{size:.2f} {unit}"
|
32
|
+
size /= 1024
|
33
|
+
return f"{size:.2f} PiB"
|
34
|
+
|
35
|
+
GRAY = "\033[90m"
|
36
|
+
GREEN = "\033[32m"
|
37
|
+
BLUE = "\033[34m"
|
38
|
+
YELLOW = "\033[33m"
|
39
|
+
RESET = "\033[0m"
|
40
|
+
|
41
|
+
total_size = 0
|
42
|
+
for cache_file in cache_files:
|
43
|
+
try:
|
44
|
+
with np.load(cache_file, allow_pickle=False) as npzfile:
|
45
|
+
array = npzfile["data"]
|
46
|
+
key = os.path.basename(cache_file)[:-4] # Remove .npz extension
|
47
|
+
|
48
|
+
hash_part = key[:16]
|
49
|
+
rest_part = key[16:]
|
50
|
+
|
51
|
+
size = array.nbytes
|
52
|
+
total_size += size
|
53
|
+
size_str = format_bytes(size)
|
54
|
+
size_num, size_unit = size_str.rsplit(" ", 1)
|
55
|
+
|
56
|
+
print(
|
57
|
+
f"{YELLOW}entry: {GRAY}{hash_part}{RESET}{rest_part} "
|
58
|
+
f"{YELLOW}size: {GREEN}{size_num} {BLUE}{size_unit}{RESET}"
|
59
|
+
)
|
60
|
+
except Exception as e:
|
61
|
+
print(f"Error reading {cache_file}: {e}")
|
62
|
+
|
63
|
+
total_str = format_bytes(total_size)
|
64
|
+
total_num, total_unit = total_str.rsplit(" ", 1)
|
65
|
+
print(f"\n{YELLOW}total cache size: {GREEN}{total_num} {BLUE}{total_unit}{RESET}")
|
66
|
+
|
67
|
+
|
68
|
+
if __name__ == "__main__":
|
69
|
+
main()
|
auto_editor/subcommands/info.py
CHANGED
@@ -163,6 +163,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
163
163
|
file_info[file]["subtitle"].append(sub)
|
164
164
|
|
165
165
|
if args.json:
|
166
|
+
if sys.platform == "win32":
|
167
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
166
168
|
dump(file_info, sys.stdout, indent=4)
|
167
169
|
return
|
168
170
|
|
@@ -5,9 +5,10 @@ from dataclasses import dataclass, field
|
|
5
5
|
from fractions import Fraction
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
+
import av
|
8
9
|
import numpy as np
|
9
10
|
|
10
|
-
from auto_editor.analyze import
|
11
|
+
from auto_editor.analyze import *
|
11
12
|
from auto_editor.ffwrapper import initFileInfo
|
12
13
|
from auto_editor.lang.palet import env
|
13
14
|
from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
|
@@ -130,9 +131,19 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
130
131
|
levels = Levels(src, tb, bar, False, log, strict=True)
|
131
132
|
try:
|
132
133
|
if method == "audio":
|
133
|
-
|
134
|
+
container = av.open(src.path, "r")
|
135
|
+
audio_stream = container.streams.audio[obj["stream"]]
|
136
|
+
log.experimental(audio_stream.codec)
|
137
|
+
print_arr_gen(iter_audio(audio_stream, tb))
|
138
|
+
container.close()
|
139
|
+
|
134
140
|
elif method == "motion":
|
135
|
-
|
141
|
+
container = av.open(src.path, "r")
|
142
|
+
video_stream = container.streams.video[obj["stream"]]
|
143
|
+
log.experimental(video_stream.codec)
|
144
|
+
print_arr_gen(iter_motion(video_stream, tb, obj["blur"], obj["width"]))
|
145
|
+
container.close()
|
146
|
+
|
136
147
|
elif method == "subtitle":
|
137
148
|
print_arr(levels.subtitle(**obj))
|
138
149
|
elif method == "none":
|
auto_editor/utils/log.py
CHANGED
@@ -5,7 +5,10 @@ from datetime import timedelta
|
|
5
5
|
from shutil import get_terminal_size, rmtree
|
6
6
|
from tempfile import mkdtemp
|
7
7
|
from time import perf_counter, sleep
|
8
|
-
from typing import NoReturn
|
8
|
+
from typing import TYPE_CHECKING, NoReturn
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
import av
|
9
12
|
|
10
13
|
|
11
14
|
class Log:
|
@@ -97,6 +100,10 @@ class Log:
|
|
97
100
|
|
98
101
|
sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
|
99
102
|
|
103
|
+
def experimental(self, codec: av.Codec) -> None:
|
104
|
+
if codec.experimental:
|
105
|
+
self.error(f"`{codec.name}` is an experimental codec")
|
106
|
+
|
100
107
|
def error(self, message: str | Exception) -> NoReturn:
|
101
108
|
if self.is_debug and isinstance(message, Exception):
|
102
109
|
self.cleanup()
|
auto_editor/utils/types.py
CHANGED
@@ -210,14 +210,13 @@ class Args:
|
|
210
210
|
sample_rate: int | None = None
|
211
211
|
resolution: tuple[int, int] | None = None
|
212
212
|
background: str = "#000000"
|
213
|
-
|
213
|
+
edit: str = "audio"
|
214
214
|
keep_tracks_separate: bool = False
|
215
215
|
audio_normalize: str = "#f"
|
216
216
|
export: str | None = None
|
217
217
|
player: str | None = None
|
218
218
|
no_open: bool = False
|
219
219
|
temp_dir: str | None = None
|
220
|
-
ffmpeg_location: str | None = None
|
221
220
|
progress: str = "modern"
|
222
221
|
version: bool = False
|
223
222
|
debug: bool = False
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 26.
|
3
|
+
Version: 26.1.1
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -11,8 +11,8 @@ Keywords: video,audio,media,editor,editing,processing,nonlinear,automatic,silenc
|
|
11
11
|
Requires-Python: <3.14,>=3.10
|
12
12
|
Description-Content-Type: text/markdown
|
13
13
|
License-File: LICENSE
|
14
|
-
Requires-Dist: numpy
|
15
|
-
Requires-Dist: pyav
|
14
|
+
Requires-Dist: numpy<3.0,>=1.24
|
15
|
+
Requires-Dist: pyav==14.*
|
16
16
|
|
17
17
|
<p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
|
18
18
|
|
@@ -1,10 +1,10 @@
|
|
1
|
-
auto_editor/__init__.py,sha256=
|
2
|
-
auto_editor/__main__.py,sha256=
|
3
|
-
auto_editor/analyze.py,sha256=
|
4
|
-
auto_editor/edit.py,sha256=
|
5
|
-
auto_editor/ffwrapper.py,sha256=
|
6
|
-
auto_editor/help.py,sha256=
|
7
|
-
auto_editor/make_layers.py,sha256=
|
1
|
+
auto_editor/__init__.py,sha256=2Ltcef2BVJgJx2W5ZkX7r21sdnzR3Zvtu1PYKRHEjLk,23
|
2
|
+
auto_editor/__main__.py,sha256=tc0M1MIPYjU5wCEU3EqmleOzaUgksU60qVHO0vRuC10,11310
|
3
|
+
auto_editor/analyze.py,sha256=Fv8NA99T1dZzrqlweJNlK7haKjgK13neR9CMw4t6rlY,12716
|
4
|
+
auto_editor/edit.py,sha256=eEMRaQbn0jylfJ6D_egnUXjoMCbdQVsAu7MDrn-xlGo,15950
|
5
|
+
auto_editor/ffwrapper.py,sha256=Tct_Q-uy5F51h8M7UFam50UzRFpgkBvUamJP1AoKVvc,4749
|
6
|
+
auto_editor/help.py,sha256=CzfDTsL4GuGu596ySHKj_wKnxGR9h8B0KUdkZpo33oE,8044
|
7
|
+
auto_editor/make_layers.py,sha256=vEeJt0PnE1vc9-cQZ_AlXVDjvWhObRCWJSCQGraoMvU,9016
|
8
8
|
auto_editor/output.py,sha256=ho8Lpqz4Sv_Gw0Vj2OvG39s83xHpyZlvtRNryTPbXqc,2563
|
9
9
|
auto_editor/preview.py,sha256=HUsjmV9Fx73rZ26BXrpz9z-z_e4oiui3u9e7qbbGoBY,3037
|
10
10
|
auto_editor/timeline.py,sha256=XfaH9cH-RB-MObOpMr5IfLcqJcjmabO1XwkUkT3_FQM,8186
|
@@ -27,13 +27,14 @@ auto_editor/lib/contracts.py,sha256=lExGQymcQUmwG5lC1lO4qm4GY8W0q_yzK_miTaAoPA4,
|
|
27
27
|
auto_editor/lib/data_structs.py,sha256=dcsXgsLLzbmFDUZucoirzewPALsKzoxz7z5L22_QJM8,7091
|
28
28
|
auto_editor/lib/err.py,sha256=UlszQJdzMZwkbT8x3sY4GkCV_5x9yrd6uVVUzvA8iiI,35
|
29
29
|
auto_editor/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
|
-
auto_editor/render/audio.py,sha256=
|
31
|
-
auto_editor/render/subtitle.py,sha256=
|
30
|
+
auto_editor/render/audio.py,sha256=1iOQCeRXfRz28cqnHp2XeK-f3_UnPf80AKQAfifGvdE,12584
|
31
|
+
auto_editor/render/subtitle.py,sha256=lf2l1QWJgFiqlpQWWBwSlKJnSgW8Lkfi59WrJMbIDqM,6240
|
32
32
|
auto_editor/render/video.py,sha256=dje0RNW2dKILfTzt0VAF0WR6REfGOsc6l17pP1Z4ooA,12215
|
33
33
|
auto_editor/subcommands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
|
+
auto_editor/subcommands/cache.py,sha256=YW_5qH0q5TVzmfOLEO117uqcY7dF6DS619ltVTPIzHQ,1959
|
34
35
|
auto_editor/subcommands/desc.py,sha256=GDrKJYiHMaeTrplZAceXl1JwoqD78XsV2_5lc0Xd7po,869
|
35
|
-
auto_editor/subcommands/info.py,sha256=
|
36
|
-
auto_editor/subcommands/levels.py,sha256=
|
36
|
+
auto_editor/subcommands/info.py,sha256=UDdoxd6_fqSoRPwthkWXqnpxHp7dJQ0Dn96lYX_ubWc,7010
|
37
|
+
auto_editor/subcommands/levels.py,sha256=psSSIsGfzr9j0HGKp2yvK6nMlrkLwxkwsyI0uF2xb_c,4496
|
37
38
|
auto_editor/subcommands/palet.py,sha256=ONzTqemaQq9YEfIOsDRNnwzfqnEMUMSXIQrETxyroRU,749
|
38
39
|
auto_editor/subcommands/repl.py,sha256=TF_I7zsFY7-KdgidrqjafTz7o_eluVbLvgTcOBG-UWQ,3449
|
39
40
|
auto_editor/subcommands/subdump.py,sha256=af_XBf7kaevqHn1A71z8C-7x8pS5WKD9FE_ugkCw6rk,665
|
@@ -44,12 +45,12 @@ auto_editor/utils/chunks.py,sha256=J-eGKtEz68gFtRrj1kOSgH4Tj_Yz6prNQ7Xr-d9NQJw,5
|
|
44
45
|
auto_editor/utils/cmdkw.py,sha256=aUGBvBel2Ko1o6Rwmr4rEL-BMc5hEnzYLbyZ1GeJdcY,5729
|
45
46
|
auto_editor/utils/container.py,sha256=Wf1ZL0tvXWl6m1B9mK_SkgVl89ilV_LpwlQq0TVroCc,2704
|
46
47
|
auto_editor/utils/func.py,sha256=kB-pNDn20M6YT7sljyd_auve5teK-E2G4TgwVOAIuJw,2754
|
47
|
-
auto_editor/utils/log.py,sha256=
|
48
|
-
auto_editor/utils/types.py,sha256=
|
49
|
-
docs/build.py,sha256=
|
50
|
-
auto_editor-26.
|
51
|
-
auto_editor-26.
|
52
|
-
auto_editor-26.
|
53
|
-
auto_editor-26.
|
54
|
-
auto_editor-26.
|
55
|
-
auto_editor-26.
|
48
|
+
auto_editor/utils/log.py,sha256=n5dlJ2CdK_54eiYE02SPgkBdBWABV7tE2p8ONj_F6TM,3813
|
49
|
+
auto_editor/utils/types.py,sha256=7BF7R7DA5eKmtI6f5ia7bOYNL0u_2sviiPsE1VmP0lc,10724
|
50
|
+
docs/build.py,sha256=POy8X8QOBYe_8A8HI_yiVI_Qg9E5mLpn1z7AHQr0_vQ,1888
|
51
|
+
auto_editor-26.1.1.dist-info/LICENSE,sha256=yiq99pWITHfqS0pbZMp7cy2dnbreTuvBwudsU-njvIM,1210
|
52
|
+
auto_editor-26.1.1.dist-info/METADATA,sha256=Ovf6CjY_x-lyih-4c1xBZEkL_X0gvifVFthPcLSMOtk,6109
|
53
|
+
auto_editor-26.1.1.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
54
|
+
auto_editor-26.1.1.dist-info/entry_points.txt,sha256=-H7zdTw4MqnAcwrN5xTNkGIhzZtJMxS9r6lTMeR9-aA,240
|
55
|
+
auto_editor-26.1.1.dist-info/top_level.txt,sha256=jBV5zlbWRbKOa-xaWPvTD45QL7lGExx2BDzv-Ji4dTw,17
|
56
|
+
auto_editor-26.1.1.dist-info/RECORD,,
|
docs/build.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
import os
|
4
4
|
import sys
|
5
|
+
from html import escape
|
5
6
|
|
6
7
|
# Put 'auto_editor' in Python path
|
7
8
|
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
@@ -19,7 +20,7 @@ def main():
|
|
19
20
|
|
20
21
|
with open("src/ref/options.html", "w") as file:
|
21
22
|
file.write(
|
22
|
-
'{{
|
23
|
+
'{{ headerdesc "Options" "These are the options and flags that auto-editor uses." }}\n'
|
23
24
|
"<body>\n"
|
24
25
|
"{{ nav }}\n"
|
25
26
|
'<section class="section">\n'
|
@@ -27,12 +28,18 @@ def main():
|
|
27
28
|
)
|
28
29
|
for op in parser.args:
|
29
30
|
if isinstance(op, OptionText):
|
30
|
-
file.write(f"<h2>{op.text}</h2>\n")
|
31
|
+
file.write(f"<h2>{escape(op.text)}</h2>\n")
|
31
32
|
else:
|
32
|
-
|
33
|
+
if op.metavar is None:
|
34
|
+
file.write(f"<h3><code>{op.names[0]}</code></h3>\n")
|
35
|
+
else:
|
36
|
+
file.write(
|
37
|
+
f"<h3><code>{op.names[0]} {escape(op.metavar)}</code></h3>\n"
|
38
|
+
)
|
39
|
+
|
33
40
|
if len(op.names) > 1:
|
34
41
|
file.write(
|
35
|
-
"<h4
|
42
|
+
"<h4>Aliases: <code>"
|
36
43
|
+ "</code> <code>".join(op.names[1:])
|
37
44
|
+ "</code></h4>\n"
|
38
45
|
)
|
File without changes
|
File without changes
|
File without changes
|