auto-editor 27.0.0__tar.gz → 27.1.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-27.0.0 → auto_editor-27.1.1}/PKG-INFO +2 -2
- auto_editor-27.1.1/auto_editor/__init__.py +1 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/__main__.py +8 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/desc.py +2 -2
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/info.py +2 -2
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/levels.py +2 -2
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/repl.py +3 -8
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/test.py +36 -2
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/edit.py +51 -88
- auto_editor-27.1.1/auto_editor/ffwrapper.py +178 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/formats/fcp11.py +10 -8
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/formats/fcp7.py +23 -64
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/formats/json.py +8 -9
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lang/stdenv.py +1 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/make_layers.py +18 -8
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/render/audio.py +219 -84
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/render/video.py +1 -2
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/timeline.py +69 -16
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/utils/container.py +19 -12
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/utils/func.py +21 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor.egg-info/PKG-INFO +2 -2
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor.egg-info/SOURCES.txt +0 -2
- {auto_editor-27.0.0 → auto_editor-27.1.1}/pyproject.toml +2 -2
- auto_editor-27.0.0/auto_editor/__init__.py +0 -1
- auto_editor-27.0.0/auto_editor/ffwrapper.py +0 -174
- auto_editor-27.0.0/auto_editor/output.py +0 -86
- auto_editor-27.0.0/auto_editor/wavfile.py +0 -310
- {auto_editor-27.0.0 → auto_editor-27.1.1}/LICENSE +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/README.md +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/analyze.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/__init__.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/cache.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/palet.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/cmds/subdump.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/formats/__init__.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/formats/shotcut.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/formats/utils.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/help.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/json.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lang/__init__.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lang/libintrospection.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lang/libmath.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lang/palet.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lib/__init__.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lib/contracts.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lib/data_structs.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/lib/err.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/preview.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/render/__init__.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/render/subtitle.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/utils/__init__.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/utils/bar.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/utils/chunks.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/utils/cmdkw.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/utils/log.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/utils/types.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor/vanparse.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor.egg-info/dependency_links.txt +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor.egg-info/entry_points.txt +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor.egg-info/requires.txt +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/auto_editor.egg-info/top_level.txt +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/docs/build.py +0 -0
- {auto_editor-27.0.0 → auto_editor-27.1.1}/setup.cfg +0 -0
@@ -1,9 +1,9 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 27.
|
3
|
+
Version: 27.1.1
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
|
-
License: Unlicense
|
6
|
+
License-Expression: Unlicense
|
7
7
|
Project-URL: Bug Tracker, https://github.com/WyattBlue/auto-editor/issues
|
8
8
|
Project-URL: Source Code, https://github.com/WyattBlue/auto-editor
|
9
9
|
Project-URL: homepage, https://auto-editor.com
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "27.1.1"
|
@@ -75,6 +75,7 @@ class Args:
|
|
75
75
|
|
76
76
|
# Audio Rendering
|
77
77
|
audio_codec: str = "auto"
|
78
|
+
audio_layout: str | None = None
|
78
79
|
audio_bitrate: str = "auto"
|
79
80
|
mix_audio_streams: bool = False
|
80
81
|
keep_tracks_separate: bool = False
|
@@ -336,6 +337,13 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
336
337
|
metavar="ENCODER",
|
337
338
|
help="Set audio codec for output media",
|
338
339
|
)
|
340
|
+
parser.add_argument(
|
341
|
+
"--audio-layout",
|
342
|
+
"-channel-layout",
|
343
|
+
"-layout",
|
344
|
+
metavar="LAYOUT",
|
345
|
+
help="Set the audio layout for the output media/timeline",
|
346
|
+
)
|
339
347
|
parser.add_argument(
|
340
348
|
"--audio-bitrate",
|
341
349
|
"-b:a",
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import sys
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
|
6
|
-
from auto_editor.ffwrapper import
|
6
|
+
from auto_editor.ffwrapper import FileInfo
|
7
7
|
from auto_editor.utils.log import Log
|
8
8
|
from auto_editor.vanparse import ArgumentParser
|
9
9
|
|
@@ -22,7 +22,7 @@ def desc_options(parser: ArgumentParser) -> ArgumentParser:
|
|
22
22
|
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
23
23
|
args = desc_options(ArgumentParser("desc")).parse_args(DescArgs, sys_args)
|
24
24
|
for path in args.input:
|
25
|
-
src =
|
25
|
+
src = FileInfo.init(path, Log())
|
26
26
|
if src.description is not None:
|
27
27
|
sys.stdout.write(f"\n{src.description}\n\n")
|
28
28
|
else:
|
@@ -5,7 +5,7 @@ import sys
|
|
5
5
|
from dataclasses import dataclass, field
|
6
6
|
from typing import Any, Literal, TypedDict
|
7
7
|
|
8
|
-
from auto_editor.ffwrapper import
|
8
|
+
from auto_editor.ffwrapper import FileInfo
|
9
9
|
from auto_editor.json import dump
|
10
10
|
from auto_editor.make_layers import make_sane_timebase
|
11
11
|
from auto_editor.timeline import v3
|
@@ -102,7 +102,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
102
102
|
file_info[file] = {"type": "timeline"}
|
103
103
|
continue
|
104
104
|
|
105
|
-
src =
|
105
|
+
src = FileInfo.init(file, log)
|
106
106
|
|
107
107
|
if len(src.videos) + len(src.audios) + len(src.subtitles) == 0:
|
108
108
|
file_info[file] = {"type": "unknown"}
|
@@ -9,7 +9,7 @@ import bv
|
|
9
9
|
import numpy as np
|
10
10
|
|
11
11
|
from auto_editor.analyze import *
|
12
|
-
from auto_editor.ffwrapper import
|
12
|
+
from auto_editor.ffwrapper import FileInfo
|
13
13
|
from auto_editor.lang.palet import env
|
14
14
|
from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
|
15
15
|
from auto_editor.utils.bar import initBar
|
@@ -87,7 +87,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
87
87
|
bar = initBar("none")
|
88
88
|
log = Log(quiet=True)
|
89
89
|
|
90
|
-
sources = [
|
90
|
+
sources = [FileInfo.init(path, log) for path in args.input]
|
91
91
|
if len(sources) < 1:
|
92
92
|
log.error("levels needs at least one input file")
|
93
93
|
|
@@ -7,7 +7,7 @@ from os import environ
|
|
7
7
|
|
8
8
|
import auto_editor
|
9
9
|
from auto_editor.analyze import initLevels
|
10
|
-
from auto_editor.ffwrapper import
|
10
|
+
from auto_editor.ffwrapper import FileInfo
|
11
11
|
from auto_editor.lang.palet import ClosingError, Lexer, Parser, env, interpret
|
12
12
|
from auto_editor.lang.stdenv import make_standard_env
|
13
13
|
from auto_editor.lib.data_structs import print_str
|
@@ -48,11 +48,6 @@ def repl_options(parser: ArgumentParser) -> ArgumentParser:
|
|
48
48
|
type=frame_rate,
|
49
49
|
help="Set custom timebase",
|
50
50
|
)
|
51
|
-
parser.add_argument(
|
52
|
-
"--temp-dir",
|
53
|
-
metavar="PATH",
|
54
|
-
help="Set where the temporary directory is located",
|
55
|
-
)
|
56
51
|
return parser
|
57
52
|
|
58
53
|
|
@@ -60,8 +55,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
60
55
|
args = repl_options(ArgumentParser(None)).parse_args(REPL_Args, sys_args)
|
61
56
|
|
62
57
|
if args.input:
|
63
|
-
log = Log(quiet=True
|
64
|
-
sources = [
|
58
|
+
log = Log(quiet=True)
|
59
|
+
sources = [FileInfo.init(path, log) for path in args.input]
|
65
60
|
src = sources[0]
|
66
61
|
tb = src.get_fps() if args.timebase is None else args.timebase
|
67
62
|
env["timebase"] = tb
|
@@ -14,7 +14,7 @@ from time import perf_counter
|
|
14
14
|
import bv
|
15
15
|
import numpy as np
|
16
16
|
|
17
|
-
from auto_editor.ffwrapper import FileInfo
|
17
|
+
from auto_editor.ffwrapper import FileInfo
|
18
18
|
from auto_editor.lang.palet import Lexer, Parser, env, interpret
|
19
19
|
from auto_editor.lang.stdenv import make_standard_env
|
20
20
|
from auto_editor.lib.data_structs import Char
|
@@ -62,7 +62,7 @@ log = Log()
|
|
62
62
|
|
63
63
|
|
64
64
|
def fileinfo(path: str) -> FileInfo:
|
65
|
-
return
|
65
|
+
return FileInfo.init(path, log)
|
66
66
|
|
67
67
|
|
68
68
|
def calculate_sha256(filename: str) -> str:
|
@@ -186,6 +186,7 @@ class Runner:
|
|
186
186
|
assert container.duration is not None
|
187
187
|
assert container.duration > 17300000 and container.duration < 2 << 24
|
188
188
|
|
189
|
+
assert len(container.streams) == 2
|
189
190
|
video = container.streams[0]
|
190
191
|
audio = container.streams[1]
|
191
192
|
assert isinstance(video, bv.VideoStream)
|
@@ -199,6 +200,29 @@ class Runner:
|
|
199
200
|
assert audio.codec.name == "aac"
|
200
201
|
assert audio.sample_rate == 48000
|
201
202
|
assert audio.language == "eng"
|
203
|
+
assert audio.layout.name == "stereo"
|
204
|
+
|
205
|
+
def test_to_mono(self) -> None:
|
206
|
+
out = self.main(["example.mp4"], ["-layout", "mono"], output="example_mono.mp4")
|
207
|
+
with bv.open(out) as container:
|
208
|
+
assert container.duration is not None
|
209
|
+
assert container.duration > 17300000 and container.duration < 2 << 24
|
210
|
+
|
211
|
+
assert len(container.streams) == 2
|
212
|
+
video = container.streams[0]
|
213
|
+
audio = container.streams[1]
|
214
|
+
assert isinstance(video, bv.VideoStream)
|
215
|
+
assert isinstance(audio, bv.AudioStream)
|
216
|
+
assert video.base_rate == 30
|
217
|
+
assert video.average_rate is not None
|
218
|
+
assert video.average_rate == 30, video.average_rate
|
219
|
+
assert (video.width, video.height) == (1280, 720)
|
220
|
+
assert video.codec.name == "h264"
|
221
|
+
assert video.language == "eng"
|
222
|
+
assert audio.codec.name == "aac"
|
223
|
+
assert audio.sample_rate == 48000
|
224
|
+
assert audio.language == "eng"
|
225
|
+
assert audio.layout.name == "mono"
|
202
226
|
|
203
227
|
# PR #260
|
204
228
|
def test_high_speed(self):
|
@@ -332,8 +356,18 @@ class Runner:
|
|
332
356
|
assert cn.videos[0].height == 380
|
333
357
|
assert cn.audios[0].samplerate == 48000
|
334
358
|
|
359
|
+
# def test_premiere_multi(self):
|
360
|
+
# p_xml = self.main([f"resources/multi-track.mov"], ["-exp"], "multi.xml")
|
361
|
+
|
362
|
+
# cn = fileinfo(self.main([p_xml], []))
|
363
|
+
# assert len(cn.videos) == 1
|
364
|
+
# assert len(cn.audios) == 2
|
365
|
+
|
335
366
|
def test_premiere(self):
|
336
367
|
for test_name in all_files:
|
368
|
+
if test_name == "multi-track.mov":
|
369
|
+
continue
|
370
|
+
|
337
371
|
p_xml = self.main([f"resources/{test_name}"], ["-exp"], "out.xml")
|
338
372
|
self.main([p_xml], [])
|
339
373
|
|
@@ -5,16 +5,15 @@ import sys
|
|
5
5
|
from fractions import Fraction
|
6
6
|
from heapq import heappop, heappush
|
7
7
|
from os.path import splitext
|
8
|
+
from pathlib import Path
|
8
9
|
from subprocess import run
|
9
10
|
from typing import TYPE_CHECKING, Any
|
10
11
|
|
11
12
|
import bv
|
12
|
-
from bv import AudioResampler, Codec
|
13
13
|
|
14
|
-
from auto_editor.ffwrapper import FileInfo
|
14
|
+
from auto_editor.ffwrapper import FileInfo
|
15
15
|
from auto_editor.lib.contracts import is_int, is_str
|
16
16
|
from auto_editor.make_layers import clipify, make_av, make_timeline
|
17
|
-
from auto_editor.output import Ensure, parse_bitrate
|
18
17
|
from auto_editor.render.audio import make_new_audio
|
19
18
|
from auto_editor.render.subtitle import make_new_subtitles
|
20
19
|
from auto_editor.render.video import render_av
|
@@ -30,14 +29,18 @@ if TYPE_CHECKING:
|
|
30
29
|
|
31
30
|
|
32
31
|
def set_output(
|
33
|
-
out: str | None, _export: str | None,
|
32
|
+
out: str | None, _export: str | None, path: Path | None, log: Log
|
34
33
|
) -> tuple[str, dict[str, Any]]:
|
35
|
-
if
|
36
|
-
|
34
|
+
if out is None:
|
35
|
+
if path is None:
|
36
|
+
log.error("`--output` must be set.") # When a timeline file is the input.
|
37
|
+
root, ext = splitext(path)
|
37
38
|
else:
|
38
|
-
root, ext = splitext(
|
39
|
-
|
40
|
-
|
39
|
+
root, ext = splitext(out)
|
40
|
+
|
41
|
+
if ext == "":
|
42
|
+
# Use `mp4` as the default, because it is most compatible.
|
43
|
+
ext = ".mp4" if path is None else path.suffix
|
41
44
|
|
42
45
|
if _export is None:
|
43
46
|
if ext == ".xml":
|
@@ -85,11 +88,11 @@ def set_video_codec(
|
|
85
88
|
|
86
89
|
if ctr.vcodecs is not None and codec not in ctr.vcodecs:
|
87
90
|
try:
|
88
|
-
cobj = Codec(codec, "w")
|
91
|
+
cobj = bv.Codec(codec, "w")
|
89
92
|
except bv.codec.codec.UnknownCodecError:
|
90
93
|
log.error(f"Unknown encoder: {codec}")
|
91
94
|
# Normalize encoder names
|
92
|
-
if cobj.id not in (Codec(x, "w").id for x in ctr.vcodecs):
|
95
|
+
if cobj.id not in (bv.Codec(x, "w").id for x in ctr.vcodecs):
|
93
96
|
log.error(codec_error.format(codec, out_ext))
|
94
97
|
|
95
98
|
return codec
|
@@ -113,11 +116,11 @@ def set_audio_codec(
|
|
113
116
|
|
114
117
|
if ctr.acodecs is None or codec not in ctr.acodecs:
|
115
118
|
try:
|
116
|
-
cobj = Codec(codec, "w")
|
119
|
+
cobj = bv.Codec(codec, "w")
|
117
120
|
except bv.codec.codec.UnknownCodecError:
|
118
121
|
log.error(f"Unknown encoder: {codec}")
|
119
122
|
# Normalize encoder names
|
120
|
-
if cobj.id not in (Codec(x, "w").id for x in ctr.acodecs):
|
123
|
+
if cobj.id not in (bv.Codec(x, "w").id for x in ctr.acodecs):
|
121
124
|
log.error(codec_error.format(codec, out_ext))
|
122
125
|
|
123
126
|
return codec
|
@@ -160,7 +163,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
160
163
|
|
161
164
|
def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
162
165
|
bar = initBar(args.progress)
|
163
|
-
tl = None
|
166
|
+
tl = src = use_path = None
|
164
167
|
|
165
168
|
if args.keep_tracks_separate:
|
166
169
|
log.deprecated("--keep-tracks-separate is deprecated.")
|
@@ -172,31 +175,20 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
172
175
|
from auto_editor.formats.fcp7 import fcp7_read_xml
|
173
176
|
|
174
177
|
tl = fcp7_read_xml(paths[0], log)
|
175
|
-
assert tl.src is not None
|
176
|
-
sources: list[FileInfo] = [tl.src]
|
177
|
-
src: FileInfo | None = tl.src
|
178
|
-
|
179
178
|
elif path_ext == ".mlt":
|
180
179
|
from auto_editor.formats.shotcut import shotcut_read_mlt
|
181
180
|
|
182
181
|
tl = shotcut_read_mlt(paths[0], log)
|
183
|
-
assert tl.src is not None
|
184
|
-
sources = [tl.src]
|
185
|
-
src = tl.src
|
186
|
-
|
187
182
|
elif path_ext == ".json":
|
188
183
|
from auto_editor.formats.json import read_json
|
189
184
|
|
190
185
|
tl = read_json(paths[0], log)
|
191
|
-
sources = [] if tl.src is None else [tl.src]
|
192
|
-
src = tl.src
|
193
186
|
else:
|
194
|
-
sources = [
|
195
|
-
src =
|
196
|
-
|
197
|
-
del paths
|
187
|
+
sources = [FileInfo.init(path, log) for path in paths]
|
188
|
+
src = sources[0]
|
189
|
+
use_path = src.path
|
198
190
|
|
199
|
-
output, export_ops = set_output(args.output, args.export,
|
191
|
+
output, export_ops = set_output(args.output, args.export, use_path, log)
|
200
192
|
assert "export" in export_ops
|
201
193
|
export = export_ops["export"]
|
202
194
|
|
@@ -256,7 +248,8 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
256
248
|
from auto_editor.formats.fcp11 import fcp11_write_xml
|
257
249
|
from auto_editor.timeline import set_stream_to_0
|
258
250
|
|
259
|
-
|
251
|
+
assert src is not None
|
252
|
+
set_stream_to_0(src, tl, log)
|
260
253
|
fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
|
261
254
|
return
|
262
255
|
|
@@ -269,7 +262,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
269
262
|
out_ext = splitext(output)[1].replace(".", "")
|
270
263
|
|
271
264
|
# Check if export options make sense.
|
272
|
-
ctr = container_constructor(out_ext.lower())
|
265
|
+
ctr = container_constructor(out_ext.lower(), log)
|
273
266
|
|
274
267
|
if ctr.samplerate is not None and args.sample_rate not in ctr.samplerate:
|
275
268
|
log.error(f"'{out_ext}' container only supports samplerates: {ctr.samplerate}")
|
@@ -278,8 +271,6 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
278
271
|
args.audio_codec = set_audio_codec(args.audio_codec, src, out_ext, ctr, log)
|
279
272
|
|
280
273
|
def make_media(tl: v3, output_path: str) -> None:
|
281
|
-
assert src is not None
|
282
|
-
|
283
274
|
options = {}
|
284
275
|
mov_flags = []
|
285
276
|
if args.fragmented and not args.no_fragmented:
|
@@ -303,51 +294,29 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
303
294
|
output_stream, vframes = None, iter([])
|
304
295
|
|
305
296
|
# Setup audio
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
log.warning("Dropping extra audio streams (container only allows one)")
|
314
|
-
audio_paths = audio_paths[0:1]
|
315
|
-
|
316
|
-
if audio_paths:
|
317
|
-
try:
|
318
|
-
audio_encoder = bv.Codec(args.audio_codec, "w")
|
319
|
-
except bv.FFmpegError as e:
|
320
|
-
log.error(e)
|
321
|
-
if audio_encoder.audio_formats is None:
|
322
|
-
log.error(f"{args.audio_codec}: No known audio formats avail.")
|
323
|
-
audio_format = audio_encoder.audio_formats[0]
|
324
|
-
resampler = AudioResampler(format=audio_format, layout="stereo", rate=tl.sr)
|
297
|
+
try:
|
298
|
+
audio_encoder = bv.Codec(args.audio_codec, "w")
|
299
|
+
except bv.FFmpegError as e:
|
300
|
+
log.error(e)
|
301
|
+
if audio_encoder.audio_formats is None:
|
302
|
+
log.error(f"{args.audio_codec}: No known audio formats avail.")
|
303
|
+
fmt = audio_encoder.audio_formats[0]
|
325
304
|
|
326
305
|
audio_streams: list[bv.AudioStream] = []
|
327
|
-
audio_inputs = []
|
328
|
-
audio_gen_frames = []
|
329
|
-
for i, audio_path in enumerate(audio_paths):
|
330
|
-
audio_stream = output.add_stream(
|
331
|
-
args.audio_codec,
|
332
|
-
format=audio_format,
|
333
|
-
rate=tl.sr,
|
334
|
-
time_base=Fraction(1, tl.sr),
|
335
|
-
)
|
336
|
-
if not isinstance(audio_stream, bv.AudioStream):
|
337
|
-
log.error(f"Not a known audio codec: {args.audio_codec}")
|
338
306
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
307
|
+
if ctr.default_aud == "none":
|
308
|
+
while len(tl.a) > 0:
|
309
|
+
tl.a.pop()
|
310
|
+
elif len(tl.a) > 1 and ctr.max_audios == 1:
|
311
|
+
log.warning("Dropping extra audio streams (container only allows one)")
|
312
|
+
|
313
|
+
while len(tl.a) > 1:
|
314
|
+
tl.a.pop()
|
346
315
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
audio_gen_frames
|
316
|
+
if len(tl.a) > 0:
|
317
|
+
audio_streams, audio_gen_frames = make_new_audio(output, fmt, tl, args, log)
|
318
|
+
else:
|
319
|
+
audio_streams, audio_gen_frames = [], [iter([])]
|
351
320
|
|
352
321
|
# Setup subtitles
|
353
322
|
if ctr.default_sub != "none" and not args.sn:
|
@@ -365,8 +334,8 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
365
334
|
subtitle_stream = output.add_stream_from_template(
|
366
335
|
subtitle_input.streams.subtitles[0]
|
367
336
|
)
|
368
|
-
if i < len(
|
369
|
-
subtitle_stream.metadata["language"] =
|
337
|
+
if i < len(tl.T.subtitles) and (lang := tl.T.subtitles[i].lang) is not None:
|
338
|
+
subtitle_stream.metadata["language"] = lang
|
370
339
|
|
371
340
|
subtitle_streams.append(subtitle_stream)
|
372
341
|
sub_gen_frames.append(subtitle_input.demux(subtitles=0))
|
@@ -463,13 +432,11 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
463
432
|
break
|
464
433
|
|
465
434
|
if should_get_audio:
|
466
|
-
for audio_stream,
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
Priority(reframe.pts, reframe, audio_stream),
|
472
|
-
)
|
435
|
+
for audio_stream, aframe in zip(audio_streams, audio_frames):
|
436
|
+
if aframe is None:
|
437
|
+
continue
|
438
|
+
assert aframe.pts is not None
|
439
|
+
heappush(frame_queue, Priority(aframe.pts, aframe, audio_stream))
|
473
440
|
if should_get_sub:
|
474
441
|
for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
|
475
442
|
if packet and packet.pts is not None:
|
@@ -511,8 +478,6 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
511
478
|
bar.end()
|
512
479
|
|
513
480
|
# Close resources
|
514
|
-
for audio_input in audio_inputs:
|
515
|
-
audio_input.close()
|
516
481
|
for subtitle_input in subtitle_inputs:
|
517
482
|
subtitle_input.close()
|
518
483
|
output.close()
|
@@ -542,11 +507,9 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
542
507
|
tl.v1.source, [clipify(padded_chunks, tl.v1.source)]
|
543
508
|
)
|
544
509
|
my_timeline = v3(
|
545
|
-
tl.v1.source,
|
546
510
|
tl.tb,
|
547
|
-
tl.sr,
|
548
|
-
tl.res,
|
549
511
|
"#000",
|
512
|
+
tl.template,
|
550
513
|
vspace,
|
551
514
|
aspace,
|
552
515
|
v1(tl.v1.source, padded_chunks),
|
@@ -0,0 +1,178 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from fractions import Fraction
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import bv
|
8
|
+
|
9
|
+
from auto_editor.utils.log import Log
|
10
|
+
|
11
|
+
|
12
|
+
def mux(input: Path, output: Path, stream: int) -> None:
|
13
|
+
input_container = bv.open(input, "r")
|
14
|
+
output_container = bv.open(output, "w")
|
15
|
+
|
16
|
+
input_audio_stream = input_container.streams.audio[stream]
|
17
|
+
output_audio_stream = output_container.add_stream("pcm_s16le")
|
18
|
+
|
19
|
+
for frame in input_container.decode(input_audio_stream):
|
20
|
+
output_container.mux(output_audio_stream.encode(frame))
|
21
|
+
|
22
|
+
output_container.mux(output_audio_stream.encode(None))
|
23
|
+
|
24
|
+
output_container.close()
|
25
|
+
input_container.close()
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass(slots=True, frozen=True)
|
29
|
+
class VideoStream:
|
30
|
+
width: int
|
31
|
+
height: int
|
32
|
+
codec: str
|
33
|
+
fps: Fraction
|
34
|
+
duration: float
|
35
|
+
sar: Fraction
|
36
|
+
time_base: Fraction | None
|
37
|
+
pix_fmt: str | None
|
38
|
+
color_range: int
|
39
|
+
color_space: int
|
40
|
+
color_primaries: int
|
41
|
+
color_transfer: int
|
42
|
+
bitrate: int
|
43
|
+
lang: str | None
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass(slots=True, frozen=True)
|
47
|
+
class AudioStream:
|
48
|
+
codec: str
|
49
|
+
samplerate: int
|
50
|
+
layout: str
|
51
|
+
channels: int
|
52
|
+
duration: float
|
53
|
+
bitrate: int
|
54
|
+
lang: str | None
|
55
|
+
|
56
|
+
|
57
|
+
@dataclass(slots=True, frozen=True)
|
58
|
+
class SubtitleStream:
|
59
|
+
codec: str
|
60
|
+
ext: str
|
61
|
+
lang: str | None
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass(slots=True, frozen=True)
|
65
|
+
class FileInfo:
|
66
|
+
path: Path
|
67
|
+
bitrate: int
|
68
|
+
duration: float
|
69
|
+
description: str | None
|
70
|
+
videos: tuple[VideoStream, ...]
|
71
|
+
audios: tuple[AudioStream, ...]
|
72
|
+
subtitles: tuple[SubtitleStream, ...]
|
73
|
+
|
74
|
+
def get_res(self) -> tuple[int, int]:
|
75
|
+
if self.videos:
|
76
|
+
return self.videos[0].width, self.videos[0].height
|
77
|
+
return 1920, 1080
|
78
|
+
|
79
|
+
def get_fps(self) -> Fraction:
|
80
|
+
if self.videos:
|
81
|
+
return self.videos[0].fps
|
82
|
+
return Fraction(30)
|
83
|
+
|
84
|
+
def get_sr(self) -> int:
|
85
|
+
if self.audios:
|
86
|
+
return self.audios[0].samplerate
|
87
|
+
return 48000
|
88
|
+
|
89
|
+
@classmethod
|
90
|
+
def init(self, path: str, log: Log) -> FileInfo:
|
91
|
+
try:
|
92
|
+
cont = bv.open(path, "r")
|
93
|
+
except bv.error.FileNotFoundError:
|
94
|
+
log.error(f"Input file doesn't exist: {path}")
|
95
|
+
except bv.error.IsADirectoryError:
|
96
|
+
log.error(f"Expected a media file, but got a directory: {path}")
|
97
|
+
except bv.error.InvalidDataError:
|
98
|
+
log.error(f"Invalid data when processing: {path}")
|
99
|
+
|
100
|
+
videos: tuple[VideoStream, ...] = ()
|
101
|
+
audios: tuple[AudioStream, ...] = ()
|
102
|
+
subtitles: tuple[SubtitleStream, ...] = ()
|
103
|
+
|
104
|
+
for v in cont.streams.video:
|
105
|
+
if v.duration is not None and v.time_base is not None:
|
106
|
+
vdur = float(v.duration * v.time_base)
|
107
|
+
else:
|
108
|
+
vdur = 0.0
|
109
|
+
|
110
|
+
fps = v.average_rate
|
111
|
+
if (fps is None or fps < 1) and v.name in {"png", "mjpeg", "webp"}:
|
112
|
+
fps = Fraction(25)
|
113
|
+
if fps is None or fps == 0:
|
114
|
+
fps = Fraction(30)
|
115
|
+
|
116
|
+
if v.sample_aspect_ratio is None:
|
117
|
+
sar = Fraction(1)
|
118
|
+
else:
|
119
|
+
sar = v.sample_aspect_ratio
|
120
|
+
|
121
|
+
cc = v.codec_context
|
122
|
+
|
123
|
+
if v.name is None:
|
124
|
+
log.error(f"Can't detect codec for video stream {v}")
|
125
|
+
|
126
|
+
videos += (
|
127
|
+
VideoStream(
|
128
|
+
v.width,
|
129
|
+
v.height,
|
130
|
+
v.codec.canonical_name,
|
131
|
+
fps,
|
132
|
+
vdur,
|
133
|
+
sar,
|
134
|
+
v.time_base,
|
135
|
+
getattr(v.format, "name", None),
|
136
|
+
cc.color_range,
|
137
|
+
cc.colorspace,
|
138
|
+
cc.color_primaries,
|
139
|
+
cc.color_trc,
|
140
|
+
0 if v.bit_rate is None else v.bit_rate,
|
141
|
+
v.language,
|
142
|
+
),
|
143
|
+
)
|
144
|
+
|
145
|
+
for a in cont.streams.audio:
|
146
|
+
adur = 0.0
|
147
|
+
if a.duration is not None and a.time_base is not None:
|
148
|
+
adur = float(a.duration * a.time_base)
|
149
|
+
|
150
|
+
a_cc = a.codec_context
|
151
|
+
audios += (
|
152
|
+
AudioStream(
|
153
|
+
a_cc.codec.canonical_name,
|
154
|
+
0 if a_cc.sample_rate is None else a_cc.sample_rate,
|
155
|
+
a.layout.name,
|
156
|
+
a_cc.channels,
|
157
|
+
adur,
|
158
|
+
0 if a_cc.bit_rate is None else a_cc.bit_rate,
|
159
|
+
a.language,
|
160
|
+
),
|
161
|
+
)
|
162
|
+
|
163
|
+
for s in cont.streams.subtitles:
|
164
|
+
codec = s.codec_context.name
|
165
|
+
sub_exts = {"mov_text": "srt", "ass": "ass", "webvtt": "vtt"}
|
166
|
+
ext = sub_exts.get(codec, "vtt")
|
167
|
+
subtitles += (SubtitleStream(codec, ext, s.language),)
|
168
|
+
|
169
|
+
desc = cont.metadata.get("description", None)
|
170
|
+
bitrate = 0 if cont.bit_rate is None else cont.bit_rate
|
171
|
+
dur = 0 if cont.duration is None else cont.duration / bv.time_base
|
172
|
+
|
173
|
+
cont.close()
|
174
|
+
|
175
|
+
return FileInfo(Path(path), bitrate, dur, desc, videos, audios, subtitles)
|
176
|
+
|
177
|
+
def __repr__(self) -> str:
|
178
|
+
return f"@{self.path.name}"
|
@@ -59,13 +59,6 @@ def fcp11_write_xml(
|
|
59
59
|
return "0s"
|
60
60
|
return f"{val * tl.tb.denominator}/{tl.tb.numerator}s"
|
61
61
|
|
62
|
-
src = tl.src
|
63
|
-
assert src is not None
|
64
|
-
|
65
|
-
proj_name = src.path.stem
|
66
|
-
src_dur = int(src.duration * tl.tb)
|
67
|
-
tl_dur = src_dur if resolve else tl.out_len()
|
68
|
-
|
69
62
|
if version == 11:
|
70
63
|
ver_str = "1.11"
|
71
64
|
elif version == 10:
|
@@ -76,7 +69,16 @@ def fcp11_write_xml(
|
|
76
69
|
fcpxml = Element("fcpxml", version=ver_str)
|
77
70
|
resources = SubElement(fcpxml, "resources")
|
78
71
|
|
72
|
+
src_dur = 0
|
73
|
+
tl_dur = 0 if resolve else tl.out_len()
|
74
|
+
|
79
75
|
for i, one_src in enumerate(tl.unique_sources()):
|
76
|
+
if i == 0:
|
77
|
+
proj_name = one_src.path.stem
|
78
|
+
src_dur = int(one_src.duration * tl.tb)
|
79
|
+
if resolve:
|
80
|
+
tl_dur = src_dur
|
81
|
+
|
80
82
|
SubElement(
|
81
83
|
resources,
|
82
84
|
"format",
|
@@ -113,7 +115,7 @@ def fcp11_write_xml(
|
|
113
115
|
format="r1",
|
114
116
|
tcStart="0s",
|
115
117
|
tcFormat="NDF",
|
116
|
-
audioLayout=
|
118
|
+
audioLayout=tl.T.layout,
|
117
119
|
audioRate="44.1k" if tl.sr == 44100 else "48k",
|
118
120
|
)
|
119
121
|
spine = SubElement(sequence, "spine")
|