auto-editor 26.3.2__py3-none-any.whl → 27.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +155 -40
- auto_editor/analyze.py +30 -36
- auto_editor/cmds/info.py +1 -1
- auto_editor/cmds/levels.py +3 -3
- auto_editor/cmds/subdump.py +62 -8
- auto_editor/cmds/test.py +73 -53
- auto_editor/edit.py +50 -58
- auto_editor/ffwrapper.py +9 -9
- auto_editor/formats/json.py +2 -2
- auto_editor/{lang/json.py → json.py} +39 -43
- auto_editor/lang/palet.py +2 -2
- auto_editor/lang/stdenv.py +12 -0
- auto_editor/make_layers.py +2 -1
- auto_editor/output.py +6 -6
- auto_editor/render/audio.py +30 -25
- auto_editor/render/subtitle.py +10 -14
- auto_editor/render/video.py +41 -45
- auto_editor/timeline.py +8 -1
- auto_editor/utils/container.py +3 -3
- auto_editor/utils/types.py +7 -118
- {auto_editor-26.3.2.dist-info → auto_editor-27.0.0.dist-info}/METADATA +8 -7
- {auto_editor-26.3.2.dist-info → auto_editor-27.0.0.dist-info}/RECORD +28 -28
- {auto_editor-26.3.2.dist-info → auto_editor-27.0.0.dist-info}/WHEEL +1 -1
- docs/build.py +16 -7
- {auto_editor-26.3.2.dist-info → auto_editor-27.0.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.3.2.dist-info → auto_editor-27.0.0.dist-info/licenses}/LICENSE +0 -0
- {auto_editor-26.3.2.dist-info → auto_editor-27.0.0.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "
|
1
|
+
__version__ = "27.0.0"
|
auto_editor/__main__.py
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
import platform as plat
|
4
4
|
import re
|
5
5
|
import sys
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from fractions import Fraction
|
6
8
|
from io import StringIO
|
7
9
|
from os import environ
|
8
10
|
from os.path import exists, isdir, isfile, lexists, splitext
|
@@ -12,21 +14,128 @@ import auto_editor
|
|
12
14
|
from auto_editor.utils.func import get_stdout
|
13
15
|
from auto_editor.utils.log import Log
|
14
16
|
from auto_editor.utils.types import (
|
15
|
-
|
17
|
+
CoerceError,
|
16
18
|
frame_rate,
|
17
|
-
|
19
|
+
natural,
|
18
20
|
number,
|
19
21
|
parse_color,
|
20
|
-
|
21
|
-
sample_rate,
|
22
|
-
speed,
|
23
|
-
speed_range,
|
24
|
-
time_range,
|
22
|
+
split_num_str,
|
25
23
|
)
|
26
24
|
from auto_editor.vanparse import ArgumentParser
|
27
25
|
|
28
26
|
|
27
|
+
@dataclass(slots=True)
|
28
|
+
class Args:
|
29
|
+
input: list[str] = field(default_factory=list)
|
30
|
+
help: bool = False
|
31
|
+
|
32
|
+
# Editing Options
|
33
|
+
margin: tuple[str, str] = ("0.2s", "0.2s")
|
34
|
+
edit: str = "audio"
|
35
|
+
export: str | None = None
|
36
|
+
output: str | None = None
|
37
|
+
silent_speed: float = 99999.0
|
38
|
+
video_speed: float = 1.0
|
39
|
+
cut_out: list[tuple[str, str]] = field(default_factory=list)
|
40
|
+
add_in: list[tuple[str, str]] = field(default_factory=list)
|
41
|
+
set_speed_for_range: list[tuple[float, str, str]] = field(default_factory=list)
|
42
|
+
|
43
|
+
# Timeline Options
|
44
|
+
frame_rate: Fraction | None = None
|
45
|
+
sample_rate: int | None = None
|
46
|
+
resolution: tuple[int, int] | None = None
|
47
|
+
background: str = "#000000"
|
48
|
+
|
49
|
+
# URL download Options
|
50
|
+
yt_dlp_location: str = "yt-dlp"
|
51
|
+
download_format: str | None = None
|
52
|
+
output_format: str | None = None
|
53
|
+
yt_dlp_extras: str | None = None
|
54
|
+
|
55
|
+
# Display Options
|
56
|
+
progress: str = "modern"
|
57
|
+
debug: bool = False
|
58
|
+
quiet: bool = False
|
59
|
+
preview: bool = False
|
60
|
+
|
61
|
+
# Container Settings
|
62
|
+
sn: bool = False
|
63
|
+
dn: bool = False
|
64
|
+
faststart: bool = False
|
65
|
+
no_faststart: bool = False
|
66
|
+
fragmented: bool = False
|
67
|
+
no_fragmented: bool = False
|
68
|
+
|
69
|
+
# Video Rendering
|
70
|
+
video_codec: str = "auto"
|
71
|
+
video_bitrate: str = "auto"
|
72
|
+
vprofile: str | None = None
|
73
|
+
scale: float = 1.0
|
74
|
+
no_seek: bool = False
|
75
|
+
|
76
|
+
# Audio Rendering
|
77
|
+
audio_codec: str = "auto"
|
78
|
+
audio_bitrate: str = "auto"
|
79
|
+
mix_audio_streams: bool = False
|
80
|
+
keep_tracks_separate: bool = False
|
81
|
+
audio_normalize: str = "#f"
|
82
|
+
|
83
|
+
# Misc.
|
84
|
+
config: bool = False
|
85
|
+
no_cache: bool = False
|
86
|
+
no_open: bool = False
|
87
|
+
temp_dir: str | None = None
|
88
|
+
player: str | None = None
|
89
|
+
version: bool = False
|
90
|
+
|
91
|
+
|
29
92
|
def main_options(parser: ArgumentParser) -> ArgumentParser:
|
93
|
+
def margin(val: str) -> tuple[str, str]:
|
94
|
+
vals = val.strip().split(",")
|
95
|
+
if len(vals) == 1:
|
96
|
+
vals.append(vals[0])
|
97
|
+
if len(vals) != 2:
|
98
|
+
raise CoerceError("--margin has too many arguments.")
|
99
|
+
return vals[0], vals[1]
|
100
|
+
|
101
|
+
def speed(val: str) -> float:
|
102
|
+
_s = number(val)
|
103
|
+
if _s <= 0 or _s > 99999:
|
104
|
+
return 99999.0
|
105
|
+
return _s
|
106
|
+
|
107
|
+
def resolution(val: str | None) -> tuple[int, int] | None:
|
108
|
+
if val is None:
|
109
|
+
return None
|
110
|
+
vals = val.strip().split(",")
|
111
|
+
if len(vals) != 2:
|
112
|
+
raise CoerceError(f"'{val}': Resolution takes two numbers")
|
113
|
+
return natural(vals[0]), natural(vals[1])
|
114
|
+
|
115
|
+
def sample_rate(val: str) -> int:
|
116
|
+
num, unit = split_num_str(val)
|
117
|
+
if unit in {"kHz", "KHz"}:
|
118
|
+
return natural(num * 1000)
|
119
|
+
if unit not in {"", "Hz"}:
|
120
|
+
raise CoerceError(f"Unknown unit: '{unit}'")
|
121
|
+
return natural(num)
|
122
|
+
|
123
|
+
def _comma_coerce(name: str, val: str, num_args: int) -> list[str]:
|
124
|
+
vals = val.strip().split(",")
|
125
|
+
if num_args > len(vals):
|
126
|
+
raise CoerceError(f"Too few arguments for {name}.")
|
127
|
+
if len(vals) > num_args:
|
128
|
+
raise CoerceError(f"Too many arguments for {name}.")
|
129
|
+
return vals
|
130
|
+
|
131
|
+
def time_range(val: str) -> tuple[str, str]:
|
132
|
+
a = _comma_coerce("time_range", val, 2)
|
133
|
+
return a[0], a[1]
|
134
|
+
|
135
|
+
def speed_range(val: str) -> tuple[float, str, str]:
|
136
|
+
a = _comma_coerce("speed_range", val, 3)
|
137
|
+
return number(a[0]), a[1], a[2]
|
138
|
+
|
30
139
|
parser.add_required("input", nargs="*", metavar="[file | url ...] [options]")
|
31
140
|
parser.add_text("Editing Options:")
|
32
141
|
parser.add_argument(
|
@@ -41,6 +150,16 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
41
150
|
metavar="METHOD",
|
42
151
|
help="Set an expression which determines how to make auto edits",
|
43
152
|
)
|
153
|
+
parser.add_argument(
|
154
|
+
"--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
|
155
|
+
)
|
156
|
+
parser.add_argument(
|
157
|
+
"--output",
|
158
|
+
"--output-file",
|
159
|
+
"-o",
|
160
|
+
metavar="FILE",
|
161
|
+
help="Set the name/path of the new output file",
|
162
|
+
)
|
44
163
|
parser.add_argument(
|
45
164
|
"--silent-speed",
|
46
165
|
"-s",
|
@@ -111,12 +230,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
111
230
|
metavar="COLOR",
|
112
231
|
help="Set the background as a solid RGB color",
|
113
232
|
)
|
114
|
-
parser.add_argument(
|
115
|
-
"--add",
|
116
|
-
nargs="*",
|
117
|
-
metavar="OBJ:START,DUR,ATTRS?",
|
118
|
-
help="Insert an audio/video object to the timeline",
|
119
|
-
)
|
120
233
|
parser.add_text("URL Download Options:")
|
121
234
|
parser.add_argument(
|
122
235
|
"--yt-dlp-location",
|
@@ -138,28 +251,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
138
251
|
metavar="CMD",
|
139
252
|
help="Add extra options for yt-dlp. Must be in quotes",
|
140
253
|
)
|
141
|
-
parser.add_text("Utility Options:")
|
142
|
-
parser.add_argument(
|
143
|
-
"--export", "-ex", metavar="EXPORT:ATTRS?", help="Choose the export mode"
|
144
|
-
)
|
145
|
-
parser.add_argument(
|
146
|
-
"--output-file",
|
147
|
-
"--output",
|
148
|
-
"-o",
|
149
|
-
metavar="FILE",
|
150
|
-
help="Set the name/path of the new output file",
|
151
|
-
)
|
152
|
-
parser.add_argument(
|
153
|
-
"--player", "-p", metavar="CMD", help="Set player to open output media files"
|
154
|
-
)
|
155
|
-
parser.add_argument(
|
156
|
-
"--no-open", flag=True, help="Do not open the output file after editing is done"
|
157
|
-
)
|
158
|
-
parser.add_argument(
|
159
|
-
"--temp-dir",
|
160
|
-
metavar="PATH",
|
161
|
-
help="Set where the temporary directory is located",
|
162
|
-
)
|
163
254
|
parser.add_text("Display Options:")
|
164
255
|
parser.add_argument(
|
165
256
|
"--progress",
|
@@ -186,6 +277,16 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
186
277
|
flag=True,
|
187
278
|
help="Disable the inclusion of data streams in the output file",
|
188
279
|
)
|
280
|
+
parser.add_argument(
|
281
|
+
"--faststart",
|
282
|
+
flag=True,
|
283
|
+
help="Enable movflags +faststart, recommended for web (default)",
|
284
|
+
)
|
285
|
+
parser.add_argument(
|
286
|
+
"--no-faststart",
|
287
|
+
flag=True,
|
288
|
+
help="Disable movflags +faststart, will be faster for large files",
|
289
|
+
)
|
189
290
|
parser.add_argument(
|
190
291
|
"--fragmented",
|
191
292
|
flag=True,
|
@@ -241,10 +342,13 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
241
342
|
metavar="BITRATE",
|
242
343
|
help="Set the number of bits per second for audio",
|
243
344
|
)
|
345
|
+
parser.add_argument(
|
346
|
+
"--mix-audio-streams", flag=True, help="Mix all audio streams together into one"
|
347
|
+
)
|
244
348
|
parser.add_argument(
|
245
349
|
"--keep-tracks-separate",
|
246
350
|
flag=True,
|
247
|
-
help="Don't mix all audio
|
351
|
+
help="Don't mix all audio streams into one when exporting (default)",
|
248
352
|
)
|
249
353
|
parser.add_argument(
|
250
354
|
"--audio-normalize",
|
@@ -258,6 +362,17 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
258
362
|
parser.add_argument(
|
259
363
|
"--no-cache", flag=True, help="Don't look for or write a cache file"
|
260
364
|
)
|
365
|
+
parser.add_argument(
|
366
|
+
"--no-open", flag=True, help="Do not open the output file after editing is done"
|
367
|
+
)
|
368
|
+
parser.add_argument(
|
369
|
+
"--temp-dir",
|
370
|
+
metavar="PATH",
|
371
|
+
help="Set where the temporary directory is located",
|
372
|
+
)
|
373
|
+
parser.add_argument(
|
374
|
+
"--player", "-p", metavar="CMD", help="Set player to open output media files"
|
375
|
+
)
|
261
376
|
parser.add_argument("--version", "-V", flag=True, help="Display version and halt")
|
262
377
|
return parser
|
263
378
|
|
@@ -337,15 +452,15 @@ def main() -> None:
|
|
337
452
|
if args.debug and not args.input:
|
338
453
|
buf = StringIO()
|
339
454
|
buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
|
340
|
-
buf.write(f"Python: {plat.python_version()}\
|
455
|
+
buf.write(f"Python: {plat.python_version()}\nAV: ")
|
341
456
|
try:
|
342
|
-
import
|
457
|
+
import bv
|
343
458
|
except (ModuleNotFoundError, ImportError):
|
344
459
|
buf.write("not found")
|
345
460
|
else:
|
346
461
|
try:
|
347
|
-
buf.write(f"{
|
348
|
-
license =
|
462
|
+
buf.write(f"{bv.__version__} ")
|
463
|
+
license = bv._core.library_meta["libavcodec"]["license"]
|
349
464
|
buf.write(f"({license})")
|
350
465
|
except AttributeError:
|
351
466
|
buf.write("error")
|
auto_editor/analyze.py
CHANGED
@@ -9,10 +9,10 @@ from math import ceil
|
|
9
9
|
from tempfile import gettempdir
|
10
10
|
from typing import TYPE_CHECKING
|
11
11
|
|
12
|
-
import
|
12
|
+
import bv
|
13
13
|
import numpy as np
|
14
|
-
from
|
15
|
-
from
|
14
|
+
from bv.audio.fifo import AudioFifo
|
15
|
+
from bv.subtitles.subtitle import AssSubtitle
|
16
16
|
|
17
17
|
from auto_editor import __version__
|
18
18
|
|
@@ -72,7 +72,7 @@ def mut_remove_large(
|
|
72
72
|
active = False
|
73
73
|
|
74
74
|
|
75
|
-
def iter_audio(audio_stream:
|
75
|
+
def iter_audio(audio_stream: bv.AudioStream, tb: Fraction) -> Iterator[np.float32]:
|
76
76
|
fifo = AudioFifo()
|
77
77
|
sr = audio_stream.rate
|
78
78
|
|
@@ -80,10 +80,10 @@ def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float3
|
|
80
80
|
accumulated_error = Fraction(0)
|
81
81
|
|
82
82
|
# Resample so that audio data is between [-1, 1]
|
83
|
-
resampler =
|
83
|
+
resampler = bv.AudioResampler(bv.AudioFormat("flt"), audio_stream.layout, sr)
|
84
84
|
|
85
85
|
container = audio_stream.container
|
86
|
-
assert isinstance(container,
|
86
|
+
assert isinstance(container, bv.container.InputContainer)
|
87
87
|
|
88
88
|
for frame in container.decode(audio_stream):
|
89
89
|
frame.pts = None # Skip time checks
|
@@ -103,7 +103,7 @@ def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float3
|
|
103
103
|
|
104
104
|
|
105
105
|
def iter_motion(
|
106
|
-
video:
|
106
|
+
video: bv.VideoStream, tb: Fraction, blur: int, width: int
|
107
107
|
) -> Iterator[np.float32]:
|
108
108
|
video.thread_type = "AUTO"
|
109
109
|
|
@@ -113,7 +113,7 @@ def iter_motion(
|
|
113
113
|
index = 0
|
114
114
|
prev_index = -1
|
115
115
|
|
116
|
-
graph =
|
116
|
+
graph = bv.filter.Graph()
|
117
117
|
graph.link_nodes(
|
118
118
|
graph.add_buffer(template=video),
|
119
119
|
graph.add("scale", f"{width}:-1"),
|
@@ -123,7 +123,7 @@ def iter_motion(
|
|
123
123
|
).configure()
|
124
124
|
|
125
125
|
container = video.container
|
126
|
-
assert isinstance(container,
|
126
|
+
assert isinstance(container, bv.container.InputContainer)
|
127
127
|
|
128
128
|
for unframe in container.decode(video):
|
129
129
|
if unframe.pts is None:
|
@@ -154,7 +154,7 @@ def iter_motion(
|
|
154
154
|
|
155
155
|
@dataclass(slots=True)
|
156
156
|
class Levels:
|
157
|
-
container:
|
157
|
+
container: bv.container.InputContainer
|
158
158
|
name: str
|
159
159
|
mod_time: int
|
160
160
|
tb: Fraction
|
@@ -258,7 +258,7 @@ class Levels:
|
|
258
258
|
if audio.duration is not None and audio.time_base is not None:
|
259
259
|
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
|
260
260
|
elif container.duration is not None:
|
261
|
-
inaccurate_dur = int(container.duration /
|
261
|
+
inaccurate_dur = int(container.duration / bv.time_base * self.tb)
|
262
262
|
else:
|
263
263
|
inaccurate_dur = 1024
|
264
264
|
|
@@ -343,15 +343,9 @@ class Levels:
|
|
343
343
|
for packet in container.demux(subtitle_stream):
|
344
344
|
if packet.pts is None or packet.duration is None:
|
345
345
|
continue
|
346
|
-
|
347
|
-
# See definition of `AVSubtitle`
|
348
|
-
# in: https://ffmpeg.org/doxygen/trunk/avcodec_8h_source.html
|
349
|
-
start = float(packet.pts * subtitle_stream.time_base)
|
350
|
-
dur = float(packet.duration * subtitle_stream.time_base)
|
351
|
-
|
352
|
-
end = round((start + dur) * self.tb)
|
353
|
-
sub_length = max(sub_length, end)
|
346
|
+
sub_length = max(sub_length, packet.pts + packet.duration)
|
354
347
|
|
348
|
+
sub_length = round(sub_length * subtitle_stream.time_base * self.tb)
|
355
349
|
result = np.zeros((sub_length), dtype=np.bool_)
|
356
350
|
del sub_length
|
357
351
|
|
@@ -363,25 +357,25 @@ class Levels:
|
|
363
357
|
continue
|
364
358
|
if early_exit:
|
365
359
|
break
|
366
|
-
for subset in packet.decode():
|
367
|
-
if max_count is not None and count >= max_count:
|
368
|
-
early_exit = True
|
369
|
-
break
|
370
360
|
|
371
|
-
|
372
|
-
|
361
|
+
if max_count is not None and count >= max_count:
|
362
|
+
early_exit = True
|
363
|
+
break
|
364
|
+
|
365
|
+
start = float(packet.pts * subtitle_stream.time_base)
|
366
|
+
dur = float(packet.duration * subtitle_stream.time_base)
|
373
367
|
|
374
|
-
|
375
|
-
|
368
|
+
san_start = round(start * self.tb)
|
369
|
+
san_end = round((start + dur) * self.tb)
|
376
370
|
|
377
|
-
|
378
|
-
|
379
|
-
|
371
|
+
for sub in packet.decode():
|
372
|
+
if not isinstance(sub, AssSubtitle):
|
373
|
+
continue
|
380
374
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
375
|
+
line = sub.dialogue.decode(errors="ignore")
|
376
|
+
if line and re.search(re_pattern, line):
|
377
|
+
result[san_start:san_end] = 1
|
378
|
+
count += 1
|
385
379
|
|
386
380
|
container.seek(0)
|
387
381
|
return result
|
@@ -391,8 +385,8 @@ def initLevels(
|
|
391
385
|
src: FileInfo, tb: Fraction, bar: Bar, no_cache: bool, log: Log
|
392
386
|
) -> Levels:
|
393
387
|
try:
|
394
|
-
container =
|
395
|
-
except
|
388
|
+
container = bv.open(src.path)
|
389
|
+
except bv.FFmpegError as e:
|
396
390
|
log.error(e)
|
397
391
|
|
398
392
|
mod_time = int(src.path.stat().st_mtime)
|
auto_editor/cmds/info.py
CHANGED
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
6
6
|
from typing import Any, Literal, TypedDict
|
7
7
|
|
8
8
|
from auto_editor.ffwrapper import initFileInfo
|
9
|
-
from auto_editor.
|
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
|
12
12
|
from auto_editor.utils.func import aspect_ratio
|
auto_editor/cmds/levels.py
CHANGED
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|
5
5
|
from fractions import Fraction
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
-
import
|
8
|
+
import bv
|
9
9
|
import numpy as np
|
10
10
|
|
11
11
|
from auto_editor.analyze import *
|
@@ -134,7 +134,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
134
134
|
if (arr := levels.read_cache("audio", (obj["stream"],))) is not None:
|
135
135
|
print_arr(arr)
|
136
136
|
else:
|
137
|
-
container =
|
137
|
+
container = bv.open(src.path, "r")
|
138
138
|
audio_stream = container.streams.audio[obj["stream"]]
|
139
139
|
|
140
140
|
values = []
|
@@ -155,7 +155,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
155
155
|
if (arr := levels.read_cache("motion", mobj)) is not None:
|
156
156
|
print_arr(arr)
|
157
157
|
else:
|
158
|
-
container =
|
158
|
+
container = bv.open(src.path, "r")
|
159
159
|
video_stream = container.streams.video[obj["stream"]]
|
160
160
|
|
161
161
|
values = []
|
auto_editor/cmds/subdump.py
CHANGED
@@ -1,18 +1,72 @@
|
|
1
1
|
import sys
|
2
|
+
from dataclasses import dataclass, field
|
2
3
|
|
3
|
-
import
|
4
|
-
from
|
4
|
+
import bv
|
5
|
+
from bv.subtitles.subtitle import AssSubtitle
|
6
|
+
|
7
|
+
from auto_editor.json import dump
|
8
|
+
from auto_editor.vanparse import ArgumentParser
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass(slots=True)
|
12
|
+
class SubdumpArgs:
|
13
|
+
help: bool = False
|
14
|
+
input: list[str] = field(default_factory=list)
|
15
|
+
json: bool = False
|
5
16
|
|
6
17
|
|
7
18
|
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
8
|
-
|
9
|
-
|
19
|
+
parser = ArgumentParser("subdump")
|
20
|
+
parser.add_required("input", nargs="*")
|
21
|
+
parser.add_argument("--json", flag=True)
|
22
|
+
args = parser.parse_args(SubdumpArgs, sys_args)
|
23
|
+
|
24
|
+
do_filter = True
|
25
|
+
|
26
|
+
if args.json:
|
27
|
+
data = {}
|
28
|
+
for input_file in args.input:
|
29
|
+
container = bv.open(input_file)
|
10
30
|
for s in range(len(container.streams.subtitles)):
|
11
|
-
|
12
|
-
|
13
|
-
|
31
|
+
entry_data = []
|
32
|
+
|
33
|
+
input_stream = container.streams.subtitles[s]
|
34
|
+
assert input_stream.time_base is not None
|
35
|
+
for packet in container.demux(input_stream):
|
36
|
+
if (
|
37
|
+
packet.dts is None
|
38
|
+
or packet.pts is None
|
39
|
+
or packet.duration is None
|
40
|
+
):
|
41
|
+
continue
|
42
|
+
|
43
|
+
start = packet.pts * input_stream.time_base
|
44
|
+
end = start + packet.duration * input_stream.time_base
|
45
|
+
|
46
|
+
startf = round(float(start), 3)
|
47
|
+
endf = round(float(end), 3)
|
48
|
+
|
49
|
+
if do_filter and endf - startf <= 0.02:
|
50
|
+
continue
|
51
|
+
|
52
|
+
for sub in packet.decode():
|
14
53
|
if isinstance(sub, AssSubtitle):
|
15
|
-
|
54
|
+
content = sub.dialogue.decode("utf-8", errors="ignore")
|
55
|
+
entry_data.append([startf, endf, content])
|
56
|
+
|
57
|
+
data[f"{input_file}:{s}"] = entry_data
|
58
|
+
container.close()
|
59
|
+
|
60
|
+
dump(data, sys.stdout, indent=4)
|
61
|
+
return
|
62
|
+
|
63
|
+
for i, input_file in enumerate(args.input):
|
64
|
+
with bv.open(input_file) as container:
|
65
|
+
for s in range(len(container.streams.subtitles)):
|
66
|
+
print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
|
67
|
+
for sub2 in container.decode(subtitles=s):
|
68
|
+
if isinstance(sub2, AssSubtitle):
|
69
|
+
print(sub2.ass.decode("utf-8", errors="ignore"))
|
16
70
|
print("------")
|
17
71
|
|
18
72
|
|