auto-editor 25.2.0__py3-none-any.whl → 25.3.1__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/output.py CHANGED
@@ -46,9 +46,9 @@ class Ensure:
46
46
  astream = in_container.streams.audio[stream]
47
47
 
48
48
  if astream.duration is None or astream.time_base is None:
49
- dur = 1
49
+ dur = 1.0
50
50
  else:
51
- dur = int(astream.duration * astream.time_base)
51
+ dur = float(astream.duration * astream.time_base)
52
52
 
53
53
  bar.start(dur, "Extracting audio")
54
54
 
@@ -58,8 +58,8 @@ class Ensure:
58
58
 
59
59
  resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate)
60
60
  for i, frame in enumerate(in_container.decode(astream)):
61
- if i % 1500 == 0:
62
- bar.tick(0 if frame.time is None else frame.time)
61
+ if i % 1500 == 0 and frame.time is not None:
62
+ bar.tick(frame.time)
63
63
 
64
64
  for new_frame in resampler.resample(frame):
65
65
  for packet in output_astream.encode(new_frame):
@@ -237,8 +237,8 @@ def mux_quality_media(
237
237
  if s_tracks > 0:
238
238
  cmd.extend(["-map", "0:t?"]) # Add input attachments to output.
239
239
 
240
- # This was causing a crash for 'example.mp4 multi-track.mov'
241
- # cmd.extend(["-map", "0:d?"])
240
+ if not args.dn:
241
+ cmd.extend(["-map", "0:d?"])
242
242
 
243
243
  cmd.append(output_path)
244
- ffmpeg.run_check_errors(cmd, log, path=output_path)
244
+ ffmpeg.run_check_errors(cmd, path=output_path)
auto_editor/preview.py CHANGED
@@ -7,7 +7,7 @@ from typing import TextIO
7
7
 
8
8
  from auto_editor.analyze import Levels
9
9
  from auto_editor.timeline import v3
10
- from auto_editor.utils.bar import Bar
10
+ from auto_editor.utils.bar import initBar
11
11
  from auto_editor.utils.func import to_timecode
12
12
  from auto_editor.utils.log import Log
13
13
 
@@ -65,7 +65,7 @@ def preview(tl: v3, log: Log) -> None:
65
65
 
66
66
  in_len = 0
67
67
  for src in all_sources:
68
- in_len += Levels(src, tb, Bar("none"), False, log, False).media_length
68
+ in_len += Levels(src, tb, initBar("none"), False, log, False).media_length
69
69
 
70
70
  out_len = tl.out_len()
71
71
 
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import io
3
4
  from pathlib import Path
4
5
  from platform import system
5
6
  from subprocess import PIPE
6
7
 
8
+ import av
7
9
  import numpy as np
8
10
 
9
11
  from auto_editor.ffwrapper import FFmpeg, FileInfo
@@ -12,7 +14,7 @@ from auto_editor.lang.palet import env
12
14
  from auto_editor.lib.contracts import andc, between_c, is_int_or_float
13
15
  from auto_editor.lib.err import MyError
14
16
  from auto_editor.output import Ensure
15
- from auto_editor.timeline import v3
17
+ from auto_editor.timeline import TlAudio, v3
16
18
  from auto_editor.utils.bar import Bar
17
19
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
18
20
  from auto_editor.utils.log import Log
@@ -165,6 +167,67 @@ def apply_audio_normalization(
165
167
  ffmpeg.run(["-i", f"{pre_master}"] + cmd + [f"{path}"])
166
168
 
167
169
 
170
+ def process_audio_clip(
171
+ clip: TlAudio, samp_list: AudioData, samp_start: int, samp_end: int, sr: int
172
+ ) -> AudioData:
173
+ input_buffer = io.BytesIO()
174
+ write(input_buffer, sr, samp_list[samp_start:samp_end])
175
+ input_buffer.seek(0)
176
+
177
+ input_file = av.open(input_buffer, "r")
178
+ input_stream = input_file.streams.audio[0]
179
+
180
+ output_bytes = io.BytesIO()
181
+ output_file = av.open(output_bytes, mode="w", format="wav")
182
+ output_stream = output_file.add_stream("pcm_s16le", rate=sr)
183
+
184
+ graph = av.filter.Graph()
185
+ args = [graph.add_abuffer(template=input_stream)]
186
+
187
+ if clip.speed != 1:
188
+ if clip.speed > 10_000:
189
+ for _ in range(3):
190
+ args.append(graph.add("atempo", f"{clip.speed ** (1/3)}"))
191
+ elif clip.speed > 100:
192
+ for _ in range(2):
193
+ args.append(graph.add("atempo", f"{clip.speed ** 0.5}"))
194
+ elif clip.speed >= 0.5:
195
+ args.append(graph.add("atempo", f"{clip.speed}"))
196
+ else:
197
+ start = 0.5
198
+ while start * 0.5 > clip.speed:
199
+ start *= 0.5
200
+ args.append(graph.add("atempo", "0.5"))
201
+ args.append(graph.add("atempo", f"{clip.speed / start}"))
202
+
203
+ if clip.volume != 1:
204
+ args.append(graph.add("volume", f"{clip.volume}"))
205
+
206
+ args.append(graph.add("abuffersink"))
207
+ graph.link_nodes(*args).configure()
208
+
209
+ for frame in input_file.decode(input_stream):
210
+ graph.push(frame)
211
+ while True:
212
+ try:
213
+ aframe = graph.pull()
214
+ assert isinstance(aframe, av.AudioFrame)
215
+ for packet in output_stream.encode(aframe):
216
+ output_file.mux(packet)
217
+ except (av.BlockingIOError, av.EOFError):
218
+ break
219
+
220
+ # Flush the stream
221
+ for packet in output_stream.encode(None):
222
+ output_file.mux(packet)
223
+
224
+ input_file.close()
225
+ output_file.close()
226
+
227
+ output_bytes.seek(0)
228
+ return read(output_bytes)[1]
229
+
230
+
168
231
  def make_new_audio(
169
232
  tl: v3, ensure: Ensure, args: Args, ffmpeg: FFmpeg, bar: Bar, log: Log
170
233
  ) -> list[str]:
@@ -175,7 +238,6 @@ def make_new_audio(
175
238
 
176
239
  norm = parse_norm(args.audio_normalize, log)
177
240
 
178
- af_tick = 0
179
241
  temp = log.temp
180
242
 
181
243
  if not tl.a or not tl.a[0]:
@@ -191,7 +253,8 @@ def make_new_audio(
191
253
  for c, clip in enumerate(layer):
192
254
  if (clip.src, clip.stream) not in samples:
193
255
  audio_path = ensure.audio(clip.src, clip.stream)
194
- samples[(clip.src, clip.stream)] = read(audio_path)[1]
256
+ with open(audio_path, "rb") as file:
257
+ samples[(clip.src, clip.stream)] = read(file)[1]
195
258
 
196
259
  if arr is None:
197
260
  leng = max(round((layer[-1].start + layer[-1].dur) * sr / tb), sr // tb)
@@ -214,42 +277,10 @@ def make_new_audio(
214
277
  if samp_end > len(samp_list):
215
278
  samp_end = len(samp_list)
216
279
 
217
- filters: list[str] = []
218
-
219
- if clip.speed != 1:
220
- if clip.speed > 10_000:
221
- filters.extend([f"atempo={clip.speed}^.33333"] * 3)
222
- elif clip.speed > 100:
223
- filters.extend(
224
- [f"atempo=sqrt({clip.speed})", f"atempo=sqrt({clip.speed})"]
225
- )
226
- elif clip.speed >= 0.5:
227
- filters.append(f"atempo={clip.speed}")
228
- else:
229
- start = 0.5
230
- while start * 0.5 > clip.speed:
231
- start *= 0.5
232
- filters.append("atempo=0.5")
233
- filters.append(f"atempo={clip.speed / start}")
234
-
235
- if clip.volume != 1:
236
- filters.append(f"volume={clip.volume}")
237
-
238
- if not filters:
239
- clip_arr = samp_list[samp_start:samp_end]
280
+ if clip.speed != 1 or clip.volume != 1:
281
+ clip_arr = process_audio_clip(clip, samp_list, samp_start, samp_end, sr)
240
282
  else:
241
- af = Path(temp, f"af{af_tick}.wav")
242
- af_out = Path(temp, f"af{af_tick}_out.wav")
243
-
244
- # Windows can't replace a file that's already in use, so we have to
245
- # cycle through file names.
246
- af_tick = (af_tick + 1) % 3
247
-
248
- with open(af, "wb") as fid:
249
- write(fid, sr, samp_list[samp_start:samp_end])
250
-
251
- ffmpeg.run(["-i", f"{af}", "-af", ",".join(filters), f"{af_out}"])
252
- clip_arr = read(f"{af_out}")[1]
283
+ clip_arr = samp_list[samp_start:samp_end]
253
284
 
254
285
  # Mix numpy arrays
255
286
  start = clip.start * sr // tb
@@ -112,13 +112,15 @@ class SubtitleParser:
112
112
  self.contents = new_content
113
113
 
114
114
  def write(self, file_path: str) -> None:
115
+ codec = self.codec
115
116
  with open(file_path, "w", encoding="utf-8") as file:
116
117
  file.write(self.header)
117
118
  for c in self.contents:
118
119
  file.write(
119
- f"{c.before}{to_timecode(c.start / self.tb, self.codec)}"
120
- f"{c.middle}{to_timecode(c.end / self.tb, self.codec)}"
121
- f"{c.after}"
120
+ f"{c.before}{to_timecode(c.start / self.tb, codec)}"
121
+ + f"{c.middle}{to_timecode(c.end / self.tb, codec)}"
122
+ + c.after
123
+ + ("\n" if codec == "webvtt" else "")
122
124
  )
123
125
  file.write(self.footer)
124
126
 
@@ -339,7 +339,7 @@ def render_av(
339
339
  process2.wait()
340
340
  except (OSError, BrokenPipeError):
341
341
  bar.end()
342
- ffmpeg.run_check_errors(cmd, log, True)
342
+ ffmpeg.run_check_errors(cmd, True)
343
343
  log.error("FFmpeg Error!")
344
344
 
345
345
  log.debug(f"Total frames saved seeking: {frames_saved}")
@@ -11,7 +11,7 @@ from auto_editor.analyze import LevelError, Levels, iter_audio, iter_motion
11
11
  from auto_editor.ffwrapper import initFileInfo
12
12
  from auto_editor.lang.palet import env
13
13
  from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
14
- from auto_editor.utils.bar import Bar
14
+ from auto_editor.utils.bar import initBar
15
15
  from auto_editor.utils.cmdkw import (
16
16
  ParserError,
17
17
  Required,
@@ -83,7 +83,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
83
83
  parser = levels_options(ArgumentParser("levels"))
84
84
  args = parser.parse_args(LevelArgs, sys_args)
85
85
 
86
- bar = Bar("none")
86
+ bar = initBar("none")
87
87
  log = Log(quiet=True)
88
88
 
89
89
  sources = [initFileInfo(path, log) for path in args.input]
@@ -11,7 +11,7 @@ from auto_editor.lang.palet import ClosingError, Lexer, Parser, env, interpret
11
11
  from auto_editor.lang.stdenv import make_standard_env
12
12
  from auto_editor.lib.data_structs import print_str
13
13
  from auto_editor.lib.err import MyError
14
- from auto_editor.utils.bar import Bar
14
+ from auto_editor.utils.bar import initBar
15
15
  from auto_editor.utils.log import Log
16
16
  from auto_editor.utils.types import frame_rate
17
17
  from auto_editor.vanparse import ArgumentParser
@@ -64,9 +64,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
64
64
  sources = [initFileInfo(path, log) for path in args.input]
65
65
  src = sources[0]
66
66
  tb = src.get_fps() if args.timebase is None else args.timebase
67
- bar = Bar("modern")
68
67
  env["timebase"] = tb
69
- env["@levels"] = Levels(src, tb, bar, False, log, strict)
68
+ env["@levels"] = Levels(src, tb, initBar("modern"), False, log, strict)
70
69
 
71
70
  env.update(make_standard_env())
72
71
  print(f"Auto-Editor {auto_editor.__version__}")
@@ -186,7 +186,7 @@ def main(sys_args: list[str] | None = None):
186
186
  "wav/pcm-f32le.wav",
187
187
  "wav/pcm-s32le.wav",
188
188
  "multi-track.mov",
189
- "subtitle.mp4",
189
+ "mov_text.mp4",
190
190
  "testsrc.mkv",
191
191
  )
192
192
 
@@ -222,7 +222,8 @@ def main(sys_args: list[str] | None = None):
222
222
  run.raw(["levels", "resources/new-commentary.mp3"])
223
223
 
224
224
  def subdump():
225
- run.raw(["subdump", "resources/subtitle.mp4"])
225
+ run.raw(["subdump", "resources/mov_text.mp4"])
226
+ run.raw(["subdump", "resources/webvtt.mkv"])
226
227
 
227
228
  def desc():
228
229
  run.raw(["desc", "example.mp4"])
@@ -357,7 +358,13 @@ def main(sys_args: list[str] | None = None):
357
358
  run.main(["example.mp4"], ["--export", 'premiere:name="Foo Bar"'])
358
359
 
359
360
  def export_subtitles():
360
- cn = fileinfo(run.main(["resources/subtitle.mp4"], []))
361
+ cn = fileinfo(run.main(["resources/mov_text.mp4"], []))
362
+
363
+ assert len(cn.videos) == 1
364
+ assert len(cn.audios) == 1
365
+ assert len(cn.subtitles) == 1
366
+
367
+ cn = fileinfo(run.main(["resources/webvtt.mkv"], []))
361
368
 
362
369
  assert len(cn.videos) == 1
363
370
  assert len(cn.audios) == 1
@@ -405,7 +412,8 @@ def main(sys_args: list[str] | None = None):
405
412
  test_file = f"resources/{test_name}"
406
413
  results.add(run.main([test_file], []))
407
414
  run.main([test_file], ["--edit", "none"])
408
- results.add(run.main([test_file], ["-exf"]))
415
+ results.add(run.main([test_file], ["--export", "final-cut-pro:version=10"]))
416
+ results.add(run.main([test_file], ["--export", "final-cut-pro:version=11"]))
409
417
  results.add(run.main([test_file], ["-exs"]))
410
418
  results.add(run.main([test_file], ["--export_as_clip_sequence"]))
411
419
  run.main([test_file], ["--stats"])
auto_editor/timeline.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from auto_editor.ffwrapper import initFileInfo, mux
6
7
  from auto_editor.lib.contracts import *
7
8
  from auto_editor.utils.cmdkw import Required, pAttr, pAttrs
8
9
  from auto_editor.utils.types import color, natural, number, threshold
@@ -10,10 +11,12 @@ from auto_editor.utils.types import color, natural, number, threshold
10
11
  if TYPE_CHECKING:
11
12
  from collections.abc import Iterator
12
13
  from fractions import Fraction
14
+ from pathlib import Path
13
15
  from typing import Any
14
16
 
15
17
  from auto_editor.ffwrapper import FileInfo
16
18
  from auto_editor.utils.chunks import Chunks
19
+ from auto_editor.utils.log import Log
17
20
 
18
21
 
19
22
  @dataclass(slots=True)
@@ -241,6 +244,13 @@ video\n"""
241
244
  for a in aclips:
242
245
  yield a.src
243
246
 
247
+ def unique_sources(self) -> Iterator[FileInfo]:
248
+ seen = set()
249
+ for source in self.sources:
250
+ if source.path not in seen:
251
+ seen.add(source.path)
252
+ yield source
253
+
244
254
  def _duration(self, layer: Any) -> int:
245
255
  total_dur = 0
246
256
  for clips in layer:
@@ -276,3 +286,38 @@ video\n"""
276
286
  "v": v,
277
287
  "a": a,
278
288
  }
289
+
290
+
291
+ def make_tracks_dir(path: Path) -> Path:
292
+ from os import mkdir
293
+ from shutil import rmtree
294
+
295
+ tracks_dir = path.parent / f"{path.stem}_tracks"
296
+
297
+ try:
298
+ mkdir(tracks_dir)
299
+ except OSError:
300
+ rmtree(tracks_dir)
301
+ mkdir(tracks_dir)
302
+
303
+ return tracks_dir
304
+
305
+
306
+ def set_stream_to_0(tl: v3, log: Log) -> None:
307
+ src = tl.src
308
+ assert src is not None
309
+ fold = make_tracks_dir(src.path)
310
+ cache: dict[Path, FileInfo] = {}
311
+
312
+ def make_track(i: int, path: Path) -> FileInfo:
313
+ newtrack = fold / f"{path.stem}_{i}.wav"
314
+ if newtrack not in cache:
315
+ mux(path, output=newtrack, stream=i)
316
+ cache[newtrack] = initFileInfo(f"{newtrack}", log)
317
+ return cache[newtrack]
318
+
319
+ for alayer in tl.a:
320
+ for aobj in alayer:
321
+ if aobj.stream > 0:
322
+ aobj.src = make_track(aobj.stream, aobj.src.path)
323
+ aobj.stream = 0
auto_editor/utils/bar.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
+ from dataclasses import dataclass
4
5
  from math import floor
5
6
  from shutil import get_terminal_size
6
7
  from time import localtime, time
@@ -8,39 +9,50 @@ from time import localtime, time
8
9
  from .func import get_stdout_bytes
9
10
 
10
11
 
11
- class Bar:
12
- def __init__(self, bar_type: str) -> None:
13
- self.machine = False
14
- self.hide = False
12
+ def initBar(bar_type: str) -> Bar:
13
+ icon = "⏳"
14
+ chars: tuple[str, ...] = (" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█")
15
+ brackets = ("|", "|")
16
+ machine = hide = False
17
+
18
+ if bar_type == "classic":
19
+ icon = "⏳"
20
+ chars = ("░", "█")
21
+ brackets = ("[", "]")
22
+ if bar_type == "ascii":
23
+ icon = "& "
24
+ chars = ("-", "#")
25
+ brackets = ("[", "]")
26
+ if bar_type == "machine":
27
+ machine = True
28
+ if bar_type == "none":
29
+ hide = True
30
+
31
+ part_width = len(chars) - 1
32
+
33
+ ampm = True
34
+ if sys.platform == "darwin" and bar_type in ("modern", "classic", "ascii"):
35
+ try:
36
+ date_format = get_stdout_bytes(
37
+ ["defaults", "read", "com.apple.menuextra.clock", "Show24Hour"]
38
+ )
39
+ ampm = date_format == b"0\n"
40
+ except FileNotFoundError:
41
+ pass
15
42
 
16
- self.icon = "⏳"
17
- self.chars: tuple[str, ...] = (" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█")
18
- self.brackets = ("|", "|")
43
+ return Bar(icon, chars, brackets, machine, hide, part_width, ampm, [])
19
44
 
20
- if bar_type == "classic":
21
- self.icon = "⏳"
22
- self.chars = ("░", "█")
23
- self.brackets = ("[", "]")
24
- if bar_type == "ascii":
25
- self.icon = "& "
26
- self.chars = ("-", "#")
27
- self.brackets = ("[", "]")
28
- if bar_type == "machine":
29
- self.machine = True
30
- if bar_type == "none":
31
- self.hide = True
32
-
33
- self.part_width = len(self.chars) - 1
34
-
35
- self.ampm = True
36
- if sys.platform == "darwin" and bar_type in ("modern", "classic", "ascii"):
37
- try:
38
- date_format = get_stdout_bytes(
39
- ["defaults", "read", "com.apple.menuextra.clock", "Show24Hour"]
40
- )
41
- self.ampm = date_format == b"0\n"
42
- except FileNotFoundError:
43
- pass
45
+
46
+ @dataclass(slots=True)
47
+ class Bar:
48
+ icon: str
49
+ chars: tuple[str, ...]
50
+ brackets: tuple[str, str]
51
+ machine: bool
52
+ hide: bool
53
+ part_width: int
54
+ ampm: bool
55
+ stack: list[tuple[str, int, float, float]]
44
56
 
45
57
  @staticmethod
46
58
  def pretty_time(my_time: float, ampm: bool) -> str:
@@ -62,28 +74,25 @@ class Bar:
62
74
  if self.hide:
63
75
  return
64
76
 
65
- progress = 0.0 if self.total == 0 else min(1, max(0, index / self.total))
66
- rate = 0.0 if progress == 0 else (time() - self.begin_time) / progress
77
+ title, len_title, total, begin = self.stack[-1]
78
+ progress = 0.0 if total == 0 else min(1, max(0, index / total))
79
+ rate = 0.0 if progress == 0 else (time() - begin) / progress
67
80
 
68
81
  if self.machine:
69
- index = min(index, self.total)
70
- secs_til_eta = round(self.begin_time + rate - time(), 2)
71
- print(
72
- f"{self.title}~{index}~{self.total}~{secs_til_eta}",
73
- end="\r",
74
- flush=True,
75
- )
82
+ index = min(index, total)
83
+ secs_til_eta = round(begin + rate - time(), 2)
84
+ print(f"{title}~{index}~{total}~{secs_til_eta}", end="\r", flush=True)
76
85
  return
77
86
 
78
- new_time = self.pretty_time(self.begin_time + rate, self.ampm)
87
+ new_time = self.pretty_time(begin + rate, self.ampm)
79
88
 
80
89
  percent = round(progress * 100, 1)
81
90
  p_pad = " " * (4 - len(str(percent)))
82
91
  columns = get_terminal_size().columns
83
- bar_len = max(1, columns - (self.len_title + 32))
92
+ bar_len = max(1, columns - (len_title + 32))
84
93
  bar_str = self._bar_str(progress, bar_len)
85
94
 
86
- bar = f" {self.icon}{self.title} {bar_str} {p_pad}{percent}% ETA {new_time}"
95
+ bar = f" {self.icon}{title} {bar_str} {p_pad}{percent}% ETA {new_time}"
87
96
 
88
97
  if len(bar) > columns - 2:
89
98
  bar = bar[: columns - 2]
@@ -93,10 +102,7 @@ class Bar:
93
102
  sys.stdout.write(bar + "\r")
94
103
 
95
104
  def start(self, total: float, title: str = "Please wait") -> None:
96
- self.title = title
97
- self.len_title = len(title)
98
- self.total = total
99
- self.begin_time = time()
105
+ self.stack.append((title, len(title), total, time()))
100
106
 
101
107
  try:
102
108
  self.tick(0)
@@ -124,6 +130,7 @@ class Bar:
124
130
  )
125
131
  return line
126
132
 
127
- @staticmethod
128
- def end() -> None:
133
+ def end(self) -> None:
129
134
  sys.stdout.write(" " * (get_terminal_size().columns - 2) + "\r")
135
+ if self.stack:
136
+ self.stack.pop()
auto_editor/utils/func.py CHANGED
@@ -10,8 +10,6 @@ if TYPE_CHECKING:
10
10
 
11
11
  from numpy.typing import NDArray
12
12
 
13
- from auto_editor.utils.log import Log
14
-
15
13
  BoolList = NDArray[np.bool_]
16
14
  BoolOperand = Callable[[BoolList, BoolList], BoolList]
17
15
 
@@ -135,30 +133,6 @@ def human_readable_time(time_in_secs: float) -> str:
135
133
  return f"{time_in_secs} {units}"
136
134
 
137
135
 
138
- def open_with_system_default(path: str, log: Log) -> None:
139
- import sys
140
- from subprocess import run
141
-
142
- if sys.platform == "win32":
143
- from os import startfile
144
-
145
- try:
146
- startfile(path)
147
- except OSError:
148
- log.warning("Could not find application to open file.")
149
- else:
150
- try: # MacOS case
151
- run(["open", path])
152
- except Exception:
153
- try: # WSL2 case
154
- run(["cmd.exe", "/C", "start", path])
155
- except Exception:
156
- try: # Linux case
157
- run(["xdg-open", path])
158
- except Exception:
159
- log.warning("Could not open output file.")
160
-
161
-
162
136
  def append_filename(path: str, val: str) -> str:
163
137
  from os.path import splitext
164
138
 
@@ -224,6 +224,7 @@ class Args:
224
224
  scale: float = 1.0
225
225
  extras: str | None = None
226
226
  sn: bool = False
227
+ dn: bool = False
227
228
  no_seek: bool = False
228
229
  cut_out: list[tuple[str, str]] = field(default_factory=list)
229
230
  add_in: list[tuple[str, str]] = field(default_factory=list)