auto-editor 26.0.0__tar.gz → 26.0.1__tar.gz
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-26.0.0 → auto_editor-26.0.1}/PKG-INFO +1 -2
- auto_editor-26.0.1/auto_editor/__init__.py +1 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/__main__.py +5 -22
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/edit.py +11 -29
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/ffwrapper.py +20 -41
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/formats/fcp7.py +1 -1
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lang/palet.py +3 -9
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lang/stdenv.py +0 -7
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/output.py +4 -1
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/render/audio.py +87 -5
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/render/subtitle.py +6 -4
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/render/video.py +1 -2
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/subcommands/test.py +1 -1
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/timeline.py +2 -2
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/utils/cmdkw.py +5 -8
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/utils/container.py +2 -5
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/utils/func.py +1 -34
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/utils/types.py +2 -15
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor.egg-info/PKG-INFO +1 -2
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor.egg-info/SOURCES.txt +0 -1
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor.egg-info/requires.txt +0 -1
- {auto_editor-26.0.0 → auto_editor-26.0.1}/pyproject.toml +0 -1
- auto_editor-26.0.0/auto_editor/__init__.py +0 -1
- auto_editor-26.0.0/auto_editor/utils/encoder.py +0 -135
- {auto_editor-26.0.0 → auto_editor-26.0.1}/LICENSE +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/README.md +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/analyze.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/formats/__init__.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/formats/fcp11.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/formats/json.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/formats/shotcut.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/formats/utils.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/help.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lang/__init__.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lang/json.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lang/libintrospection.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lang/libmath.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lib/__init__.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lib/contracts.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lib/data_structs.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/lib/err.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/make_layers.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/preview.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/render/__init__.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/subcommands/__init__.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/subcommands/desc.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/subcommands/info.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/subcommands/levels.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/subcommands/palet.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/subcommands/repl.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/subcommands/subdump.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/utils/__init__.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/utils/bar.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/utils/chunks.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/utils/log.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/vanparse.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor/wavfile.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor.egg-info/dependency_links.txt +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor.egg-info/entry_points.txt +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/auto_editor.egg-info/top_level.txt +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/docs/build.py +0 -0
- {auto_editor-26.0.0 → auto_editor-26.0.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 26.0.
|
3
|
+
Version: 26.0.1
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -13,7 +13,6 @@ Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
14
14
|
Requires-Dist: numpy<3.0,>=1.23.0
|
15
15
|
Requires-Dist: pyav==13.1.*
|
16
|
-
Requires-Dist: ae-ffmpeg==1.2.*
|
17
16
|
|
18
17
|
<p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
|
19
18
|
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "26.0.1"
|
@@ -8,15 +8,15 @@ from subprocess import run
|
|
8
8
|
|
9
9
|
import auto_editor
|
10
10
|
from auto_editor.edit import edit_media
|
11
|
-
from auto_editor.ffwrapper import FFmpeg
|
11
|
+
from auto_editor.ffwrapper import FFmpeg
|
12
12
|
from auto_editor.utils.func import get_stdout
|
13
13
|
from auto_editor.utils.log import Log
|
14
14
|
from auto_editor.utils.types import (
|
15
15
|
Args,
|
16
|
-
color,
|
17
16
|
frame_rate,
|
18
17
|
margin,
|
19
18
|
number,
|
19
|
+
parse_color,
|
20
20
|
resolution,
|
21
21
|
sample_rate,
|
22
22
|
speed,
|
@@ -108,7 +108,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
108
108
|
parser.add_argument(
|
109
109
|
"--background",
|
110
110
|
"-b",
|
111
|
-
type=
|
111
|
+
type=parse_color,
|
112
112
|
metavar="COLOR",
|
113
113
|
help="Set the background as a solid RGB color",
|
114
114
|
)
|
@@ -166,11 +166,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
166
166
|
metavar="PATH",
|
167
167
|
help="Set a custom path to the ffmpeg location",
|
168
168
|
)
|
169
|
-
parser.add_argument(
|
170
|
-
"--my-ffmpeg",
|
171
|
-
flag=True,
|
172
|
-
help="Use the ffmpeg on your PATH instead of the one packaged",
|
173
|
-
)
|
174
169
|
parser.add_text("Display Options:")
|
175
170
|
parser.add_argument(
|
176
171
|
"--progress",
|
@@ -179,12 +174,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
179
174
|
help="Set what type of progress bar to use",
|
180
175
|
)
|
181
176
|
parser.add_argument("--debug", flag=True, help="Show debugging messages and values")
|
182
|
-
parser.add_argument(
|
183
|
-
"--show-ffmpeg-commands", flag=True, help="Show ffmpeg commands"
|
184
|
-
)
|
185
|
-
parser.add_argument(
|
186
|
-
"--show-ffmpeg-output", flag=True, help="Show ffmpeg stdout and stderr"
|
187
|
-
)
|
188
177
|
parser.add_argument("--quiet", "-q", flag=True, help="Display less output")
|
189
178
|
parser.add_argument(
|
190
179
|
"--preview",
|
@@ -285,7 +274,7 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
|
285
274
|
|
286
275
|
yt_dlp_path = args.yt_dlp_location
|
287
276
|
|
288
|
-
cmd = ["--ffmpeg-location", ffmpeg.
|
277
|
+
cmd = ["--ffmpeg-location", ffmpeg.get_path("yt-dlp", log)]
|
289
278
|
|
290
279
|
if download_format is not None:
|
291
280
|
cmd.extend(["-f", download_format])
|
@@ -363,13 +352,7 @@ def main() -> None:
|
|
363
352
|
is_machine = args.progress == "machine"
|
364
353
|
log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
|
365
354
|
|
366
|
-
ffmpeg =
|
367
|
-
log,
|
368
|
-
args.ffmpeg_location,
|
369
|
-
args.my_ffmpeg,
|
370
|
-
args.show_ffmpeg_commands,
|
371
|
-
args.show_ffmpeg_output,
|
372
|
-
)
|
355
|
+
ffmpeg = FFmpeg(args.ffmpeg_location)
|
373
356
|
paths = []
|
374
357
|
for my_input in args.input:
|
375
358
|
if my_input.startswith("http://") or my_input.startswith("https://"):
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import os
|
4
4
|
import sys
|
5
5
|
from fractions import Fraction
|
6
|
+
from os.path import splitext
|
6
7
|
from subprocess import run
|
7
8
|
from typing import Any
|
8
9
|
|
@@ -11,7 +12,7 @@ from av import AudioResampler
|
|
11
12
|
|
12
13
|
from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
|
13
14
|
from auto_editor.lib.contracts import is_int, is_str
|
14
|
-
from auto_editor.make_layers import make_timeline
|
15
|
+
from auto_editor.make_layers import clipify, make_av, make_timeline
|
15
16
|
from auto_editor.output import Ensure, parse_bitrate
|
16
17
|
from auto_editor.render.audio import make_new_audio
|
17
18
|
from auto_editor.render.subtitle import make_new_subtitles
|
@@ -31,7 +32,7 @@ def set_output(
|
|
31
32
|
if src is None:
|
32
33
|
root, ext = "out", ".mp4"
|
33
34
|
else:
|
34
|
-
root, ext =
|
35
|
+
root, ext = splitext(src.path if out is None else out)
|
35
36
|
if ext == "":
|
36
37
|
ext = src.path.suffix
|
37
38
|
|
@@ -164,7 +165,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
164
165
|
tl = None
|
165
166
|
|
166
167
|
if paths:
|
167
|
-
path_ext =
|
168
|
+
path_ext = splitext(paths[0])[1].lower()
|
168
169
|
if path_ext == ".xml":
|
169
170
|
from auto_editor.formats.fcp7 import fcp7_read_xml
|
170
171
|
|
@@ -243,7 +244,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
243
244
|
from auto_editor.formats.fcp7 import fcp7_write_xml
|
244
245
|
|
245
246
|
is_resolve = export.startswith("resolve")
|
246
|
-
fcp7_write_xml(export_ops["name"], output, is_resolve, tl
|
247
|
+
fcp7_write_xml(export_ops["name"], output, is_resolve, tl)
|
247
248
|
return
|
248
249
|
|
249
250
|
if export == "final-cut-pro":
|
@@ -267,7 +268,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
267
268
|
shotcut_write_mlt(output, tl)
|
268
269
|
return
|
269
270
|
|
270
|
-
out_ext =
|
271
|
+
out_ext = splitext(output)[1].replace(".", "")
|
271
272
|
|
272
273
|
# Check if export options make sense.
|
273
274
|
ctr = container_constructor(out_ext.lower())
|
@@ -293,27 +294,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
293
294
|
|
294
295
|
if ctr.default_aud != "none":
|
295
296
|
ensure = Ensure(bar, samplerate, log)
|
296
|
-
audio_paths = make_new_audio(tl, ensure, args, ffmpeg, bar, log)
|
297
|
-
if (
|
298
|
-
not (args.keep_tracks_separate and ctr.max_audios is None)
|
299
|
-
and len(audio_paths) > 1
|
300
|
-
):
|
301
|
-
# Merge all the audio a_tracks into one.
|
302
|
-
new_a_file = os.path.join(log.temp, "new_audio.wav")
|
303
|
-
new_cmd = []
|
304
|
-
for path in audio_paths:
|
305
|
-
new_cmd.extend(["-i", path])
|
306
|
-
new_cmd.extend(
|
307
|
-
[
|
308
|
-
"-filter_complex",
|
309
|
-
f"amix=inputs={len(audio_paths)}:duration=longest",
|
310
|
-
"-ac",
|
311
|
-
"2",
|
312
|
-
new_a_file,
|
313
|
-
]
|
314
|
-
)
|
315
|
-
ffmpeg.run(new_cmd)
|
316
|
-
audio_paths = [new_a_file]
|
297
|
+
audio_paths = make_new_audio(tl, ctr, ensure, args, ffmpeg, bar, log)
|
317
298
|
else:
|
318
299
|
audio_paths = []
|
319
300
|
|
@@ -430,14 +411,15 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
430
411
|
if tl.v1 is None:
|
431
412
|
log.error("Timeline too complex to use clip-sequence export")
|
432
413
|
|
433
|
-
from auto_editor.make_layers import clipify, make_av
|
434
|
-
from auto_editor.utils.func import append_filename
|
435
|
-
|
436
414
|
def pad_chunk(chunk: Chunk, total: int) -> Chunks:
|
437
415
|
start = [] if chunk[0] == 0 else [(0, chunk[0], 99999.0)]
|
438
416
|
end = [] if chunk[1] == total else [(chunk[1], total, 99999.0)]
|
439
417
|
return start + [chunk] + end
|
440
418
|
|
419
|
+
def append_filename(path: str, val: str) -> str:
|
420
|
+
root, ext = splitext(path)
|
421
|
+
return root + val + ext
|
422
|
+
|
441
423
|
total_frames = tl.v1.chunks[-1][1] - 1
|
442
424
|
clip_num = 0
|
443
425
|
for chunk in tl.v1.chunks:
|
@@ -1,61 +1,40 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import sys
|
4
3
|
from dataclasses import dataclass
|
5
4
|
from fractions import Fraction
|
6
5
|
from pathlib import Path
|
7
6
|
from shutil import which
|
8
|
-
from subprocess import PIPE, Popen
|
9
|
-
from typing import Any
|
7
|
+
from subprocess import PIPE, Popen
|
10
8
|
|
11
9
|
import av
|
12
10
|
|
13
11
|
from auto_editor.utils.log import Log
|
14
12
|
|
15
13
|
|
16
|
-
def
|
17
|
-
|
18
|
-
)
|
19
|
-
|
20
|
-
|
21
|
-
elif my_ffmpeg:
|
22
|
-
program = "ffmpeg"
|
23
|
-
else:
|
24
|
-
try:
|
25
|
-
import ae_ffmpeg
|
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
|
26
19
|
|
27
|
-
program = ae_ffmpeg.get_path()
|
28
|
-
except ImportError:
|
29
|
-
program = "ffmpeg"
|
30
20
|
|
31
|
-
|
32
|
-
|
33
|
-
|
21
|
+
@dataclass(slots=True)
|
22
|
+
class FFmpeg:
|
23
|
+
ffmpeg_location: str | None
|
24
|
+
path: str | None = None
|
34
25
|
|
35
|
-
|
26
|
+
def get_path(self, reason: str, log: Log) -> str:
|
27
|
+
if self.path is not None:
|
28
|
+
return self.path
|
36
29
|
|
30
|
+
self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
|
31
|
+
return self.path
|
37
32
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
debug: bool
|
44
|
-
|
45
|
-
def run(self, cmd: list[str]) -> None:
|
46
|
-
cmd = [self.path, "-hide_banner", "-y"] + cmd
|
47
|
-
if not self.debug:
|
48
|
-
cmd.extend(["-nostats", "-loglevel", "error"])
|
49
|
-
if self.show_cmd:
|
50
|
-
sys.stderr.write(f"{' '.join(cmd)}\n\n")
|
51
|
-
run(cmd)
|
52
|
-
|
53
|
-
def Popen(
|
54
|
-
self, cmd: list[str], stdin: Any = None, stdout: Any = PIPE, stderr: Any = None
|
55
|
-
) -> Popen:
|
56
|
-
if self.show_cmd:
|
57
|
-
sys.stderr.write(f"{self.path} {' '.join(cmd)}\n\n")
|
58
|
-
return Popen([self.path] + cmd, stdin=stdin, stdout=stdout, stderr=stderr)
|
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)
|
59
38
|
|
60
39
|
|
61
40
|
def mux(input: Path, output: Path, stream: int) -> None:
|
@@ -485,7 +485,7 @@ def premiere_write_audio(audio: Element, make_filedef, src: FileInfo, tl: v3) ->
|
|
485
485
|
audio.append(track)
|
486
486
|
|
487
487
|
|
488
|
-
def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3
|
488
|
+
def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
489
489
|
width, height = tl.res
|
490
490
|
timebase, ntsc = set_tb_ntsc(tl.tb)
|
491
491
|
|
@@ -353,9 +353,7 @@ class Lexer:
|
|
353
353
|
if is_method:
|
354
354
|
from auto_editor.utils.cmdkw import parse_method
|
355
355
|
|
356
|
-
return Token(
|
357
|
-
M, parse_method(name, result, env), self.lineno, self.column
|
358
|
-
)
|
356
|
+
return Token(M, parse_method(name, result), self.lineno, self.column)
|
359
357
|
|
360
358
|
if self.char == ".": # handle `object.method` syntax
|
361
359
|
self.advance()
|
@@ -635,6 +633,8 @@ def edit_subtitle(pattern, stream=0, **kwargs):
|
|
635
633
|
|
636
634
|
|
637
635
|
class StackTraceManager:
|
636
|
+
__slots__ = ("stack",)
|
637
|
+
|
638
638
|
def __init__(self) -> None:
|
639
639
|
self.stack: list[Sym] = []
|
640
640
|
|
@@ -645,12 +645,6 @@ class StackTraceManager:
|
|
645
645
|
if self.stack:
|
646
646
|
self.stack.pop()
|
647
647
|
|
648
|
-
def get_stacktrace(self) -> str:
|
649
|
-
return "\n".join(
|
650
|
-
f" at {sym.val} ({sym.lineno}:{sym.column})"
|
651
|
-
for sym in reversed(self.stack)
|
652
|
-
)
|
653
|
-
|
654
648
|
|
655
649
|
stack_trace_manager = StackTraceManager()
|
656
650
|
|
@@ -14,7 +14,6 @@ if TYPE_CHECKING:
|
|
14
14
|
from numpy.typing import NDArray
|
15
15
|
|
16
16
|
Number = int | float | complex | Fraction
|
17
|
-
Real = int | float | Fraction
|
18
17
|
BoolList = NDArray[np.bool_]
|
19
18
|
Node = tuple
|
20
19
|
|
@@ -831,12 +830,6 @@ def make_standard_env() -> dict[str, Any]:
|
|
831
830
|
check_args("xor", vals, (2, None), (is_bool,))
|
832
831
|
return reduce(lambda a, b: a ^ b, vals)
|
833
832
|
|
834
|
-
def string_ref(s: str, ref: int) -> Char:
|
835
|
-
try:
|
836
|
-
return Char(s[ref])
|
837
|
-
except IndexError:
|
838
|
-
raise MyError(f"string index {ref} is out of range")
|
839
|
-
|
840
833
|
def number_to_string(val: Number) -> str:
|
841
834
|
if isinstance(val, complex):
|
842
835
|
join = "" if val.imag < 0 else "+"
|
@@ -13,7 +13,10 @@ from auto_editor.utils.types import _split_num_str
|
|
13
13
|
|
14
14
|
|
15
15
|
def parse_bitrate(input_: str, log: Log) -> int:
|
16
|
-
|
16
|
+
try:
|
17
|
+
val, unit = _split_num_str(input_)
|
18
|
+
except Exception as e:
|
19
|
+
log.error(e)
|
17
20
|
|
18
21
|
if unit.lower() == "k":
|
19
22
|
return int(val * 1000)
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
3
3
|
import io
|
4
4
|
from pathlib import Path
|
5
5
|
from platform import system
|
6
|
-
from subprocess import PIPE
|
7
6
|
|
8
7
|
import av
|
9
8
|
import numpy as np
|
@@ -17,6 +16,7 @@ from auto_editor.output import Ensure
|
|
17
16
|
from auto_editor.timeline import TlAudio, v3
|
18
17
|
from auto_editor.utils.bar import Bar
|
19
18
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
19
|
+
from auto_editor.utils.container import Container
|
20
20
|
from auto_editor.utils.log import Log
|
21
21
|
from auto_editor.utils.types import Args
|
22
22
|
from auto_editor.wavfile import AudioData, read, write
|
@@ -122,8 +122,7 @@ def apply_audio_normalization(
|
|
122
122
|
"null",
|
123
123
|
file_null,
|
124
124
|
]
|
125
|
-
|
126
|
-
stderr = process.communicate()[1]
|
125
|
+
stderr = ffmpeg.Popen("EBU", cmd, log).communicate()[1]
|
127
126
|
name, filter_args = parse_ebu_bytes(norm, stderr, log)
|
128
127
|
else:
|
129
128
|
assert "t" in norm
|
@@ -238,12 +237,90 @@ def process_audio_clip(
|
|
238
237
|
return read(output_bytes)[1]
|
239
238
|
|
240
239
|
|
240
|
+
def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
241
|
+
mixed_audio = None
|
242
|
+
max_length = 0
|
243
|
+
|
244
|
+
# First pass: determine the maximum length
|
245
|
+
for path in audio_paths:
|
246
|
+
container = av.open(path)
|
247
|
+
stream = container.streams.audio[0]
|
248
|
+
|
249
|
+
# Calculate duration in samples
|
250
|
+
assert stream.duration is not None
|
251
|
+
assert stream.time_base is not None
|
252
|
+
duration_samples = int(stream.duration * sr / stream.time_base.denominator)
|
253
|
+
max_length = max(max_length, duration_samples)
|
254
|
+
container.close()
|
255
|
+
|
256
|
+
# Second pass: read and mix audio
|
257
|
+
for path in audio_paths:
|
258
|
+
container = av.open(path)
|
259
|
+
stream = container.streams.audio[0]
|
260
|
+
|
261
|
+
resampler = av.audio.resampler.AudioResampler(
|
262
|
+
format="s16", layout="mono", rate=sr
|
263
|
+
)
|
264
|
+
|
265
|
+
audio_array: list[np.ndarray] = []
|
266
|
+
for frame in container.decode(audio=0):
|
267
|
+
frame.pts = None
|
268
|
+
resampled = resampler.resample(frame)[0]
|
269
|
+
audio_array.extend(resampled.to_ndarray().flatten())
|
270
|
+
|
271
|
+
# Pad or truncate to max_length
|
272
|
+
current_audio = np.array(audio_array[:max_length])
|
273
|
+
if len(current_audio) < max_length:
|
274
|
+
current_audio = np.pad(
|
275
|
+
current_audio, (0, max_length - len(current_audio)), "constant"
|
276
|
+
)
|
277
|
+
|
278
|
+
if mixed_audio is None:
|
279
|
+
mixed_audio = current_audio.astype(np.float32)
|
280
|
+
else:
|
281
|
+
mixed_audio += current_audio.astype(np.float32)
|
282
|
+
|
283
|
+
container.close()
|
284
|
+
|
285
|
+
if mixed_audio is None:
|
286
|
+
raise ValueError("mixed_audio is None")
|
287
|
+
|
288
|
+
# Normalize the mixed audio
|
289
|
+
max_val = np.max(np.abs(mixed_audio))
|
290
|
+
if max_val > 0:
|
291
|
+
mixed_audio = mixed_audio * (32767 / max_val)
|
292
|
+
mixed_audio = mixed_audio.astype(np.int16) # type: ignore
|
293
|
+
|
294
|
+
output_container = av.open(output_path, mode="w")
|
295
|
+
output_stream = output_container.add_stream("pcm_s16le", rate=sr)
|
296
|
+
|
297
|
+
chunk_size = sr # Process 1 second at a time
|
298
|
+
for i in range(0, len(mixed_audio), chunk_size):
|
299
|
+
# Shape becomes (1, samples) for mono
|
300
|
+
chunk = np.array([mixed_audio[i : i + chunk_size]])
|
301
|
+
|
302
|
+
frame = av.AudioFrame.from_ndarray(chunk, format="s16", layout="mono")
|
303
|
+
frame.rate = sr
|
304
|
+
frame.pts = i # Set presentation timestamp
|
305
|
+
|
306
|
+
output_container.mux(output_stream.encode(frame))
|
307
|
+
|
308
|
+
output_container.mux(output_stream.encode(None))
|
309
|
+
output_container.close()
|
310
|
+
|
311
|
+
|
241
312
|
def make_new_audio(
|
242
|
-
tl: v3,
|
313
|
+
tl: v3,
|
314
|
+
ctr: Container,
|
315
|
+
ensure: Ensure,
|
316
|
+
args: Args,
|
317
|
+
ffmpeg: FFmpeg,
|
318
|
+
bar: Bar,
|
319
|
+
log: Log,
|
243
320
|
) -> list[str]:
|
244
321
|
sr = tl.sr
|
245
322
|
tb = tl.tb
|
246
|
-
output = []
|
323
|
+
output: list[str] = []
|
247
324
|
samples: dict[tuple[FileInfo, int], AudioData] = {}
|
248
325
|
|
249
326
|
norm = parse_norm(args.audio_normalize, log)
|
@@ -321,4 +398,9 @@ def make_new_audio(
|
|
321
398
|
Path(temp, "asdf.map").unlink(missing_ok=True)
|
322
399
|
except PermissionError:
|
323
400
|
pass
|
401
|
+
|
402
|
+
if not (args.keep_tracks_separate and ctr.max_audios is None) and len(output) > 1:
|
403
|
+
new_a_file = f"{Path(temp, 'new_audio.wav')}"
|
404
|
+
mix_audio_files(sr, output, new_a_file)
|
405
|
+
return [new_a_file]
|
324
406
|
return output
|
@@ -157,7 +157,7 @@ def make_srt(input_: Input, stream: int) -> str:
|
|
157
157
|
return output_bytes.getvalue()
|
158
158
|
|
159
159
|
|
160
|
-
def _ensure(input_: Input, format: str, stream: int
|
160
|
+
def _ensure(input_: Input, format: str, stream: int) -> str:
|
161
161
|
output_bytes = io.BytesIO()
|
162
162
|
output = av.open(output_bytes, "w", format=format)
|
163
163
|
|
@@ -187,7 +187,9 @@ def make_new_subtitles(tl: v3, log: Log) -> list[str]:
|
|
187
187
|
continue
|
188
188
|
|
189
189
|
parser = SubtitleParser(tl.tb)
|
190
|
-
if sub.codec
|
190
|
+
if sub.codec == "ssa":
|
191
|
+
format = "ass"
|
192
|
+
elif sub.codec in ("webvtt", "ass"):
|
191
193
|
format = sub.codec
|
192
194
|
else:
|
193
195
|
log.error(f"Unknown subtitle codec: {sub.codec}")
|
@@ -195,8 +197,8 @@ def make_new_subtitles(tl: v3, log: Log) -> list[str]:
|
|
195
197
|
if sub.codec == "mov_text":
|
196
198
|
ret = make_srt(input_, s)
|
197
199
|
else:
|
198
|
-
ret = _ensure(input_, format, s
|
199
|
-
parser.parse(ret,
|
200
|
+
ret = _ensure(input_, format, s)
|
201
|
+
parser.parse(ret, format)
|
200
202
|
parser.edit(tl.v1.chunks)
|
201
203
|
|
202
204
|
new_path = os.path.join(log.temp, f"new{s}s.{sub.ext}")
|
@@ -8,7 +8,6 @@ import numpy as np
|
|
8
8
|
|
9
9
|
from auto_editor.output import parse_bitrate
|
10
10
|
from auto_editor.timeline import TlImage, TlRect, TlVideo
|
11
|
-
from auto_editor.utils.types import color
|
12
11
|
|
13
12
|
if TYPE_CHECKING:
|
14
13
|
from collections.abc import Iterator
|
@@ -203,7 +202,7 @@ def render_av(
|
|
203
202
|
|
204
203
|
bar.start(tl.end, "Creating new video")
|
205
204
|
|
206
|
-
bg =
|
205
|
+
bg = args.background
|
207
206
|
null_frame = make_solid(target_width, target_height, target_pix_fmt, bg)
|
208
207
|
frame_index = -1
|
209
208
|
|
@@ -360,7 +360,7 @@ def main(sys_args: list[str] | None = None):
|
|
360
360
|
"""{"version": "1", "source": "example.mp4", "chunks": [ [0, 26, 1.0], [26, 34, 0] ]}"""
|
361
361
|
)
|
362
362
|
|
363
|
-
return run.main(["v1.json"], [])
|
363
|
+
return "v1.json", run.main(["v1.json"], [])
|
364
364
|
|
365
365
|
def premiere_named_export():
|
366
366
|
run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
6
6
|
from auto_editor.ffwrapper import initFileInfo, mux
|
7
7
|
from auto_editor.lib.contracts import *
|
8
8
|
from auto_editor.utils.cmdkw import Required, pAttr, pAttrs
|
9
|
-
from auto_editor.utils.types import
|
9
|
+
from auto_editor.utils.types import natural, number, parse_color, threshold
|
10
10
|
|
11
11
|
if TYPE_CHECKING:
|
12
12
|
from collections.abc import Iterator
|
@@ -165,7 +165,7 @@ rect_builder = pAttrs(
|
|
165
165
|
pAttr("y", Required, is_int, int),
|
166
166
|
pAttr("width", Required, is_int, int),
|
167
167
|
pAttr("height", Required, is_int, int),
|
168
|
-
pAttr("fill", "#c4c4c4", is_str,
|
168
|
+
pAttr("fill", "#c4c4c4", is_str, parse_color),
|
169
169
|
)
|
170
170
|
visual_objects = {
|
171
171
|
"rect": (TlRect, rect_builder),
|
@@ -35,11 +35,6 @@ class pAttrs:
|
|
35
35
|
self.attrs = attrs
|
36
36
|
|
37
37
|
|
38
|
-
def _norm_name(s: str) -> str:
|
39
|
-
# Python does not allow - in variable names
|
40
|
-
return s.replace("-", "_")
|
41
|
-
|
42
|
-
|
43
38
|
class PLexer:
|
44
39
|
__slots__ = ("text", "pos", "char")
|
45
40
|
|
@@ -101,6 +96,10 @@ def parse_with_palet(
|
|
101
96
|
KEYWORD_SEP = "="
|
102
97
|
kwargs: dict[str, Any] = {}
|
103
98
|
|
99
|
+
def _norm_name(s: str) -> str:
|
100
|
+
# Python does not allow - in variable names
|
101
|
+
return s.replace("-", "_")
|
102
|
+
|
104
103
|
def go(text: str, c: Any) -> Any:
|
105
104
|
try:
|
106
105
|
env = _env if isinstance(_env, Env) else Env(_env)
|
@@ -174,9 +173,7 @@ def parse_with_palet(
|
|
174
173
|
return kwargs
|
175
174
|
|
176
175
|
|
177
|
-
def parse_method(
|
178
|
-
name: str, text: str, env: Env
|
179
|
-
) -> tuple[str, list[Any], dict[str, Any]]:
|
176
|
+
def parse_method(name: str, text: str) -> tuple[str, list[Any], dict[str, Any]]:
|
180
177
|
from auto_editor.lang.palet import Lexer, Parser
|
181
178
|
|
182
179
|
# Positional Arguments
|
@@ -55,12 +55,9 @@ def codec_type(x: str) -> str:
|
|
55
55
|
return "subtitle"
|
56
56
|
|
57
57
|
try:
|
58
|
-
return Codec(x, "
|
58
|
+
return Codec(x, "w").type
|
59
59
|
except Exception:
|
60
|
-
|
61
|
-
return Codec(x, "w").type
|
62
|
-
except Exception:
|
63
|
-
return ""
|
60
|
+
return ""
|
64
61
|
|
65
62
|
|
66
63
|
def container_constructor(ext: str) -> Container:
|
@@ -81,21 +81,10 @@ def mut_margin(arr: BoolList, start_m: int, end_m: int) -> None:
|
|
81
81
|
arr[max(i + end_m, 0) : i] = False
|
82
82
|
|
83
83
|
|
84
|
-
def merge(start_list: np.ndarray, end_list: np.ndarray) -> BoolList:
|
85
|
-
result = np.zeros((len(start_list)), dtype=np.bool_)
|
86
|
-
|
87
|
-
for i, item in enumerate(start_list):
|
88
|
-
if item == True:
|
89
|
-
where = np.where(end_list[i:])[0]
|
90
|
-
if len(where) > 0:
|
91
|
-
result[i : where[0]] = True
|
92
|
-
return result
|
93
|
-
|
94
|
-
|
95
84
|
def get_stdout(cmd: list[str]) -> str:
|
96
85
|
from subprocess import DEVNULL, PIPE, Popen
|
97
86
|
|
98
|
-
stdout
|
87
|
+
stdout = Popen(cmd, stdin=DEVNULL, stdout=PIPE, stderr=PIPE).communicate()[0]
|
99
88
|
return stdout.decode("utf-8", "replace")
|
100
89
|
|
101
90
|
|
@@ -116,25 +105,3 @@ def aspect_ratio(width: int, height: int) -> tuple[int, int]:
|
|
116
105
|
|
117
106
|
c = gcd(width, height)
|
118
107
|
return width // c, height // c
|
119
|
-
|
120
|
-
|
121
|
-
def human_readable_time(time_in_secs: float) -> str:
|
122
|
-
units = "seconds"
|
123
|
-
if time_in_secs >= 3600:
|
124
|
-
time_in_secs = round(time_in_secs / 3600, 1)
|
125
|
-
if time_in_secs % 1 == 0:
|
126
|
-
time_in_secs = round(time_in_secs)
|
127
|
-
units = "hours"
|
128
|
-
if time_in_secs >= 60:
|
129
|
-
time_in_secs = round(time_in_secs / 60, 1)
|
130
|
-
if time_in_secs >= 10 or time_in_secs % 1 == 0:
|
131
|
-
time_in_secs = round(time_in_secs)
|
132
|
-
units = "minutes"
|
133
|
-
return f"{time_in_secs} {units}"
|
134
|
-
|
135
|
-
|
136
|
-
def append_filename(path: str, val: str) -> str:
|
137
|
-
from os.path import splitext
|
138
|
-
|
139
|
-
root, ext = splitext(path)
|
140
|
-
return root + val + ext
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
3
3
|
import re
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
from fractions import Fraction
|
6
|
-
from typing import Literal
|
7
6
|
|
8
7
|
|
9
8
|
class CoerceError(Exception):
|
@@ -156,16 +155,7 @@ def speed_range(val: str) -> tuple[float, str, str]:
|
|
156
155
|
return number(a[0]), a[1], a[2]
|
157
156
|
|
158
157
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
def stream(val: str) -> Stream:
|
163
|
-
if val == "all" or val == "'all":
|
164
|
-
return "all"
|
165
|
-
return natural(val)
|
166
|
-
|
167
|
-
|
168
|
-
def color(val: str) -> str:
|
158
|
+
def parse_color(val: str) -> str:
|
169
159
|
"""
|
170
160
|
Convert a color str into an RGB tuple
|
171
161
|
|
@@ -219,7 +209,7 @@ class Args:
|
|
219
209
|
frame_rate: Fraction | None = None
|
220
210
|
sample_rate: int | None = None
|
221
211
|
resolution: tuple[int, int] | None = None
|
222
|
-
background: str = "#
|
212
|
+
background: str = "#000000"
|
223
213
|
edit_based_on: str = "audio"
|
224
214
|
keep_tracks_separate: bool = False
|
225
215
|
audio_normalize: str = "#f"
|
@@ -228,13 +218,10 @@ class Args:
|
|
228
218
|
no_open: bool = False
|
229
219
|
temp_dir: str | None = None
|
230
220
|
ffmpeg_location: str | None = None
|
231
|
-
my_ffmpeg: bool = False
|
232
221
|
progress: str = "modern"
|
233
222
|
version: bool = False
|
234
223
|
debug: bool = False
|
235
224
|
config: bool = False
|
236
|
-
show_ffmpeg_commands: bool = False
|
237
|
-
show_ffmpeg_output: bool = False
|
238
225
|
quiet: bool = False
|
239
226
|
preview: bool = False
|
240
227
|
no_cache: bool = False
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 26.0.
|
3
|
+
Version: 26.0.1
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -13,7 +13,6 @@ Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
14
14
|
Requires-Dist: numpy<3.0,>=1.23.0
|
15
15
|
Requires-Dist: pyav==13.1.*
|
16
|
-
Requires-Dist: ae-ffmpeg==1.2.*
|
17
16
|
|
18
17
|
<p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
|
19
18
|
|
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "26.0.0"
|
@@ -1,135 +0,0 @@
|
|
1
|
-
encoders = {
|
2
|
-
"libx264": (
|
3
|
-
"yuv420p",
|
4
|
-
"yuvj420p",
|
5
|
-
"yuv422p",
|
6
|
-
"yuvj422p",
|
7
|
-
"yuv444p",
|
8
|
-
"yuvj444p",
|
9
|
-
"nv12",
|
10
|
-
"nv16",
|
11
|
-
"nv21",
|
12
|
-
"yuv420p10le",
|
13
|
-
"yuv422p10le",
|
14
|
-
"yuv444p10le",
|
15
|
-
"nv20le",
|
16
|
-
"gray",
|
17
|
-
"gray10le",
|
18
|
-
),
|
19
|
-
"libx264rgb": ("bgr0", "bgr24", "rgb24"),
|
20
|
-
"h264_videotoolbox": ("videotoolbox_vld", "nv12", "yuv420p"),
|
21
|
-
"h264": ("videotoolbox_vld", "nv12", "yuv420p"),
|
22
|
-
"libx265": (
|
23
|
-
"yuv420p",
|
24
|
-
"yuvj420p",
|
25
|
-
"yuv422p",
|
26
|
-
"yuvj422p",
|
27
|
-
"yuv444p",
|
28
|
-
"yuvj444p",
|
29
|
-
"gbrp",
|
30
|
-
"yuv420p10le",
|
31
|
-
"yuv422p10le",
|
32
|
-
"yuv444p10le",
|
33
|
-
"gbrp10le",
|
34
|
-
"yuv420p12le",
|
35
|
-
"yuv422p12le",
|
36
|
-
"yuv444p12le",
|
37
|
-
"gbrp12le",
|
38
|
-
"gray",
|
39
|
-
"gray10le",
|
40
|
-
"gray12le",
|
41
|
-
),
|
42
|
-
"hevc_videotoolbox": ("videotoolbox_vld", "nv12", "yuv420p", "bgra", "p010le"),
|
43
|
-
"hevc": (
|
44
|
-
"yuv420p",
|
45
|
-
"yuvj420p",
|
46
|
-
"yuv422p",
|
47
|
-
"yuvj422p",
|
48
|
-
"yuv444p",
|
49
|
-
"yuvj444p",
|
50
|
-
"gbrp",
|
51
|
-
"yuv420p10le",
|
52
|
-
"yuv422p10le",
|
53
|
-
"yuv444p10le",
|
54
|
-
"gbrp10le",
|
55
|
-
"yuv420p12le",
|
56
|
-
"yuv422p12le",
|
57
|
-
"yuv444p12le",
|
58
|
-
"gbrp12le",
|
59
|
-
"gray",
|
60
|
-
"gray10le",
|
61
|
-
"gray12le",
|
62
|
-
),
|
63
|
-
"hevc_nvenc": (
|
64
|
-
"yuv420p",
|
65
|
-
"nv12",
|
66
|
-
"p010le",
|
67
|
-
"yuv444p",
|
68
|
-
"p016le",
|
69
|
-
"yuv444p16le",
|
70
|
-
"bgr0",
|
71
|
-
"rgb0",
|
72
|
-
"gbrp",
|
73
|
-
"gbrp16le",
|
74
|
-
"cuda",
|
75
|
-
"d3d11",
|
76
|
-
),
|
77
|
-
"hevc_amf": ("yuv420p", "nv12", "d3d11", "dxva2_vld"),
|
78
|
-
"h264_nvenc": (
|
79
|
-
"yuv420p",
|
80
|
-
"nv12",
|
81
|
-
"p010le",
|
82
|
-
"yuv444p",
|
83
|
-
"p016le",
|
84
|
-
"yuv444p16le",
|
85
|
-
"bgr0",
|
86
|
-
"rgb0",
|
87
|
-
"gbrp",
|
88
|
-
"gbrp16le",
|
89
|
-
"cuda",
|
90
|
-
"d3d11",
|
91
|
-
),
|
92
|
-
"h264_amf": ("yuv420p", "nv12", "d3d11", "dxva2_vld"),
|
93
|
-
"hevc_qsv": ("nv12", "p010le", "yuyv422", "y210le", "qsv", "bgra", "x2rgb10le"),
|
94
|
-
"h264_qsv": ("nv12", "p010le", "qsv"),
|
95
|
-
"vp9": (
|
96
|
-
"yuv420p",
|
97
|
-
"yuva420p",
|
98
|
-
"yuv422p",
|
99
|
-
"yuv440p",
|
100
|
-
"yuv444p",
|
101
|
-
"yuv420p10le",
|
102
|
-
"yuv422p10le",
|
103
|
-
"yuv440p10le",
|
104
|
-
"yuv444p10le",
|
105
|
-
"yuv420p12le",
|
106
|
-
"yuv422p12le",
|
107
|
-
"yuv440p12le",
|
108
|
-
"yuv444p12le",
|
109
|
-
"gbrp",
|
110
|
-
"gbrp10le",
|
111
|
-
"gbrp12le",
|
112
|
-
),
|
113
|
-
"vp8": ("yuv420p", "yuva420p"),
|
114
|
-
"prores": ("yuv422p10le", "yuv444p10le", "yuva444p10le"),
|
115
|
-
"av1": (
|
116
|
-
"yuv420p",
|
117
|
-
"yuv422p",
|
118
|
-
"yuv444p",
|
119
|
-
"gbrp",
|
120
|
-
"yuv420p10le",
|
121
|
-
"yuv422p10le",
|
122
|
-
"yuv444p10le",
|
123
|
-
"yuv420p12le",
|
124
|
-
"yuv422p12le",
|
125
|
-
"yuv444p12le",
|
126
|
-
"gbrp10le",
|
127
|
-
"gbrp12le",
|
128
|
-
"gray",
|
129
|
-
"gray10le",
|
130
|
-
"gray12le",
|
131
|
-
),
|
132
|
-
"mpeg4": ("yuv420p"),
|
133
|
-
"mpeg2video": ("yuv420p", "yuv422p"),
|
134
|
-
"mjpeg": ("yuvj420p", "yuvj422p", "yuvj444p"),
|
135
|
-
}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|