auto-editor 24.24.1__tar.gz → 24.27.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-24.24.1/auto_editor.egg-info → auto_editor-24.27.1}/PKG-INFO +6 -8
- {auto_editor-24.24.1 → auto_editor-24.27.1}/README.md +4 -6
- auto_editor-24.27.1/auto_editor/__init__.py +2 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/__main__.py +29 -26
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/analyze.py +54 -19
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/edit.py +7 -17
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/ffwrapper.py +6 -2
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/fcp7.py +3 -3
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/shotcut.py +1 -2
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/make_layers.py +46 -8
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/output.py +50 -14
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/render/subtitle.py +7 -13
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/render/video.py +9 -9
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/info.py +1 -1
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/levels.py +1 -1
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/repl.py +1 -1
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/test.py +12 -5
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/log.py +33 -34
- auto_editor-24.27.1/auto_editor/utils/subtitle_tools.py +29 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/validate_input.py +9 -12
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/vanparse.py +26 -27
- {auto_editor-24.24.1 → auto_editor-24.27.1/auto_editor.egg-info}/PKG-INFO +6 -8
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/SOURCES.txt +1 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/requires.txt +1 -1
- {auto_editor-24.24.1 → auto_editor-24.27.1}/pyproject.toml +1 -1
- auto_editor-24.24.1/auto_editor/__init__.py +0 -2
- {auto_editor-24.24.1 → auto_editor-24.27.1}/LICENSE +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/ae-ffmpeg/ae_ffmpeg/__init__.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/ae-ffmpeg/ae_ffmpeg/py.typed +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/ae-ffmpeg/setup.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/__init__.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/fcp11.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/json.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/formats/utils.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/help.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lang/__init__.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lang/json.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lang/libmath.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lang/palet.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lib/__init__.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lib/contracts.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lib/data_structs.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/lib/err.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/preview.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/render/__init__.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/render/audio.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/__init__.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/desc.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/palet.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/subcommands/subdump.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/timeline.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/__init__.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/bar.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/chunks.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/cmdkw.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/container.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/encoder.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/func.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/utils/types.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor/wavfile.py +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/dependency_links.txt +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/entry_points.txt +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/auto_editor.egg-info/top_level.txt +0 -0
- {auto_editor-24.24.1 → auto_editor-24.27.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: auto-editor
|
3
|
-
Version: 24.
|
3
|
+
Version: 24.27.1
|
4
4
|
Summary: Auto-Editor: Effort free video editing!
|
5
5
|
Author-email: WyattBlue <wyattblue@auto-editor.com>
|
6
6
|
License: Unlicense
|
@@ -12,7 +12,7 @@ Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
13
13
|
License-File: LICENSE
|
14
14
|
Requires-Dist: numpy>=1.22.0
|
15
|
-
Requires-Dist: pyav==12.
|
15
|
+
Requires-Dist: pyav==12.2.*
|
16
16
|
Requires-Dist: ae-ffmpeg==1.2.*
|
17
17
|
|
18
18
|
<p align="center"><img src="https://auto-editor.com/img/auto-editor-banner.webp" title="Auto-Editor" width="700"></p>
|
@@ -105,16 +105,14 @@ Auto-Editor can also export to:
|
|
105
105
|
- Individual media clips with `--export clip-sequence`
|
106
106
|
|
107
107
|
### Naming Timelines
|
108
|
-
By default, auto-editor will
|
108
|
+
Some editors support naming timelines. By default, auto-editor will use the name "Auto-Editor Media Group". For `premiere` `resolve` and `final-cut-pro` export options, you can change the name with the following syntax.
|
109
109
|
|
110
110
|
```
|
111
|
+
# for POSIX shells
|
111
112
|
auto-editor example.mp4 --export 'premiere:name="Your name here"'
|
112
113
|
|
113
|
-
|
114
|
-
|
115
|
-
auto-editor example.mp4 --export 'final-cut-pro:name="Your name here"'
|
116
|
-
|
117
|
-
# No other export options support naming
|
114
|
+
# for Powershell
|
115
|
+
auto-editor example.mp4 --export 'premiere:name=""Your name here""'
|
118
116
|
```
|
119
117
|
|
120
118
|
### Split by Clip
|
@@ -88,16 +88,14 @@ Auto-Editor can also export to:
|
|
88
88
|
- Individual media clips with `--export clip-sequence`
|
89
89
|
|
90
90
|
### Naming Timelines
|
91
|
-
By default, auto-editor will
|
91
|
+
Some editors support naming timelines. By default, auto-editor will use the name "Auto-Editor Media Group". For `premiere` `resolve` and `final-cut-pro` export options, you can change the name with the following syntax.
|
92
92
|
|
93
93
|
```
|
94
|
+
# for POSIX shells
|
94
95
|
auto-editor example.mp4 --export 'premiere:name="Your name here"'
|
95
96
|
|
96
|
-
|
97
|
-
|
98
|
-
auto-editor example.mp4 --export 'final-cut-pro:name="Your name here"'
|
99
|
-
|
100
|
-
# No other export options support naming
|
97
|
+
# for Powershell
|
98
|
+
auto-editor example.mp4 --export 'premiere:name=""Your name here""'
|
101
99
|
```
|
102
100
|
|
103
101
|
### Split by Clip
|
@@ -1,10 +1,13 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
|
3
3
|
import sys
|
4
|
+
from os import environ
|
4
5
|
|
5
6
|
import auto_editor
|
7
|
+
from auto_editor.edit import edit_media
|
8
|
+
from auto_editor.ffwrapper import FFmpeg
|
6
9
|
from auto_editor.utils.func import setup_tempdir
|
7
|
-
from auto_editor.utils.log import Log
|
10
|
+
from auto_editor.utils.log import Log
|
8
11
|
from auto_editor.utils.types import (
|
9
12
|
Args,
|
10
13
|
bitrate,
|
@@ -277,11 +280,16 @@ def main() -> None:
|
|
277
280
|
f"auto_editor.subcommands.{sys.argv[1]}", fromlist=["subcommands"]
|
278
281
|
)
|
279
282
|
obj.main(sys.argv[2:])
|
280
|
-
|
283
|
+
return
|
284
|
+
|
285
|
+
ff_color = "AV_LOG_FORCE_NOCOLOR"
|
286
|
+
no_color = bool(environ.get("NO_COLOR")) or bool(environ.get(ff_color))
|
287
|
+
log = Log(no_color=no_color)
|
281
288
|
|
282
289
|
args = main_options(ArgumentParser("Auto-Editor")).parse_args(
|
283
290
|
Args,
|
284
291
|
sys.argv[1:],
|
292
|
+
log,
|
285
293
|
macros=[
|
286
294
|
({"--frame-margin"}, ["--margin"]),
|
287
295
|
({"--export-to-premiere", "-exp"}, ["--export", "premiere"]),
|
@@ -296,41 +304,36 @@ def main() -> None:
|
|
296
304
|
|
297
305
|
if args.version:
|
298
306
|
print(f"{auto_editor.version} ({auto_editor.__version__})")
|
299
|
-
|
300
|
-
|
301
|
-
from auto_editor.edit import edit_media
|
302
|
-
from auto_editor.ffwrapper import FFmpeg
|
303
|
-
|
304
|
-
log = Log(args.debug, args.quiet)
|
305
|
-
ffmpeg = FFmpeg(
|
306
|
-
args.ffmpeg_location,
|
307
|
-
args.my_ffmpeg,
|
308
|
-
args.show_ffmpeg_commands,
|
309
|
-
args.show_ffmpeg_output,
|
310
|
-
)
|
307
|
+
return
|
311
308
|
|
312
|
-
if args.debug and args.input
|
309
|
+
if args.debug and not args.input:
|
313
310
|
import platform as plat
|
314
311
|
|
315
|
-
|
316
|
-
|
317
|
-
print(f"
|
318
|
-
print(f"
|
319
|
-
|
312
|
+
import av
|
313
|
+
|
314
|
+
print(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}")
|
315
|
+
print(f"Python: {plat.python_version()}")
|
316
|
+
print(f"PyAV: {av.__version__}")
|
317
|
+
print(f"Auto-Editor: {auto_editor.version}")
|
318
|
+
return
|
320
319
|
|
321
|
-
if args.input
|
320
|
+
if not args.input:
|
322
321
|
log.error("You need to give auto-editor an input file.")
|
323
322
|
|
324
|
-
temp = setup_tempdir(args.temp_dir,
|
325
|
-
log = Log(args.debug, args.quiet, temp)
|
326
|
-
log.machine = args.progress == "machine"
|
323
|
+
temp = setup_tempdir(args.temp_dir, log)
|
324
|
+
log = Log(args.debug, args.quiet, temp, args.progress == "machine", no_color)
|
327
325
|
log.debug(f"Temp Directory: {temp}")
|
328
326
|
|
327
|
+
ffmpeg = FFmpeg(
|
328
|
+
args.ffmpeg_location,
|
329
|
+
args.my_ffmpeg,
|
330
|
+
args.show_ffmpeg_commands,
|
331
|
+
args.show_ffmpeg_output,
|
332
|
+
)
|
329
333
|
paths = valid_input(args.input, ffmpeg, args, log)
|
330
|
-
timer = Timer(args.quiet or log.machine)
|
331
334
|
|
332
335
|
try:
|
333
|
-
edit_media(paths, ffmpeg, args, temp,
|
336
|
+
edit_media(paths, ffmpeg, args, temp, log)
|
334
337
|
except KeyboardInterrupt:
|
335
338
|
log.error("Keyboard Interrupt")
|
336
339
|
log.cleanup()
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import os
|
4
4
|
import re
|
5
5
|
from dataclasses import dataclass
|
6
|
+
from fractions import Fraction
|
6
7
|
from typing import TYPE_CHECKING
|
7
8
|
|
8
9
|
import numpy as np
|
@@ -19,12 +20,12 @@ from auto_editor.lib.contracts import (
|
|
19
20
|
orc,
|
20
21
|
)
|
21
22
|
from auto_editor.lib.data_structs import Sym
|
22
|
-
from auto_editor.render.subtitle import SubtitleParser
|
23
23
|
from auto_editor.utils.cmdkw import (
|
24
24
|
Required,
|
25
25
|
pAttr,
|
26
26
|
pAttrs,
|
27
27
|
)
|
28
|
+
from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
28
29
|
from auto_editor.wavfile import read
|
29
30
|
|
30
31
|
if TYPE_CHECKING:
|
@@ -307,31 +308,65 @@ class Levels:
|
|
307
308
|
except re.error as e:
|
308
309
|
self.log.error(e)
|
309
310
|
|
310
|
-
|
311
|
-
|
311
|
+
import av
|
312
|
+
from av.subtitles.subtitle import AssSubtitle, TextSubtitle
|
312
313
|
|
313
|
-
|
314
|
-
|
314
|
+
try:
|
315
|
+
container = av.open(self.src.path, "r")
|
316
|
+
subtitle_stream = container.streams.subtitles[stream]
|
317
|
+
assert isinstance(subtitle_stream.time_base, Fraction)
|
318
|
+
except Exception as e:
|
319
|
+
self.log.error(e)
|
315
320
|
|
316
|
-
#
|
317
|
-
|
318
|
-
|
319
|
-
|
321
|
+
# Get the length of the subtitle stream.
|
322
|
+
sub_length = 0
|
323
|
+
for packet in container.demux(subtitle_stream):
|
324
|
+
if packet.pts is None or packet.duration is None:
|
325
|
+
continue
|
326
|
+
for subset in packet.decode():
|
327
|
+
# See definition of `AVSubtitle`
|
328
|
+
# in: https://ffmpeg.org/doxygen/trunk/avcodec_8h_source.html
|
329
|
+
start = float(packet.pts * subtitle_stream.time_base)
|
330
|
+
dur = float(packet.duration * subtitle_stream.time_base)
|
320
331
|
|
321
|
-
|
322
|
-
|
332
|
+
end = round((start + dur) * self.tb)
|
333
|
+
sub_length = max(sub_length, end)
|
323
334
|
|
324
|
-
result = np.zeros((
|
335
|
+
result = np.zeros((sub_length), dtype=np.bool_)
|
336
|
+
del sub_length
|
325
337
|
|
326
338
|
count = 0
|
327
|
-
|
328
|
-
|
339
|
+
early_exit = False
|
340
|
+
container.seek(0)
|
341
|
+
for packet in container.demux(subtitle_stream):
|
342
|
+
if packet.pts is None or packet.duration is None:
|
343
|
+
continue
|
344
|
+
if early_exit:
|
329
345
|
break
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
346
|
+
for subset in packet.decode():
|
347
|
+
if max_count is not None and count >= max_count:
|
348
|
+
early_exit = True
|
349
|
+
break
|
350
|
+
|
351
|
+
start = float(packet.pts * subtitle_stream.time_base)
|
352
|
+
dur = float(packet.duration * subtitle_stream.time_base)
|
353
|
+
|
354
|
+
san_start = round(start * self.tb)
|
355
|
+
san_end = round((start + dur) * self.tb)
|
356
|
+
|
357
|
+
for sub in subset:
|
358
|
+
if isinstance(sub, AssSubtitle):
|
359
|
+
line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
|
360
|
+
elif isinstance(sub, TextSubtitle):
|
361
|
+
line = sub.text.decode(errors="ignore")
|
362
|
+
else:
|
363
|
+
continue
|
364
|
+
|
365
|
+
if line and re.search(pattern, line):
|
366
|
+
result[san_start:san_end] = 1
|
367
|
+
count += 1
|
368
|
+
|
369
|
+
container.close()
|
335
370
|
|
336
371
|
return result
|
337
372
|
|
@@ -15,7 +15,7 @@ from auto_editor.utils.bar import Bar
|
|
15
15
|
from auto_editor.utils.chunks import Chunk, Chunks
|
16
16
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
17
17
|
from auto_editor.utils.container import Container, container_constructor
|
18
|
-
from auto_editor.utils.log import Log
|
18
|
+
from auto_editor.utils.log import Log
|
19
19
|
from auto_editor.utils.types import Args
|
20
20
|
|
21
21
|
|
@@ -150,7 +150,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
150
150
|
|
151
151
|
|
152
152
|
def edit_media(
|
153
|
-
paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str,
|
153
|
+
paths: list[str], ffmpeg: FFmpeg, args: Args, temp: str, log: Log
|
154
154
|
) -> None:
|
155
155
|
bar = Bar(args.progress)
|
156
156
|
tl = None
|
@@ -160,7 +160,7 @@ def edit_media(
|
|
160
160
|
if path_ext == ".xml":
|
161
161
|
from auto_editor.formats.fcp7 import fcp7_read_xml
|
162
162
|
|
163
|
-
tl = fcp7_read_xml(paths[0],
|
163
|
+
tl = fcp7_read_xml(paths[0], log)
|
164
164
|
assert tl.src is not None
|
165
165
|
sources: list[FileInfo] = [tl.src]
|
166
166
|
src: FileInfo | None = tl.src
|
@@ -168,7 +168,7 @@ def edit_media(
|
|
168
168
|
elif path_ext == ".mlt":
|
169
169
|
from auto_editor.formats.shotcut import shotcut_read_mlt
|
170
170
|
|
171
|
-
tl = shotcut_read_mlt(paths[0],
|
171
|
+
tl = shotcut_read_mlt(paths[0], log)
|
172
172
|
assert tl.src is not None
|
173
173
|
sources = [tl.src]
|
174
174
|
src = tl.src
|
@@ -190,7 +190,6 @@ def edit_media(
|
|
190
190
|
|
191
191
|
if export["export"] == "timeline":
|
192
192
|
log.quiet = True
|
193
|
-
timer.quiet = True
|
194
193
|
|
195
194
|
if not args.preview:
|
196
195
|
log.conwrite("Starting")
|
@@ -210,18 +209,9 @@ def edit_media(
|
|
210
209
|
else:
|
211
210
|
samplerate = args.sample_rate
|
212
211
|
|
213
|
-
ensure = Ensure(ffmpeg, samplerate, temp, log)
|
212
|
+
ensure = Ensure(ffmpeg, bar, samplerate, temp, log)
|
214
213
|
|
215
214
|
if tl is None:
|
216
|
-
# Extract subtitles in their native format.
|
217
|
-
if src is not None and len(src.subtitles) > 0 and not args.sn:
|
218
|
-
cmd = ["-i", f"{src.path}", "-hide_banner"]
|
219
|
-
for s, sub in enumerate(src.subtitles):
|
220
|
-
cmd.extend(["-map", f"0:s:{s}"])
|
221
|
-
for s, sub in enumerate(src.subtitles):
|
222
|
-
cmd.extend([os.path.join(temp, f"{s}s.{sub.ext}")])
|
223
|
-
ffmpeg.run(cmd)
|
224
|
-
|
225
215
|
tl = make_timeline(sources, ensure, args, samplerate, bar, temp, log)
|
226
216
|
|
227
217
|
if export["export"] == "timeline":
|
@@ -283,7 +273,7 @@ def edit_media(
|
|
283
273
|
apply_later = False
|
284
274
|
|
285
275
|
if ctr.allow_subtitle and not args.sn:
|
286
|
-
sub_output = make_new_subtitles(tl,
|
276
|
+
sub_output = make_new_subtitles(tl, ensure, temp)
|
287
277
|
|
288
278
|
if ctr.allow_audio:
|
289
279
|
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, temp, log)
|
@@ -357,7 +347,7 @@ def edit_media(
|
|
357
347
|
else:
|
358
348
|
make_media(tl, output)
|
359
349
|
|
360
|
-
|
350
|
+
log.stop_timer()
|
361
351
|
|
362
352
|
if not args.no_open and export["export"] in ("default", "audio", "clip-sequence"):
|
363
353
|
if args.player is None:
|
@@ -11,6 +11,8 @@ from shutil import which
|
|
11
11
|
from subprocess import PIPE, Popen
|
12
12
|
from typing import Any
|
13
13
|
|
14
|
+
import av
|
15
|
+
|
14
16
|
from auto_editor.utils.func import get_stdout
|
15
17
|
from auto_editor.utils.log import Log
|
16
18
|
|
@@ -190,10 +192,12 @@ class FileInfo:
|
|
190
192
|
|
191
193
|
|
192
194
|
def initFileInfo(path: str, log: Log) -> FileInfo:
|
193
|
-
import av
|
194
|
-
|
195
195
|
try:
|
196
196
|
cont = av.open(path, "r")
|
197
|
+
except av.error.FileNotFoundError:
|
198
|
+
log.error(f"Could not find '{path}'")
|
199
|
+
except av.error.IsADirectoryError:
|
200
|
+
log.error(f"Expected a media file, but got a directory: {path}")
|
197
201
|
except av.error.InvalidDataError:
|
198
202
|
log.error(f"Invalid data when processing: {path}")
|
199
203
|
|
@@ -7,7 +7,7 @@ from math import ceil
|
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
from xml.etree.ElementTree import Element
|
9
9
|
|
10
|
-
from auto_editor.ffwrapper import
|
10
|
+
from auto_editor.ffwrapper import FileInfo, initFileInfo
|
11
11
|
from auto_editor.timeline import ASpace, TlAudio, TlVideo, VSpace, v3
|
12
12
|
|
13
13
|
from .utils import Validator, show
|
@@ -177,7 +177,7 @@ def read_filters(clipitem: Element, log: Log) -> float:
|
|
177
177
|
return 1.0
|
178
178
|
|
179
179
|
|
180
|
-
def fcp7_read_xml(path: str,
|
180
|
+
def fcp7_read_xml(path: str, log: Log) -> v3:
|
181
181
|
def xml_bool(val: str) -> bool:
|
182
182
|
if val == "TRUE":
|
183
183
|
return True
|
@@ -188,7 +188,7 @@ def fcp7_read_xml(path: str, ffmpeg: FFmpeg, log: Log) -> v3:
|
|
188
188
|
try:
|
189
189
|
tree = ET.parse(path)
|
190
190
|
except FileNotFoundError:
|
191
|
-
log.
|
191
|
+
log.error(f"Could not find '{path}'")
|
192
192
|
|
193
193
|
root = tree.getroot()
|
194
194
|
|
@@ -9,7 +9,6 @@ from auto_editor.utils.func import aspect_ratio, to_timecode
|
|
9
9
|
if TYPE_CHECKING:
|
10
10
|
from collections.abc import Sequence
|
11
11
|
|
12
|
-
from auto_editor.ffwrapper import FFmpeg
|
13
12
|
from auto_editor.timeline import TlAudio, TlVideo
|
14
13
|
from auto_editor.utils.log import Log
|
15
14
|
|
@@ -22,7 +21,7 @@ https://mltframework.org/docs/mltxml/
|
|
22
21
|
"""
|
23
22
|
|
24
23
|
|
25
|
-
def shotcut_read_mlt(path: str,
|
24
|
+
def shotcut_read_mlt(path: str, log: Log) -> v3:
|
26
25
|
raise NotImplementedError
|
27
26
|
|
28
27
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from fractions import Fraction
|
4
|
+
from math import ceil
|
4
5
|
from typing import TYPE_CHECKING, NamedTuple
|
5
6
|
|
6
7
|
import numpy as np
|
@@ -37,7 +38,7 @@ def clipify(chunks: Chunks, src: FileInfo, start: int = 0) -> list[Clip]:
|
|
37
38
|
clips: list[Clip] = []
|
38
39
|
i = 0
|
39
40
|
for chunk in chunks:
|
40
|
-
if chunk[2]
|
41
|
+
if chunk[2] > 0 and chunk[2] < 99999.0:
|
41
42
|
dur = round((chunk[1] - chunk[0]) / chunk[2])
|
42
43
|
if dur == 0:
|
43
44
|
continue
|
@@ -112,8 +113,13 @@ def run_interpreter_for_edit_option(
|
|
112
113
|
|
113
114
|
def make_sane_timebase(fps: Fraction) -> Fraction:
|
114
115
|
tb = round(fps, 2)
|
116
|
+
|
117
|
+
ntsc_60 = Fraction(60_000, 1001)
|
115
118
|
ntsc = Fraction(30_000, 1001)
|
116
119
|
film_ntsc = Fraction(24_000, 1001)
|
120
|
+
|
121
|
+
if tb == round(ntsc_60, 2):
|
122
|
+
return ntsc_60
|
117
123
|
if tb == round(ntsc, 2):
|
118
124
|
return ntsc
|
119
125
|
if tb == round(film_ntsc, 2):
|
@@ -226,21 +232,35 @@ def make_timeline(
|
|
226
232
|
chunks.append((src, start, arr_length, speed_map[arr[j]]))
|
227
233
|
return chunks
|
228
234
|
|
235
|
+
# Assert timeline is monotonic because non-monotonic timelines are incorrect
|
236
|
+
# here and causes back-seeking (performance issue) in video rendering.
|
237
|
+
|
238
|
+
# We don't properly check monotonicity for multiple sources, so skip those.
|
239
|
+
|
240
|
+
check_monotonic = len(sources) == 1
|
241
|
+
last_i = 0
|
242
|
+
|
229
243
|
clips: list[Clip] = []
|
230
|
-
i = 0
|
231
244
|
start = 0
|
245
|
+
|
232
246
|
for chunk in echunk(speed_index, src_index):
|
233
247
|
if chunk[3] != 99999:
|
234
|
-
dur =
|
248
|
+
dur = int((chunk[2] - chunk[1]) / chunk[3])
|
235
249
|
if dur == 0:
|
236
250
|
continue
|
237
251
|
|
238
|
-
offset =
|
252
|
+
offset = ceil(chunk[1] / chunk[3])
|
253
|
+
|
254
|
+
if check_monotonic:
|
255
|
+
max_end = start + dur - 1
|
256
|
+
this_i = round((offset + max_end - start) * chunk[3])
|
257
|
+
if this_i < last_i:
|
258
|
+
raise ValueError("not monotonic", sources, this_i, last_i)
|
259
|
+
last_i = this_i
|
260
|
+
|
261
|
+
clips.append(Clip(start, dur, offset, chunk[3], chunk[0]))
|
239
262
|
|
240
|
-
if not (clips and clips[-1].start == round(start)):
|
241
|
-
clips.append(Clip(start, dur, offset, chunk[3], chunk[0]))
|
242
263
|
start += dur
|
243
|
-
i += 1
|
244
264
|
|
245
265
|
vtl: VSpace = []
|
246
266
|
atl: ASpace = []
|
@@ -276,4 +296,22 @@ def make_timeline(
|
|
276
296
|
else:
|
277
297
|
v1_compatiable = None
|
278
298
|
|
279
|
-
|
299
|
+
tl = v3(inp, tb, sr, res, args.background, vtl, atl, v1_compatiable)
|
300
|
+
|
301
|
+
# Additional monotonic check, o(n^2) time complexity so disable by default.
|
302
|
+
|
303
|
+
# if len(sources) != 1:
|
304
|
+
# return tl
|
305
|
+
|
306
|
+
# last_i = 0
|
307
|
+
# for index in range(tl.end):
|
308
|
+
# for layer in tl.v:
|
309
|
+
# for lobj in layer:
|
310
|
+
# if index >= lobj.start and index < (lobj.start + lobj.dur):
|
311
|
+
# _i = round((lobj.offset + index - lobj.start) * lobj.speed)
|
312
|
+
# if (_i < last_i):
|
313
|
+
# print(_i, last_i)
|
314
|
+
# raise ValueError("not monotonic")
|
315
|
+
# last_i = _i
|
316
|
+
|
317
|
+
return tl
|
@@ -4,7 +4,11 @@ import os.path
|
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
from fractions import Fraction
|
6
6
|
|
7
|
+
import av
|
8
|
+
from av.audio.resampler import AudioResampler
|
9
|
+
|
7
10
|
from auto_editor.ffwrapper import FFmpeg, FileInfo
|
11
|
+
from auto_editor.utils.bar import Bar
|
8
12
|
from auto_editor.utils.container import Container
|
9
13
|
from auto_editor.utils.log import Log
|
10
14
|
from auto_editor.utils.types import Args
|
@@ -13,44 +17,76 @@ from auto_editor.utils.types import Args
|
|
13
17
|
@dataclass(slots=True)
|
14
18
|
class Ensure:
|
15
19
|
_ffmpeg: FFmpeg
|
20
|
+
_bar: Bar
|
16
21
|
_sr: int
|
17
22
|
temp: str
|
18
23
|
log: Log
|
19
|
-
|
20
|
-
|
24
|
+
_audios: list[tuple[FileInfo, int]] = field(default_factory=list)
|
25
|
+
_subtitles: list[tuple[FileInfo, int, str]] = field(default_factory=list)
|
21
26
|
|
22
27
|
def audio(self, src: FileInfo, stream: int) -> str:
|
23
28
|
try:
|
24
|
-
label = self.
|
29
|
+
label = self._audios.index((src, stream))
|
25
30
|
first_time = False
|
26
31
|
except ValueError:
|
27
|
-
self.
|
28
|
-
label = len(self.
|
32
|
+
self._audios.append((src, stream))
|
33
|
+
label = len(self._audios) - 1
|
29
34
|
first_time = True
|
30
35
|
|
31
36
|
out_path = os.path.join(self.temp, f"{label:x}.wav")
|
32
37
|
|
33
38
|
if first_time:
|
34
|
-
self.
|
39
|
+
sample_rate = self._sr
|
40
|
+
bar = self._bar
|
41
|
+
self.log.debug(f"Making external audio: {out_path}")
|
42
|
+
|
43
|
+
in_container = av.open(src.path, "r")
|
44
|
+
out_container = av.open(
|
45
|
+
out_path, "w", format="wav", options={"rf64": "always"}
|
46
|
+
)
|
47
|
+
astream = in_container.streams.audio[stream]
|
48
|
+
|
49
|
+
if astream.duration is None or astream.time_base is None:
|
50
|
+
dur = 1
|
51
|
+
else:
|
52
|
+
dur = int(astream.duration * astream.time_base)
|
53
|
+
|
54
|
+
bar.start(dur, "Extracting audio")
|
55
|
+
|
56
|
+
# PyAV always uses "stereo" layout, which is what we want.
|
57
|
+
output_astream = out_container.add_stream("pcm_s16le", rate=sample_rate)
|
58
|
+
assert isinstance(output_astream, av.audio.stream.AudioStream)
|
59
|
+
|
60
|
+
resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate) # type: ignore
|
61
|
+
for i, frame in enumerate(in_container.decode(astream)):
|
62
|
+
if i % 1500 == 0:
|
63
|
+
bar.tick(0 if frame.time is None else frame.time)
|
64
|
+
|
65
|
+
for new_frame in resampler.resample(frame):
|
66
|
+
for packet in output_astream.encode(new_frame):
|
67
|
+
out_container.mux_one(packet)
|
68
|
+
|
69
|
+
for packet in output_astream.encode():
|
70
|
+
out_container.mux_one(packet)
|
35
71
|
|
36
|
-
|
37
|
-
|
38
|
-
|
72
|
+
out_container.close()
|
73
|
+
in_container.close()
|
74
|
+
bar.end()
|
39
75
|
|
40
76
|
return out_path
|
41
77
|
|
42
|
-
def subtitle(self, src: FileInfo, stream: int) -> str:
|
78
|
+
def subtitle(self, src: FileInfo, stream: int, ext: str) -> str:
|
43
79
|
try:
|
44
|
-
|
80
|
+
self._subtitles.index((src, stream, ext))
|
45
81
|
first_time = False
|
46
82
|
except ValueError:
|
47
|
-
self.
|
48
|
-
label = len(self.sub_labels) - 1
|
83
|
+
self._subtitles.append((src, stream, ext))
|
49
84
|
first_time = True
|
50
85
|
|
51
|
-
out_path = os.path.join(self.temp, f"{
|
86
|
+
out_path = os.path.join(self.temp, f"{stream}s.{ext}")
|
52
87
|
|
53
88
|
if first_time:
|
89
|
+
self.log.debug(f"Making external subtitle: {out_path}")
|
54
90
|
self.log.conwrite("Extracting subtitle")
|
55
91
|
self._ffmpeg.run(["-i", f"{src.path}", "-map", f"0:s:{stream}", out_path])
|
56
92
|
|
@@ -10,10 +10,9 @@ from auto_editor.utils.func import to_timecode
|
|
10
10
|
if TYPE_CHECKING:
|
11
11
|
from fractions import Fraction
|
12
12
|
|
13
|
-
from auto_editor.
|
13
|
+
from auto_editor.output import Ensure
|
14
14
|
from auto_editor.timeline import v3
|
15
15
|
from auto_editor.utils.chunks import Chunks
|
16
|
-
from auto_editor.utils.log import Log
|
17
16
|
|
18
17
|
|
19
18
|
@dataclass(slots=True)
|
@@ -122,26 +121,21 @@ class SubtitleParser:
|
|
122
121
|
file.write(self.footer)
|
123
122
|
|
124
123
|
|
125
|
-
def make_new_subtitles(tl: v3,
|
124
|
+
def make_new_subtitles(tl: v3, ensure: Ensure, temp: str) -> list[str]:
|
126
125
|
if tl.v1 is None:
|
127
126
|
return []
|
128
127
|
|
129
128
|
new_paths = []
|
130
129
|
|
131
130
|
for s, sub in enumerate(tl.v1.source.subtitles):
|
132
|
-
file_path = os.path.join(temp, f"{s}s.{sub.ext}")
|
133
131
|
new_path = os.path.join(temp, f"new{s}s.{sub.ext}")
|
134
|
-
|
135
132
|
parser = SubtitleParser(tl.tb)
|
136
133
|
|
137
|
-
if sub.codec in parser.supported_codecs
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
ffmpeg.run(["-i", file_path, convert_path])
|
143
|
-
with open(convert_path, encoding="utf-8") as file:
|
144
|
-
parser.parse(file.read(), "webvtt")
|
134
|
+
ext = sub.ext if sub.codec in parser.supported_codecs else "vtt"
|
135
|
+
file_path = ensure.subtitle(tl.v1.source, s, ext)
|
136
|
+
|
137
|
+
with open(file_path, encoding="utf-8") as file:
|
138
|
+
parser.parse(file.read(), sub.codec)
|
145
139
|
|
146
140
|
parser.edit(tl.v1.chunks)
|
147
141
|
parser.write(new_path)
|