auto-editor 28.0.2__py3-none-any.whl → 29.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-28.0.2.dist-info → auto_editor-29.0.0.dist-info}/METADATA +5 -4
- auto_editor-29.0.0.dist-info/RECORD +5 -0
- auto_editor-29.0.0.dist-info/top_level.txt +1 -0
- auto_editor/__init__.py +0 -1
- auto_editor/__main__.py +0 -503
- auto_editor/analyze.py +0 -393
- auto_editor/cmds/__init__.py +0 -0
- auto_editor/cmds/cache.py +0 -69
- auto_editor/cmds/desc.py +0 -32
- auto_editor/cmds/info.py +0 -213
- auto_editor/cmds/levels.py +0 -199
- auto_editor/cmds/palet.py +0 -29
- auto_editor/cmds/repl.py +0 -113
- auto_editor/cmds/subdump.py +0 -72
- auto_editor/cmds/test.py +0 -812
- auto_editor/edit.py +0 -548
- auto_editor/exports/__init__.py +0 -0
- auto_editor/exports/fcp11.py +0 -195
- auto_editor/exports/fcp7.py +0 -313
- auto_editor/exports/json.py +0 -63
- auto_editor/exports/shotcut.py +0 -147
- auto_editor/ffwrapper.py +0 -187
- auto_editor/help.py +0 -223
- auto_editor/imports/__init__.py +0 -0
- auto_editor/imports/fcp7.py +0 -275
- auto_editor/imports/json.py +0 -234
- auto_editor/json.py +0 -297
- auto_editor/lang/__init__.py +0 -0
- auto_editor/lang/libintrospection.py +0 -10
- auto_editor/lang/libmath.py +0 -23
- auto_editor/lang/palet.py +0 -724
- auto_editor/lang/stdenv.py +0 -1184
- auto_editor/lib/__init__.py +0 -0
- auto_editor/lib/contracts.py +0 -235
- auto_editor/lib/data_structs.py +0 -278
- auto_editor/lib/err.py +0 -2
- auto_editor/make_layers.py +0 -315
- auto_editor/preview.py +0 -93
- auto_editor/render/__init__.py +0 -0
- auto_editor/render/audio.py +0 -517
- auto_editor/render/subtitle.py +0 -205
- auto_editor/render/video.py +0 -312
- auto_editor/timeline.py +0 -331
- auto_editor/utils/__init__.py +0 -0
- auto_editor/utils/bar.py +0 -142
- auto_editor/utils/chunks.py +0 -2
- auto_editor/utils/cmdkw.py +0 -206
- auto_editor/utils/container.py +0 -102
- auto_editor/utils/func.py +0 -128
- auto_editor/utils/log.py +0 -124
- auto_editor/utils/types.py +0 -277
- auto_editor/vanparse.py +0 -313
- auto_editor-28.0.2.dist-info/RECORD +0 -56
- auto_editor-28.0.2.dist-info/entry_points.txt +0 -6
- auto_editor-28.0.2.dist-info/top_level.txt +0 -2
- docs/build.py +0 -70
- {auto_editor-28.0.2.dist-info → auto_editor-29.0.0.dist-info}/WHEEL +0 -0
- {auto_editor-28.0.2.dist-info → auto_editor-29.0.0.dist-info}/licenses/LICENSE +0 -0
auto_editor/analyze.py
DELETED
@@ -1,393 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import os
|
4
|
-
import re
|
5
|
-
from dataclasses import dataclass
|
6
|
-
from fractions import Fraction
|
7
|
-
from hashlib import sha1
|
8
|
-
from math import ceil
|
9
|
-
from tempfile import gettempdir
|
10
|
-
from typing import TYPE_CHECKING
|
11
|
-
|
12
|
-
import bv
|
13
|
-
import numpy as np
|
14
|
-
from bv.audio.fifo import AudioFifo
|
15
|
-
from bv.subtitles.subtitle import AssSubtitle
|
16
|
-
|
17
|
-
from auto_editor import __version__
|
18
|
-
|
19
|
-
if TYPE_CHECKING:
|
20
|
-
from collections.abc import Iterator, Sequence
|
21
|
-
from fractions import Fraction
|
22
|
-
|
23
|
-
from numpy.typing import NDArray
|
24
|
-
|
25
|
-
from auto_editor.ffwrapper import FileInfo
|
26
|
-
from auto_editor.utils.bar import Bar
|
27
|
-
from auto_editor.utils.log import Log
|
28
|
-
|
29
|
-
|
30
|
-
__all__ = ("LevelError", "initLevels", "iter_audio", "iter_motion")
|
31
|
-
|
32
|
-
|
33
|
-
class LevelError(Exception):
|
34
|
-
pass
|
35
|
-
|
36
|
-
|
37
|
-
def mut_remove_small(
|
38
|
-
arr: NDArray[np.bool_], lim: int, replace: int, with_: int
|
39
|
-
) -> None:
|
40
|
-
start_p = 0
|
41
|
-
active = False
|
42
|
-
for j, item in enumerate(arr):
|
43
|
-
if item == replace:
|
44
|
-
if not active:
|
45
|
-
start_p = j
|
46
|
-
active = True
|
47
|
-
|
48
|
-
if j == len(arr) - 1 and j - start_p < lim:
|
49
|
-
arr[start_p:] = with_
|
50
|
-
elif active:
|
51
|
-
if j - start_p < lim:
|
52
|
-
arr[start_p:j] = with_
|
53
|
-
active = False
|
54
|
-
|
55
|
-
|
56
|
-
def mut_remove_large(
|
57
|
-
arr: NDArray[np.bool_], lim: int, replace: int, with_: int
|
58
|
-
) -> None:
|
59
|
-
start_p = 0
|
60
|
-
active = False
|
61
|
-
for j, item in enumerate(arr):
|
62
|
-
if item == replace:
|
63
|
-
if not active:
|
64
|
-
start_p = j
|
65
|
-
active = True
|
66
|
-
|
67
|
-
if j == len(arr) - 1 and j - start_p >= lim:
|
68
|
-
arr[start_p:] = with_
|
69
|
-
elif active:
|
70
|
-
if j - start_p > lim:
|
71
|
-
arr[start_p:j] = with_
|
72
|
-
active = False
|
73
|
-
|
74
|
-
|
75
|
-
def iter_audio(audio_stream: bv.AudioStream, tb: Fraction) -> Iterator[np.float32]:
|
76
|
-
fifo = AudioFifo()
|
77
|
-
sr = audio_stream.rate
|
78
|
-
|
79
|
-
exact_size = (1 / tb) * sr
|
80
|
-
accumulated_error = Fraction(0)
|
81
|
-
|
82
|
-
# Resample so that audio data is between [-1, 1]
|
83
|
-
resampler = bv.AudioResampler(bv.AudioFormat("flt"), audio_stream.layout, sr)
|
84
|
-
|
85
|
-
container = audio_stream.container
|
86
|
-
assert isinstance(container, bv.container.InputContainer)
|
87
|
-
|
88
|
-
for frame in container.decode(audio_stream):
|
89
|
-
frame.pts = None # Skip time checks
|
90
|
-
|
91
|
-
for reframe in resampler.resample(frame):
|
92
|
-
fifo.write(reframe)
|
93
|
-
|
94
|
-
while fifo.samples >= ceil(exact_size):
|
95
|
-
size_with_error = exact_size + accumulated_error
|
96
|
-
current_size = round(size_with_error)
|
97
|
-
accumulated_error = size_with_error - current_size
|
98
|
-
|
99
|
-
audio_chunk = fifo.read(current_size)
|
100
|
-
assert audio_chunk is not None
|
101
|
-
arr = audio_chunk.to_ndarray().flatten()
|
102
|
-
yield np.max(np.abs(arr))
|
103
|
-
|
104
|
-
|
105
|
-
def iter_motion(
|
106
|
-
video: bv.VideoStream, tb: Fraction, blur: int, width: int
|
107
|
-
) -> Iterator[np.float32]:
|
108
|
-
video.thread_type = "AUTO"
|
109
|
-
|
110
|
-
prev_frame = None
|
111
|
-
current_frame = None
|
112
|
-
total_pixels = None
|
113
|
-
index = 0
|
114
|
-
prev_index = -1
|
115
|
-
|
116
|
-
graph = bv.filter.Graph()
|
117
|
-
graph.link_nodes(
|
118
|
-
graph.add_buffer(template=video),
|
119
|
-
graph.add("scale", f"{width}:-1"),
|
120
|
-
graph.add("format", "gray"),
|
121
|
-
graph.add("gblur", f"sigma={blur}"),
|
122
|
-
graph.add("buffersink"),
|
123
|
-
).configure()
|
124
|
-
|
125
|
-
container = video.container
|
126
|
-
assert isinstance(container, bv.container.InputContainer)
|
127
|
-
|
128
|
-
for unframe in container.decode(video):
|
129
|
-
if unframe.pts is None:
|
130
|
-
continue
|
131
|
-
|
132
|
-
graph.push(unframe)
|
133
|
-
frame = graph.vpull()
|
134
|
-
assert frame.time is not None
|
135
|
-
index = round(frame.time * tb)
|
136
|
-
|
137
|
-
if total_pixels is None:
|
138
|
-
total_pixels = frame.width * frame.height
|
139
|
-
|
140
|
-
current_frame = frame.to_ndarray()
|
141
|
-
if prev_frame is None:
|
142
|
-
value = np.float32(0.0)
|
143
|
-
else:
|
144
|
-
# Use `int16` to avoid underflow with `uint8` datatype
|
145
|
-
diff = np.abs(prev_frame.astype(np.int16) - current_frame.astype(np.int16))
|
146
|
-
value = np.float32(np.count_nonzero(diff) / total_pixels)
|
147
|
-
|
148
|
-
for _ in range(index - prev_index):
|
149
|
-
yield value
|
150
|
-
|
151
|
-
prev_frame = current_frame
|
152
|
-
prev_index = index
|
153
|
-
|
154
|
-
|
155
|
-
@dataclass(slots=True)
|
156
|
-
class Levels:
|
157
|
-
container: bv.container.InputContainer
|
158
|
-
name: str
|
159
|
-
mod_time: int
|
160
|
-
tb: Fraction
|
161
|
-
bar: Bar
|
162
|
-
no_cache: bool
|
163
|
-
log: Log
|
164
|
-
|
165
|
-
@property
|
166
|
-
def media_length(self) -> int:
|
167
|
-
container = self.container
|
168
|
-
if container.streams.audio:
|
169
|
-
if (arr := self.read_cache("audio", (0,))) is not None:
|
170
|
-
return len(arr)
|
171
|
-
|
172
|
-
audio_stream = container.streams.audio[0]
|
173
|
-
result = sum(1 for _ in iter_audio(audio_stream, self.tb))
|
174
|
-
container.seek(0)
|
175
|
-
self.log.debug(f"Audio Length: {result}")
|
176
|
-
return result
|
177
|
-
|
178
|
-
# If there's no audio, get length in video metadata.
|
179
|
-
if not container.streams.video:
|
180
|
-
self.log.error("Could not get media duration")
|
181
|
-
|
182
|
-
video = container.streams.video[0]
|
183
|
-
if video.duration is None or video.time_base is None:
|
184
|
-
dur = 0
|
185
|
-
else:
|
186
|
-
dur = int(video.duration * video.time_base * self.tb)
|
187
|
-
self.log.debug(f"Video duration: {dur}")
|
188
|
-
container.seek(0)
|
189
|
-
|
190
|
-
return dur
|
191
|
-
|
192
|
-
def obj_tag(self, kind: str, obj: Sequence[object]) -> str:
|
193
|
-
mod_time = self.mod_time
|
194
|
-
key = f"{self.name}:{mod_time:x}:{self.tb}:" + ",".join(f"{v}" for v in obj)
|
195
|
-
return f"{sha1(key.encode()).hexdigest()[:16]}{kind}"
|
196
|
-
|
197
|
-
def none(self) -> NDArray[np.bool_]:
|
198
|
-
return np.ones(self.media_length, dtype=np.bool_)
|
199
|
-
|
200
|
-
def all(self) -> NDArray[np.bool_]:
|
201
|
-
return np.zeros(self.media_length, dtype=np.bool_)
|
202
|
-
|
203
|
-
def read_cache(self, kind: str, obj: Sequence[object]) -> None | np.ndarray:
|
204
|
-
if self.no_cache:
|
205
|
-
return None
|
206
|
-
|
207
|
-
key = self.obj_tag(kind, obj)
|
208
|
-
cache_file = os.path.join(gettempdir(), f"ae-{__version__}", f"{key}.npz")
|
209
|
-
|
210
|
-
try:
|
211
|
-
with np.load(cache_file, allow_pickle=False) as npzfile:
|
212
|
-
return npzfile["data"]
|
213
|
-
except Exception as e:
|
214
|
-
self.log.debug(e)
|
215
|
-
return None
|
216
|
-
|
217
|
-
def cache(self, arr: np.ndarray, kind: str, obj: Sequence[object]) -> np.ndarray:
|
218
|
-
if self.no_cache:
|
219
|
-
return arr
|
220
|
-
|
221
|
-
workdir = os.path.join(gettempdir(), f"ae-{__version__}")
|
222
|
-
os.makedirs(workdir, exist_ok=True)
|
223
|
-
cache_file = os.path.join(workdir, f"{self.obj_tag(kind, obj)}.npz")
|
224
|
-
|
225
|
-
try:
|
226
|
-
np.savez(cache_file, data=arr)
|
227
|
-
except Exception as e:
|
228
|
-
self.log.warning(f"Cache write failed: {e}")
|
229
|
-
|
230
|
-
cache_entries = []
|
231
|
-
with os.scandir(workdir) as entries:
|
232
|
-
for entry in entries:
|
233
|
-
if entry.name.endswith(".npz"):
|
234
|
-
cache_entries.append((entry.path, entry.stat().st_mtime))
|
235
|
-
|
236
|
-
if len(cache_entries) > 10:
|
237
|
-
# Sort by modification time, oldest first
|
238
|
-
cache_entries.sort(key=lambda x: x[1])
|
239
|
-
# Remove oldest files until we're back to 10
|
240
|
-
for filepath, _ in cache_entries[:-10]:
|
241
|
-
try:
|
242
|
-
os.remove(filepath)
|
243
|
-
except OSError:
|
244
|
-
pass
|
245
|
-
|
246
|
-
return arr
|
247
|
-
|
248
|
-
def audio(self, stream: int) -> NDArray[np.float32]:
|
249
|
-
container = self.container
|
250
|
-
if stream >= len(container.streams.audio):
|
251
|
-
raise LevelError(f"audio: audio stream '{stream}' does not exist.")
|
252
|
-
|
253
|
-
if (arr := self.read_cache("audio", (stream,))) is not None:
|
254
|
-
return arr
|
255
|
-
|
256
|
-
audio = container.streams.audio[stream]
|
257
|
-
|
258
|
-
if audio.duration is not None and audio.time_base is not None:
|
259
|
-
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
|
260
|
-
elif container.duration is not None:
|
261
|
-
inaccurate_dur = int(container.duration / bv.time_base * self.tb)
|
262
|
-
else:
|
263
|
-
inaccurate_dur = 1024
|
264
|
-
|
265
|
-
bar = self.bar
|
266
|
-
bar.start(inaccurate_dur, "Analyzing audio volume")
|
267
|
-
|
268
|
-
result: NDArray[np.float32] = np.zeros(inaccurate_dur, dtype=np.float32)
|
269
|
-
index = 0
|
270
|
-
|
271
|
-
for value in iter_audio(audio, self.tb):
|
272
|
-
if index > len(result) - 1:
|
273
|
-
result = np.concatenate(
|
274
|
-
(result, np.zeros(len(result), dtype=np.float32))
|
275
|
-
)
|
276
|
-
|
277
|
-
result[index] = value
|
278
|
-
bar.tick(index)
|
279
|
-
index += 1
|
280
|
-
|
281
|
-
bar.end()
|
282
|
-
container.seek(0)
|
283
|
-
assert len(result) > 0
|
284
|
-
return self.cache(result[:index], "audio", (stream,))
|
285
|
-
|
286
|
-
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
|
287
|
-
container = self.container
|
288
|
-
if stream >= len(container.streams.video):
|
289
|
-
raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
290
|
-
|
291
|
-
mobj = (stream, width, blur)
|
292
|
-
if (arr := self.read_cache("motion", mobj)) is not None:
|
293
|
-
return arr
|
294
|
-
|
295
|
-
video = container.streams.video[stream]
|
296
|
-
inaccurate_dur = (
|
297
|
-
1024
|
298
|
-
if video.duration is None or video.time_base is None
|
299
|
-
else int(video.duration * video.time_base * self.tb)
|
300
|
-
)
|
301
|
-
bar = self.bar
|
302
|
-
bar.start(inaccurate_dur, "Analyzing motion")
|
303
|
-
|
304
|
-
result: NDArray[np.float32] = np.zeros(inaccurate_dur, dtype=np.float32)
|
305
|
-
index = 0
|
306
|
-
for value in iter_motion(video, self.tb, blur, width):
|
307
|
-
if index > len(result) - 1:
|
308
|
-
result = np.concatenate(
|
309
|
-
(result, np.zeros(len(result), dtype=np.float32))
|
310
|
-
)
|
311
|
-
result[index] = value
|
312
|
-
bar.tick(index)
|
313
|
-
index += 1
|
314
|
-
|
315
|
-
bar.end()
|
316
|
-
container.seek(0)
|
317
|
-
return self.cache(result[:index], "motion", mobj)
|
318
|
-
|
319
|
-
def subtitle(
|
320
|
-
self,
|
321
|
-
pattern: str,
|
322
|
-
stream: int,
|
323
|
-
ignore_case: bool,
|
324
|
-
max_count: int | None,
|
325
|
-
) -> NDArray[np.bool_]:
|
326
|
-
container = self.container
|
327
|
-
if stream >= len(container.streams.subtitles):
|
328
|
-
raise LevelError(f"subtitle: subtitle stream '{stream}' does not exist.")
|
329
|
-
|
330
|
-
try:
|
331
|
-
flags = re.IGNORECASE if ignore_case else 0
|
332
|
-
re_pattern = re.compile(pattern, flags)
|
333
|
-
except re.error as e:
|
334
|
-
self.log.error(e)
|
335
|
-
try:
|
336
|
-
subtitle_stream = container.streams.subtitles[stream]
|
337
|
-
assert isinstance(subtitle_stream.time_base, Fraction)
|
338
|
-
except Exception as e:
|
339
|
-
self.log.error(e)
|
340
|
-
|
341
|
-
# Get the length of the subtitle stream.
|
342
|
-
sub_length = 0
|
343
|
-
for packet in container.demux(subtitle_stream):
|
344
|
-
if packet.pts is None or packet.duration is None:
|
345
|
-
continue
|
346
|
-
sub_length = max(sub_length, packet.pts + packet.duration)
|
347
|
-
|
348
|
-
sub_length = round(sub_length * subtitle_stream.time_base * self.tb)
|
349
|
-
result = np.zeros((sub_length), dtype=np.bool_)
|
350
|
-
del sub_length
|
351
|
-
|
352
|
-
count = 0
|
353
|
-
early_exit = False
|
354
|
-
container.seek(0)
|
355
|
-
for packet in container.demux(subtitle_stream):
|
356
|
-
if packet.pts is None or packet.duration is None:
|
357
|
-
continue
|
358
|
-
if early_exit:
|
359
|
-
break
|
360
|
-
|
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)
|
367
|
-
|
368
|
-
san_start = round(start * self.tb)
|
369
|
-
san_end = round((start + dur) * self.tb)
|
370
|
-
|
371
|
-
for sub in packet.decode():
|
372
|
-
if not isinstance(sub, AssSubtitle):
|
373
|
-
continue
|
374
|
-
|
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
|
379
|
-
|
380
|
-
container.seek(0)
|
381
|
-
return result
|
382
|
-
|
383
|
-
|
384
|
-
def initLevels(
|
385
|
-
src: FileInfo, tb: Fraction, bar: Bar, no_cache: bool, log: Log
|
386
|
-
) -> Levels:
|
387
|
-
try:
|
388
|
-
container = bv.open(src.path)
|
389
|
-
except bv.FFmpegError as e:
|
390
|
-
log.error(e)
|
391
|
-
|
392
|
-
mod_time = int(src.path.stat().st_mtime)
|
393
|
-
return Levels(container, src.path.name, mod_time, tb, bar, no_cache, log)
|
auto_editor/cmds/__init__.py
DELETED
File without changes
|
auto_editor/cmds/cache.py
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
import glob
|
2
|
-
import os
|
3
|
-
import sys
|
4
|
-
from shutil import rmtree
|
5
|
-
from tempfile import gettempdir
|
6
|
-
|
7
|
-
import numpy as np
|
8
|
-
|
9
|
-
from auto_editor import __version__
|
10
|
-
|
11
|
-
|
12
|
-
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
13
|
-
cache_dir = os.path.join(gettempdir(), f"ae-{__version__}")
|
14
|
-
|
15
|
-
if sys_args and sys_args[0] in {"clean", "clear"}:
|
16
|
-
rmtree(cache_dir, ignore_errors=True)
|
17
|
-
return
|
18
|
-
|
19
|
-
if not os.path.exists(cache_dir):
|
20
|
-
print("Empty cache")
|
21
|
-
return
|
22
|
-
|
23
|
-
cache_files = glob.glob(os.path.join(cache_dir, "*.npz"))
|
24
|
-
if not cache_files:
|
25
|
-
print("Empty cache")
|
26
|
-
return
|
27
|
-
|
28
|
-
def format_bytes(size: float) -> str:
|
29
|
-
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
30
|
-
if size < 1024:
|
31
|
-
return f"{size:.2f} {unit}"
|
32
|
-
size /= 1024
|
33
|
-
return f"{size:.2f} PiB"
|
34
|
-
|
35
|
-
GRAY = "\033[90m"
|
36
|
-
GREEN = "\033[32m"
|
37
|
-
BLUE = "\033[34m"
|
38
|
-
YELLOW = "\033[33m"
|
39
|
-
RESET = "\033[0m"
|
40
|
-
|
41
|
-
total_size = 0
|
42
|
-
for cache_file in cache_files:
|
43
|
-
try:
|
44
|
-
with np.load(cache_file, allow_pickle=False) as npzfile:
|
45
|
-
array = npzfile["data"]
|
46
|
-
key = os.path.basename(cache_file)[:-4] # Remove .npz extension
|
47
|
-
|
48
|
-
hash_part = key[:16]
|
49
|
-
rest_part = key[16:]
|
50
|
-
|
51
|
-
size = array.nbytes
|
52
|
-
total_size += size
|
53
|
-
size_str = format_bytes(size)
|
54
|
-
size_num, size_unit = size_str.rsplit(" ", 1)
|
55
|
-
|
56
|
-
print(
|
57
|
-
f"{YELLOW}entry: {GRAY}{hash_part}{RESET}{rest_part} "
|
58
|
-
f"{YELLOW}size: {GREEN}{size_num} {BLUE}{size_unit}{RESET}"
|
59
|
-
)
|
60
|
-
except Exception as e:
|
61
|
-
print(f"Error reading {cache_file}: {e}")
|
62
|
-
|
63
|
-
total_str = format_bytes(total_size)
|
64
|
-
total_num, total_unit = total_str.rsplit(" ", 1)
|
65
|
-
print(f"\n{YELLOW}total cache size: {GREEN}{total_num} {BLUE}{total_unit}{RESET}")
|
66
|
-
|
67
|
-
|
68
|
-
if __name__ == "__main__":
|
69
|
-
main()
|
auto_editor/cmds/desc.py
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
import sys
|
2
|
-
from dataclasses import dataclass, field
|
3
|
-
|
4
|
-
import bv
|
5
|
-
|
6
|
-
from auto_editor.vanparse import ArgumentParser
|
7
|
-
|
8
|
-
|
9
|
-
@dataclass(slots=True)
|
10
|
-
class DescArgs:
|
11
|
-
help: bool = False
|
12
|
-
input: list[str] = field(default_factory=list)
|
13
|
-
|
14
|
-
|
15
|
-
def desc_options(parser: ArgumentParser) -> ArgumentParser:
|
16
|
-
parser.add_required("input", nargs="*")
|
17
|
-
return parser
|
18
|
-
|
19
|
-
|
20
|
-
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
21
|
-
args = desc_options(ArgumentParser("desc")).parse_args(DescArgs, sys_args)
|
22
|
-
for path in args.input:
|
23
|
-
try:
|
24
|
-
container = bv.open(path)
|
25
|
-
desc = container.metadata.get("description", None)
|
26
|
-
except Exception:
|
27
|
-
desc = None
|
28
|
-
sys.stdout.write("\nNo description.\n\n" if desc is None else f"\n{desc}\n\n")
|
29
|
-
|
30
|
-
|
31
|
-
if __name__ == "__main__":
|
32
|
-
main()
|
auto_editor/cmds/info.py
DELETED
@@ -1,213 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import os.path
|
4
|
-
import sys
|
5
|
-
from dataclasses import dataclass, field
|
6
|
-
from typing import Any, Literal, TypedDict
|
7
|
-
|
8
|
-
from auto_editor.ffwrapper import FileInfo
|
9
|
-
from auto_editor.json import dump
|
10
|
-
from auto_editor.make_layers import make_sane_timebase
|
11
|
-
from auto_editor.utils.func import aspect_ratio
|
12
|
-
from auto_editor.utils.log import Log
|
13
|
-
from auto_editor.vanparse import ArgumentParser
|
14
|
-
|
15
|
-
|
16
|
-
@dataclass(slots=True)
|
17
|
-
class InfoArgs:
|
18
|
-
json: bool = False
|
19
|
-
help: bool = False
|
20
|
-
input: list[str] = field(default_factory=list)
|
21
|
-
|
22
|
-
|
23
|
-
def info_options(parser: ArgumentParser) -> ArgumentParser:
|
24
|
-
parser.add_required("input", nargs="*")
|
25
|
-
parser.add_argument("--json", flag=True, help="Export info in JSON format")
|
26
|
-
return parser
|
27
|
-
|
28
|
-
|
29
|
-
class VideoJson(TypedDict):
|
30
|
-
codec: str
|
31
|
-
fps: str
|
32
|
-
resolution: list[int]
|
33
|
-
aspect_ratio: list[int]
|
34
|
-
pixel_aspect_ratio: str
|
35
|
-
duration: float
|
36
|
-
pix_fmt: str | None
|
37
|
-
color_range: int
|
38
|
-
color_space: int
|
39
|
-
color_primaries: int
|
40
|
-
color_transfer: int
|
41
|
-
timebase: str
|
42
|
-
bitrate: int
|
43
|
-
lang: str | None
|
44
|
-
|
45
|
-
|
46
|
-
class AudioJson(TypedDict):
|
47
|
-
codec: str
|
48
|
-
samplerate: int
|
49
|
-
layout: str
|
50
|
-
duration: float
|
51
|
-
bitrate: int
|
52
|
-
lang: str | None
|
53
|
-
|
54
|
-
|
55
|
-
class SubtitleJson(TypedDict):
|
56
|
-
codec: str
|
57
|
-
lang: str | None
|
58
|
-
|
59
|
-
|
60
|
-
class ContainerJson(TypedDict):
|
61
|
-
duration: float
|
62
|
-
bitrate: int
|
63
|
-
|
64
|
-
|
65
|
-
class MediaJson(TypedDict, total=False):
|
66
|
-
video: list[VideoJson]
|
67
|
-
audio: list[AudioJson]
|
68
|
-
subtitle: list[SubtitleJson]
|
69
|
-
container: ContainerJson
|
70
|
-
type: Literal["media", "timeline", "unknown"]
|
71
|
-
recommendedTimebase: str
|
72
|
-
version: Literal["v1", "v3"]
|
73
|
-
clips: int
|
74
|
-
|
75
|
-
|
76
|
-
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
77
|
-
args = info_options(ArgumentParser("info")).parse_args(InfoArgs, sys_args)
|
78
|
-
|
79
|
-
log = Log(quiet=not args.json)
|
80
|
-
|
81
|
-
file_info: dict[str, MediaJson] = {}
|
82
|
-
|
83
|
-
for file in args.input:
|
84
|
-
if not os.path.isfile(file):
|
85
|
-
log.error(f"Could not find '{file}'")
|
86
|
-
|
87
|
-
ext = os.path.splitext(file)[1]
|
88
|
-
if ext in {".v1", ".v3", ".json", ".xml", ".fcpxml", ".mlt"}:
|
89
|
-
file_info[file] = {"type": "timeline"}
|
90
|
-
continue
|
91
|
-
|
92
|
-
src = FileInfo.init(file, log)
|
93
|
-
|
94
|
-
if len(src.videos) + len(src.audios) + len(src.subtitles) == 0:
|
95
|
-
file_info[file] = {"type": "unknown"}
|
96
|
-
continue
|
97
|
-
|
98
|
-
file_info[file] = {
|
99
|
-
"type": "media",
|
100
|
-
"recommendedTimebase": "30/1",
|
101
|
-
"video": [],
|
102
|
-
"audio": [],
|
103
|
-
"subtitle": [],
|
104
|
-
"container": {
|
105
|
-
"duration": src.duration,
|
106
|
-
"bitrate": src.bitrate,
|
107
|
-
},
|
108
|
-
}
|
109
|
-
|
110
|
-
if src.videos:
|
111
|
-
recTb = make_sane_timebase(src.videos[0].fps)
|
112
|
-
file_info[file]["recommendedTimebase"] = (
|
113
|
-
f"{recTb.numerator}/{recTb.denominator}"
|
114
|
-
)
|
115
|
-
|
116
|
-
for v in src.videos:
|
117
|
-
w, h = v.width, v.height
|
118
|
-
|
119
|
-
vid: VideoJson = {
|
120
|
-
"codec": v.codec,
|
121
|
-
"fps": str(v.fps),
|
122
|
-
"resolution": [w, h],
|
123
|
-
"aspect_ratio": list(aspect_ratio(w, h)),
|
124
|
-
"pixel_aspect_ratio": str(v.sar).replace("/", ":"),
|
125
|
-
"duration": v.duration,
|
126
|
-
"pix_fmt": v.pix_fmt,
|
127
|
-
"color_range": v.color_range,
|
128
|
-
"color_space": v.color_space,
|
129
|
-
"color_primaries": v.color_primaries,
|
130
|
-
"color_transfer": v.color_transfer,
|
131
|
-
"timebase": str(v.time_base),
|
132
|
-
"bitrate": v.bitrate,
|
133
|
-
"lang": v.lang,
|
134
|
-
}
|
135
|
-
file_info[file]["video"].append(vid)
|
136
|
-
|
137
|
-
for a in src.audios:
|
138
|
-
aud: AudioJson = {
|
139
|
-
"codec": a.codec,
|
140
|
-
"layout": a.layout,
|
141
|
-
"samplerate": a.samplerate,
|
142
|
-
"duration": a.duration,
|
143
|
-
"bitrate": a.bitrate,
|
144
|
-
"lang": a.lang,
|
145
|
-
}
|
146
|
-
file_info[file]["audio"].append(aud)
|
147
|
-
|
148
|
-
for s_stream in src.subtitles:
|
149
|
-
sub: SubtitleJson = {"codec": s_stream.codec, "lang": s_stream.lang}
|
150
|
-
file_info[file]["subtitle"].append(sub)
|
151
|
-
|
152
|
-
if args.json:
|
153
|
-
if sys.platform == "win32":
|
154
|
-
sys.stdout.reconfigure(encoding="utf-8")
|
155
|
-
dump(file_info, sys.stdout, indent=4)
|
156
|
-
return
|
157
|
-
|
158
|
-
def is_null(key: str, val: object) -> bool:
|
159
|
-
return val is None or (key in {"bitrate", "duration"} and val == 0.0)
|
160
|
-
|
161
|
-
def stream_to_text(text: str, label: str, streams: list[dict[str, Any]]) -> str:
|
162
|
-
if len(streams) > 0:
|
163
|
-
text += f" - {label}:\n"
|
164
|
-
|
165
|
-
for s, stream in enumerate(streams):
|
166
|
-
text += f" - track {s}:\n"
|
167
|
-
for key, value in stream.items():
|
168
|
-
if not is_null(key, value):
|
169
|
-
if isinstance(value, list):
|
170
|
-
sep = "x" if key == "resolution" else ":"
|
171
|
-
value = sep.join(f"{x}" for x in value)
|
172
|
-
|
173
|
-
if key in {
|
174
|
-
"color_range",
|
175
|
-
"color_space",
|
176
|
-
"color_transfer",
|
177
|
-
"color_primaries",
|
178
|
-
}:
|
179
|
-
if key == "color_range":
|
180
|
-
if value == 1:
|
181
|
-
text += " - color range: 1 (tv)\n"
|
182
|
-
elif value == 2:
|
183
|
-
text += " - color range: 2 (pc)\n"
|
184
|
-
elif value == 1:
|
185
|
-
text += f" - {key.replace('_', ' ')}: 1 (bt709)\n"
|
186
|
-
elif value != 2:
|
187
|
-
text += f" - {key.replace('_', ' ')}: {value}\n"
|
188
|
-
else:
|
189
|
-
text += f" - {key.replace('_', ' ')}: {value}\n"
|
190
|
-
return text
|
191
|
-
|
192
|
-
text = ""
|
193
|
-
for name, info in file_info.items():
|
194
|
-
text += f"{name}:\n"
|
195
|
-
|
196
|
-
for label, streams in info.items():
|
197
|
-
if isinstance(streams, list):
|
198
|
-
text = stream_to_text(text, label, streams)
|
199
|
-
continue
|
200
|
-
elif isinstance(streams, dict):
|
201
|
-
text += " - container:\n"
|
202
|
-
for key, value in streams.items():
|
203
|
-
if value is not None:
|
204
|
-
text += f" - {key}: {value}\n"
|
205
|
-
elif label != "type" or streams != "media":
|
206
|
-
text += f" - {label}: {streams}\n"
|
207
|
-
text += "\n"
|
208
|
-
|
209
|
-
sys.stdout.write(text)
|
210
|
-
|
211
|
-
|
212
|
-
if __name__ == "__main__":
|
213
|
-
main()
|