命名は慎重に…。
ちゃんとするつもりなら、もちっと安全な命名を考えた方がいいんだけど、チマっとした作業に一時的に使うインチキ道具なので、てけとー:
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 import sys
5 import subprocess
6 import os
7
8
9 if hasattr("", "decode"):
10 _encode = lambda s: s.encode(sys.getfilesystemencoding())
11 else:
12 _encode = lambda s: s
13
14
15 def parse_time(s):
16 """
17 >>> print("%.3f" % parse_time(3.2))
18 3.200
19 >>> print("%.3f" % parse_time(3))
20 3.000
21 >>> print("%.3f" % parse_time("00:00:01"))
22 1.000
23 >>> print("%.3f" % parse_time("00:00:01.3"))
24 1.300
25 >>> print("%.3f" % parse_time("00:00:01.34"))
26 1.340
27 >>> print("%.3f" % parse_time("00:00:01.034"))
28 1.034
29 >>> print("%.3f" % parse_time("00:00:01.345"))
30 1.345
31 >>> print("%.3f" % parse_time("00:01:01.345"))
32 61.345
33 >>> print("%.3f" % parse_time("02:01:01.345"))
34 7261.345
35 >>> print("%.3f" % parse_time("01:01.345"))
36 61.345
37 """
38 try:
39 return float(s)
40 except ValueError:
41 if "." in s:
42 n, _, ss = s.rpartition(".")
43 else:
44 n, ss = s, "0"
45 n = n.split(":")
46 if len(n) > 3:
47 raise ValueError("'{}' is not valid time.".format(s))
48 result = sum([
49 p * 60**(len(n) - 1 - i)
50 for i, p in enumerate(list(map(int, n)))])
51 result += int(ss) / float((10**len(ss)))
52 return result
53
54
55 if __name__ == '__main__':
56 import argparse
57 import tempfile
58 import atexit
59
60 ap = argparse.ArgumentParser()
61 ap.add_argument("video")
62 ap.add_argument("--ss", default="0")
63 ap.add_argument("--to")
64 ap.add_argument("--t", default="1")
65 ap.add_argument("--ffmpeg", default="ffmpeg")
66 args = ap.parse_args()
67 _ss = parse_time(args.ss)
68 if args.to:
69 _t = parse_time(args.to) - _ss
70 else:
71 _t = parse_time(args.t)
72 #
73 tmpout = tempfile.mktemp(suffix=".mkv")
74 atexit.register(os.remove, tmpout)
75 #
76 cmdl = [
77 args.ffmpeg,
78 "-hide_banner",
79 "-ss", "{}".format(_ss),
80 "-i", args.video,
81 "-vf", "scale=640:480,setsar=1",
82 "-af", "volume=0.25",
83 "-ss", "0",
84 "-t", "{}".format(_t),
85 tmpout
86 ]
87 subprocess.check_call(map(_encode, cmdl))
88 #
89 cmdl = [
90 "ffplay",
91 "-hide_banner",
92 tmpout
93 ]
94 subprocess.check_call(map(_encode, cmdl))
要は ffmpeg で特定範囲で千切るって作業を頻繁にやる場合に、簡単にその範囲をプレビューする術が欲しいんだけれど、「-ss」の妙な仕様のせいで、都度やろうとするとかなり鬱陶しいのよね。
一時ファイルを介さず直接 ffplay に渡すようにもできるし、渡す追加フィルターの部分の柔軟性についてはいくらでも考えられる。まぁ使いたくて色々やりたい人は頑張ってみてくれ。あと出来上がったビデオのプレビューについて昨日のにしようかとも思ったんだけれど、今の場合は atexit との兼ね合いもあるしで ffplay でいいかなと。
2020-10-16:
「version 2」なんて言い方で追記続けてると「rev103」みたいなことにもなりかねないんだけども。要するにこういう需要って、「分割位置をみたいのぢゃぁ」てことなわけで、しかも複数箇所を一気にみたいことも多くて、こんな使い方が出来たらいいなぁと:
1 [me@host: ~]$ ffchopreview.py --as_r='["00:02:19.3", "00:02:20.8", "00:02:23"]' hoge.mkv
ちぅわけで:
1 #! py -3
2 # -*- coding: utf-8 -*-
3 from __future__ import unicode_literals
4
5 import sys
6 import subprocess
7 import os
8
9
10 if hasattr("", "decode"):
11 _encode = lambda s: s.encode(sys.getfilesystemencoding())
12 else:
13 _encode = lambda s: s
14
15
16 def parse_time(s):
17 """
18 >>> print("%.3f" % parse_time(3.2))
19 3.200
20 >>> print("%.3f" % parse_time(3))
21 3.000
22 >>> print("%.3f" % parse_time("00:00:01"))
23 1.000
24 >>> print("%.3f" % parse_time("00:00:01.3"))
25 1.300
26 >>> print("%.3f" % parse_time("00:00:01.34"))
27 1.340
28 >>> print("%.3f" % parse_time("00:00:01.034"))
29 1.034
30 >>> print("%.3f" % parse_time("00:00:01.345"))
31 1.345
32 >>> print("%.3f" % parse_time("00:01:01.345"))
33 61.345
34 >>> print("%.3f" % parse_time("02:01:01.345"))
35 7261.345
36 >>> print("%.3f" % parse_time("01:01.345"))
37 61.345
38 """
39 try:
40 return float(s)
41 except ValueError:
42 if "." in s:
43 n, _, ss = s.rpartition(".")
44 else:
45 n, ss = s, "0"
46 n = n.split(":")
47 if len(n) > 3:
48 raise ValueError("'{}' is not valid time.".format(s))
49 result = sum([
50 p * 60**(len(n) - 1 - i)
51 for i, p in enumerate(list(map(int, n)))])
52 result += int(ss) / float((10**len(ss)))
53 return result
54
55
56 def _ts_to_tss(ts, frac=3):
57 d, _, f = (("%%.%df" % frac) % ts).partition(".")
58 d = abs(int(d))
59 ss_h = int(d / 3600)
60 d -= ss_h * 3600
61 ss_m = int(d / 60)
62 d -= ss_m * 60
63 ss_s = int(d)
64 return "%s%02d:%02d:%02d.%s" % (
65 "" if ts >= 0 else "-",
66 ss_h, ss_m, ss_s, f)
67
68
69 if __name__ == '__main__':
70 import argparse
71 import json
72 import tempfile
73 import atexit
74
75 ap = argparse.ArgumentParser()
76 ap.add_argument("video")
77 ap.add_argument("--ss", default="0")
78 ap.add_argument("--to")
79 ap.add_argument("--t", default="1")
80 ap.add_argument("--as_range") # as json's list
81 ap.add_argument("--ffmpeg", default="ffmpeg")
82 args = ap.parse_args()
83 def _yr():
84 if args.as_range:
85 invalid = True
86 try:
87 _aslst = json.loads(args.as_range)
88 if isinstance(_aslst, (list,)) and len(_aslst) == 1:
89 _aslst = _aslst[0]
90 if isinstance(_aslst, (list,)):
91 _aslst = list(map(parse_time, _aslst))
92 invalid = False
93 elif isinstance(_aslst, (str, float, int)):
94 t = parse_time(_aslst)
95 if t > 1:
96 _aslst = [t - 1, t, t + 1]
97 else:
98 _aslst = [t, t + 1]
99 invalid = False
100 except (json.decoder.JSONDecodeError, ValueError):
101 pass
102 if invalid:
103 ap.error("invalid ranges:\n {!r}".format(args.as_range))
104 for _ss, _to in zip(_aslst[:-1], _aslst[1:]):
105 _t = _to - _ss
106 yield _ss, _t
107 else:
108 _ss = parse_time(args.ss)
109 if args.to:
110 _t = parse_time(args.to) - _ss
111 else:
112 _t = parse_time(args.t)
113 yield _ss, _t
114 #
115 for _ss, _t in _yr():
116 tmpout = tempfile.mktemp(
117 suffix=".mkv",
118 prefix=" {} ~ {} ".format(
119 _ts_to_tss(_ss).replace(":", "_"),
120 _ts_to_tss(_t).replace(":", "_")))
121 atexit.register(os.remove, tmpout)
122 #
123 cmdl = [
124 args.ffmpeg,
125 "-hide_banner",
126 "-ss", "{}".format(_ss),
127 "-i", args.video,
128 "-vf", "scale=640:480,setsar=1",
129 "-af", "volume=0.5",
130 "-ss", "0",
131 "-t", "{}".format(_t),
132 tmpout
133 ]
134 subprocess.check_call(map(_encode, cmdl))
135 #
136 cmdl = [
137 "ffplay",
138 "-hide_banner",
139 tmpout
140 ]
141 subprocess.check_call(map(_encode, cmdl))
「一時ファイルを介さず直接 ffplay に渡すようにもできるし、渡す追加フィルターの部分の柔軟性についてはいくらでも考えられる」云々のくだりに関することは変わらず。scaleとvolumeはある程度の「大きなお世話」が(「プレビュー」という目的の性質上)必要ではあるものの、この硬直っぷりはさすがにもう一声欲しいところではあるね。せめて固定オプションを設定ファイルからも読めるようにするとか、かなぁ、そうすりゃ少し使い勝手がいいかな、たぶん。
2020-10-17:
やっぱ gist のほうがえぇかのぉ? とまれ。成果がめっぽう少ないわりにえれー苦労しちゃったもんで。やぱーし Windows でな MSYS でばめっさダルわらし、ダルはだるざすーぱーはか。ごめん、ちと壊れてる。「ともあれ、これな」:
1 #! py -3
2 # -*- coding: utf-8 -*-
3 from __future__ import unicode_literals
4
5 import io
6 import sys
7 import subprocess
8 import os
9 import re
10
11
12 if hasattr("", "decode"):
13 _encode = lambda s: s.encode(sys.getfilesystemencoding())
14 else:
15 _encode = lambda s: s
16
17
18 def parse_time(s):
19 """
20 >>> print("%.3f" % parse_time(3.2))
21 3.200
22 >>> print("%.3f" % parse_time(3))
23 3.000
24 >>> print("%.3f" % parse_time("00:00:01"))
25 1.000
26 >>> print("%.3f" % parse_time("00:00:01.3"))
27 1.300
28 >>> print("%.3f" % parse_time("00:00:01.34"))
29 1.340
30 >>> print("%.3f" % parse_time("00:00:01.034"))
31 1.034
32 >>> print("%.3f" % parse_time("00:00:01.345"))
33 1.345
34 >>> print("%.3f" % parse_time("00:01:01.345"))
35 61.345
36 >>> print("%.3f" % parse_time("02:01:01.345"))
37 7261.345
38 >>> print("%.3f" % parse_time("01:01.345"))
39 61.345
40 """
41 try:
42 return float(s)
43 except ValueError:
44 if "." in s:
45 n, _, ss = s.rpartition(".")
46 else:
47 n, ss = s, "0"
48 n = n.split(":")
49 if len(n) > 3:
50 raise ValueError("'{}' is not valid time.".format(s))
51 result = sum([
52 p * 60**(len(n) - 1 - i)
53 for i, p in enumerate(list(map(int, n)))])
54 result += int(ss) / float((10**len(ss)))
55 return result
56
57
58 def _ts_to_tss(ts, frac=3):
59 d, _, f = (("%%.%df" % frac) % ts).partition(".")
60 d = abs(int(d))
61 ss_h = int(d / 3600)
62 d -= ss_h * 3600
63 ss_m = int(d / 60)
64 d -= ss_m * 60
65 ss_s = int(d)
66 return "%s%02d:%02d:%02d.%s" % (
67 "" if ts >= 0 else "-",
68 ss_h, ss_m, ss_s, f)
69
70
71 if __name__ == '__main__':
72 import argparse
73 import json
74 import tempfile
75 import atexit
76
77 ap = argparse.ArgumentParser()
78 ap.add_argument("video")
79 ap.add_argument("--ss", default="0")
80 ap.add_argument("--to")
81 ap.add_argument("--t", default="1")
82 ap.add_argument("--as_range") # as json's list
83 ap.add_argument("--ffmpeg", default="ffmpeg")
84 args = ap.parse_args()
85
86 _mar = 1.5
87 def _yr():
88 if args.as_range:
89 invalid = True
90 try:
91 _aslst = json.loads(args.as_range)
92 if isinstance(_aslst, (list,)) and len(_aslst) == 1:
93 _aslst = _aslst[0]
94 if isinstance(_aslst, (list,)):
95 _aslst = list(map(parse_time, _aslst))
96 invalid = False
97 elif isinstance(_aslst, (str, float, int)):
98 t = parse_time(_aslst)
99 if t > _mar:
100 _aslst = [t - _mar, t, t + _mar]
101 else:
102 _aslst = [t, t + _mar]
103 invalid = False
104 except (json.decoder.JSONDecodeError, ValueError):
105 pass
106 if invalid:
107 ap.error("invalid ranges:\n {!r}".format(args.as_range))
108 for _ss, _to in zip(_aslst[:-1], _aslst[1:]):
109 _t = _to - _ss
110 if _t <= 0:
111 ap.error("invalid ranges:\n {!r}".format(
112 [_ts_to_tss(_ss), _ts_to_tss(_ss + _t)]))
113 yield _ss, _t
114 else:
115 _ss = parse_time(args.ss)
116 if args.to:
117 _t = parse_time(args.to) - _ss
118 else:
119 _t = parse_time(args.t)
120 yield _ss, _t
121 #
122 for _ss, _t in _yr():
123 _ss1_s, _to1_s = _ts_to_tss(_ss), _ts_to_tss(_ss + _t)
124 _ss2_s, _to2_s = _ts_to_tss(0), _ts_to_tss(_t)
125 tmpoutb = tempfile.mktemp()
126 tmpout, tmpoutsrt = tmpoutb + ".mkv", (tmpoutb + ".srt").replace("\\", "/")
127 atexit.register(os.remove, tmpout)
128 atexit.register(os.remove, tmpoutsrt)
129 #
130 srttxt = """\
131 1
132 {} --> {}
133 {} ~ {}
134
135 """.format(_ss2_s.replace(".", ","), _to2_s.replace(".", ","), _ss1_s, _to1_s)
136 with io.open(tmpoutsrt, "w") as fosrt:
137 fosrt.write(srttxt)
138 cmdl = [
139 args.ffmpeg,
140 "-hide_banner",
141 "-ss", "{}".format(_ss),
142 "-i", args.video,
143 "-filter_complex",
144 "[0:v]subtitles='{}',scale=640:-1,setsar=1[v];[0:a]volume=0.4[a]".format(
145 tmpoutsrt.replace(":", "\\:")),
146 "-ss", "0",
147 "-t", "{}".format(_t),
148 "-map", "[v]", "-map", "[a]",
149 tmpout
150 ]
151 subprocess.check_call(map(_encode, cmdl))
152 #
153 cmdl = [
154 "ffplay",
155 "-hide_banner",
156 tmpout
157 ]
158 subprocess.check_call(map(_encode, cmdl))
「ちぎった範囲のプレビュー」なのであるからして、「どこよ?」が視覚的にすぐにわかればよかろうべ、てなノリであるのよ。そこはまぁよかろ。ffmpeg のエラー報告がさ、相変わらずバカでバカでバカなのでバカなのよなのだわよ。ファイル自体の存在証明とファイル内容の正当性証明の区別が簡単に付かんのよ、ええかげんにせい。(何言ってるのかわからんと思うけれど、この「苦労」では最低でも3つ以上の独立した問題と格闘した。どの3つかは言わない。コードから察してくれる手もあるし、自力てみればきっと何かわかると思う。)
昨日版でペンディングにしてた部分は変わってないよ。あんまりそこはまだワタシに需要ないんでな。
それより、「-vf」「-af」が複数フィルタのチェイン出来ないことに今更気づいた。確かに普段は -filter_complex ばかり使ってるけどね、今更だ…。
2020-10-17 (2):
いいかげん gist 日和なのかもなのかしらなのよ。ベティはこんなことしたくなるのよ:
1 [me@host: ~]$ ffchopreview.py --g='{"ss": "00:04:31", "to": "00:06:20", "step": 3}'
2 ... (["00:04:31", "00:04:34"], ["00:04:34", "00:04:37"], ["00:04:37", "00:04:40"], ...)
脚本はゴミなのよ:
1 #! py -3
2 # -*- coding: utf-8 -*-
3 from __future__ import unicode_literals
4
5 import io
6 import sys
7 import subprocess
8 import os
9 import re
10
11
12 if hasattr("", "decode"):
13 _encode = lambda s: s.encode(sys.getfilesystemencoding())
14 else:
15 _encode = lambda s: s
16
17
18 def parse_time(s):
19 """
20 >>> print("%.3f" % parse_time(3.2))
21 3.200
22 >>> print("%.3f" % parse_time(3))
23 3.000
24 >>> print("%.3f" % parse_time("00:00:01"))
25 1.000
26 >>> print("%.3f" % parse_time("00:00:01.3"))
27 1.300
28 >>> print("%.3f" % parse_time("00:00:01.34"))
29 1.340
30 >>> print("%.3f" % parse_time("00:00:01.034"))
31 1.034
32 >>> print("%.3f" % parse_time("00:00:01.345"))
33 1.345
34 >>> print("%.3f" % parse_time("00:01:01.345"))
35 61.345
36 >>> print("%.3f" % parse_time("02:01:01.345"))
37 7261.345
38 >>> print("%.3f" % parse_time("01:01.345"))
39 61.345
40 """
41 try:
42 return float(s)
43 except ValueError:
44 if "." in s:
45 n, _, ss = s.rpartition(".")
46 else:
47 n, ss = s, "0"
48 n = n.split(":")
49 if len(n) > 3:
50 raise ValueError("'{}' is not valid time.".format(s))
51 result = sum([
52 p * 60**(len(n) - 1 - i)
53 for i, p in enumerate(list(map(int, n)))])
54 result += int(ss) / float((10**len(ss)))
55 return result
56
57
58 def _ts_to_tss(ts, frac=3):
59 d, _, f = (("%%.%df" % frac) % ts).partition(".")
60 d = abs(int(d))
61 ss_h = int(d / 3600)
62 d -= ss_h * 3600
63 ss_m = int(d / 60)
64 d -= ss_m * 60
65 ss_s = int(d)
66 return "%s%02d:%02d:%02d.%s" % (
67 "" if ts >= 0 else "-",
68 ss_h, ss_m, ss_s, f)
69
70
71 def _yr(ap, args):
72 _mar = 1.5
73 if args.gen_ranges:
74 try:
75 _sl = json.loads(args.gen_ranges)
76 _times = 5
77 _t = 2.0
78 if isinstance(_sl, (dict,)):
79 _ss, _t = _sl.get("ss", 0.0), _sl.get("step", _t)
80 _ss = parse_time(_ss)
81 _to = _sl.get("to", _ss + _t * _times)
82 elif not isinstance(_sl, (list,)) or not len(_sl):
83 raise ValueError("--gen_ranges: invalid")
84 elif len(_sl) == 1:
85 _ss = parse_time(_sl[0])
86 _to = _ss + _t * _times
87 elif len(_sl) == 2:
88 _ss, _to = _sl
89 elif len(_sl) >= 2:
90 _ss, _to, _t = _sl[:3]
91 _ss, _to = parse_time(_ss), parse_time(_to)
92 except Exception:
93 ap.error("cannot generate ranges:\n {!r}".format(args.gen_ranges))
94 if _ss >= _to or _t <= 0:
95 ap.error("cannot generate ranges:\n {!r}".format(args.gen_ranges))
96 _t = float(_t)
97 while _ss < _to:
98 yield _ss, _t
99 _ss += _t
100 elif args.as_range:
101 invalid = True
102 try:
103 _aslst = json.loads(args.as_range)
104 if isinstance(_aslst, (list,)) and len(_aslst) == 1:
105 _aslst = _aslst[0]
106 if isinstance(_aslst, (list,)):
107 _aslst = list(map(parse_time, _aslst))
108 invalid = False
109 elif isinstance(_aslst, (str, float, int)):
110 t = parse_time(_aslst)
111 if t > _mar:
112 _aslst = [t - _mar, t, t + _mar]
113 else:
114 _aslst = [t, t + _mar]
115 invalid = False
116 except (json.decoder.JSONDecodeError, ValueError):
117 pass
118 if invalid:
119 ap.error("invalid ranges:\n {!r}".format(args.as_range))
120 for _ss, _to in zip(_aslst[:-1], _aslst[1:]):
121 _t = _to - _ss
122 if _t <= 0:
123 ap.error("invalid ranges:\n {!r}".format(
124 [_ts_to_tss(_ss), _ts_to_tss(_ss + _t)]))
125 yield _ss, _t
126 else:
127 _ss = parse_time(args.ss)
128 if args.to:
129 _t = parse_time(args.to) - _ss
130 else:
131 _t = parse_time(args.t)
132 yield _ss, _t
133
134
135 if __name__ == '__main__':
136 import argparse
137 import json
138 import tempfile
139 import atexit
140
141 ap = argparse.ArgumentParser()
142 ap.add_argument("video")
143 ap.add_argument("--ss", default="0")
144 ap.add_argument("--to")
145 ap.add_argument("--t", default="1")
146 ap.add_argument("--as_range") # as json's list
147 ap.add_argument("--gen_ranges") # [start, end, step]
148 ap.add_argument("--ffmpeg", default="ffmpeg")
149 args = ap.parse_args()
150
151 #
152 for _ss, _t in _yr(ap, args):
153 _ss1_s, _to1_s = _ts_to_tss(_ss), _ts_to_tss(_ss + _t)
154 _ss2_s, _to2_s = _ts_to_tss(0), _ts_to_tss(_t)
155 tmpoutb = tempfile.mktemp()
156 tmpout, tmpoutsrt = tmpoutb + ".mkv", (tmpoutb + ".srt").replace("\\", "/")
157 atexit.register(os.remove, tmpout)
158 atexit.register(os.remove, tmpoutsrt)
159 #
160 srttxt = """\
161 1
162 {} --> {}
163 {} <font color=0x7F7FFF3F>~</font> {}
164
165 """.format(
166 _ss2_s.replace(".", ","),
167 _to2_s.replace(".", ","),
168 re.sub(r"([\d:]+\.)(\d+)", r"<font size=30>\1</font><font size=20>\2</font>", _ss1_s),
169 re.sub(r"([\d:]+\.)(\d+)", r"<font size=30>\1</font><font size=20>\2</font>", _to1_s))
170 with io.open(tmpoutsrt, "w") as fosrt:
171 fosrt.write(srttxt)
172 cmdl = [
173 args.ffmpeg,
174 "-hide_banner",
175 "-ss", "{}".format(_ss),
176 "-i", args.video,
177 "-filter_complex",
178 "[0:v]subtitles='{}',scale=640:-1,setsar=1[v];[0:a]volume=0.4[a]".format(
179 tmpoutsrt.replace(":", "\\:")),
180 "-ss", "0",
181 "-t", "{}".format(_t),
182 "-map", "[v]", "-map", "[a]",
183 tmpout
184 ]
185 subprocess.check_call(map(_encode, cmdl))
186 #
187 cmdl = [
188 "ffplay",
189 "-hide_banner",
190 tmpout
191 ]
192 subprocess.check_call(map(_encode, cmdl))
なんか今回の拡張でいつもより多めに汚れたのかしら。
雑にいい加減に作ったもんだけど、想像してたよりずっと使いやすくて、お気に入りの道具になりそうだ。あまりにもこれを育てるようならやはり gist に持ってくだろう。そんときゃここに貼り付けると思われ。楽しまずに待っとけ。
2020-10-18:
一つ前からの変更点は些末だが、さすがに gist た:
にしても、一つ前の際に書こうか迷って書かなかったことなんだけれど、これさ、「二律背反的な指定オプション」問題にハマっちゃってるのよね。まぁわりかし良く起こることだとはいえ、「雑なプロジェクト」でないならもっと頭を痛くしてるとこだ。つまり「オプション A を使う場合は同時に B を使うことは論理的に無意味」みたいなものってさ、あまりよろしくないわけよ、わかりにくいし、場合によっては非常に使いにくいこともある。今の場合は「–ssと–to、–as_range、–gen_ranges」はこれらは同時に指定するのは意味がなく、プログラム的には単に優先順位をつけて参照してるだけ、つまりこの優先順位という「隠れ仕様」がね、製品品質のものなら「ドキュメントすべし」なものであるし、あるいは「そういうわかりにくいデザインをするなかれ」てことね。こやつ、結構自分で気に入っちゃったもんで、余計に気になっちゃってな。どうにかすべきならどうしてくれようか? わからん、いまのとこ。
2020-10-20:
やはりというかなんというか。排他的なオプションというのは、プログラムの保守にも悪影響がありえて、かなり鬱陶しくなってきて、さっき「–ss、–to、–t」を消した。100%完全互換とはいかないが、–as_range が 97.23975411165513% くらい互換になりうる、絶対にだ。
「ビデオから単一範囲を切り取る」のためだけの「–ss、–to、–t」だったわけだが、「ビデオから複数範囲を切る取る」のための「–as_range」、代替できないはずがないわけである。ただ、「終了時刻は知らん、duration だけ意識したいんぢゃぁ(あるいは to だけ意識したい)」の向きには「–as_range」は劣っていて、これが 2.760245888344869% ね。
いつも思うんだが、こういう UI 設計の「あるあるネタ」って、どっかにまとまったものがあったりしないだろうか? いわゆるソフトウェア構造設計のデザインパターン・アンチパターンみたいに、真面目にやればちゃんとした工学だと思うんで、ちゃんと書いたちゃんとした本とかサイト、ないかなぁ?
2020-10-19:
gist 内容は勝手に本体 gist に追従しちゃうんで、昨日追記の文章は辻褄合わなくなっておるが、まぁわかるよね、「変更は些末」、ではない、もはや。動かしてみればわかることだし、まだ誰が読んでも最長10分もあれば全貌理解できるはずの分量のスクリプトだとは思うんで、今日あえて「ほにゃららしましたー」って言うのも大げさではある。インチキ GUI を追加した、てことである。
「インチキ GUI を追加した」ことで、実は「ちゃんとした動画編集アプリ」の道筋が出来上がってしまっていることについては、まぁそのうち考える。今すぐにどうこうしようとは思ってない。
で。なにゆえブログの編集画面を開いて今追記してるかというと、「GUIりました」の報告よりは、そもそも「使い方の想定」について、なんにも説明してないなぁ、と思ったもんでね。ちょっとだけ。
ちゃんとした、というか、本格的な動画編集ソフトがあればこんなもんいらんわい、というノリでここまで言ってきたけれど、実はそうした重量級アプリケーションよりもワタシのコレの方が優れる局面が少なからずある。
たとえば、「機械的に区切り位置を見つけることが出来ない結構長いビデオについて、目視で区切り位置を探したい」とする。こうした作業を、一般的な動画編集ソフトの UI でやろうとすると、「再生しっぱなし、凝視し続けながら」か、「テキトーにシークバーをごにょごにょ動かしてどうにか探す」しかない。「機械的にはわからない」となれば人の目を使うしかないわけだが、このタイプの作業方法を取った場合の問題は、「集中力が続かない」ことだったりする。つまり(動画の切れ目が見つからないのと同時に)「作業の切れ目も見つからない」ということが起こるのだ。
ワタシのこのスクリプトは、「インチキかつ雑で単純(でおバカ)」であるがゆえに、「作業の切れ目」を作りやすい形で作業することが出来る:
1 [me@host: ~]$ ffchopreview.py --gen='["00:01:00", "00:23:49", 30]' myvideo.mp4
2 ... "00:01:00" から "00:23:49" までの間を 30 秒ずつ区切る ...
これであれば、「区切り位置探し」の作業を「30秒ずつ」区切って行うことが出来る。かなり些細なことなのだが、直面している問題(作業)の性質によっては、精神疲労の量がかなり違ってくる可能性がある。そもそも「30分ずっと凝視しっぱなし」は、作業開始してからよりも始める前の方が精神的ダメージが大きい、かもしれない。
2020-10-21:
なんの責任もないインチキプロジェクトだから許してね、って前置きから始まる異世界生活。
スマソ。オプションの命名もテキトーにやってたもんで、変更したくなった。「–gen」だの「–as_range」だので説明してるけど、「–from_splitpoints」「–from_slice」に変えた。あと gui がかなり自分的に満足できるものになれた。なんか作業のテンポが格段に上がるのよね。「ffchopreview.py起動→確認→気に食わない→ffchopreview.py起動しなおし→…」のサイクルだったのが、起動して動かしっぱなしに出来るようになったんで。ストレスがかなり減る。こいつぁいいや。
ま…、実際に動かしてみた人にしかこの感じってわからんと思うのよね。なんせ見た目が地味な上に「ダサ」くて、初見で使いやすいに違いないと「思うはずがない」見た目なのですもの。
あ、そうそう。言い忘れてたけど Python 2.x では正しく動作してないです。見かけ上あたかも 2.x 対応してそうだけどしてない。
一応このスクリプトはもう少しだけ拡張する。今 Rev34 時点で gui が「from_sliceし直し」にまで対応できてるけど、from_splitpoints の方もやりたいわけよね。前者が「固定幅での区切り」、後者が「任意幅での区切り」に対応するんで、どっちも個人的にないがしろにしたくないんで。
で、問題はその先。ちょっと上で言ったようにこれ、実際は「本格的な動画編集ソフト事始め」になってたりするんよね。すぐに出来ることとしては、またしてもMako利用して「色んなもの吐き出し機」にしちゃえる。ffmpeg を使うシェルスクリプトを生成するとか、あるいは何か別のツールのための設定ファイルを吐き出す道具に仕立て上げてみたりとか。あるいは「サムネイル抽出道具ぅ」なんか即効で作れたりもする。ただその場合もはや「chopreview」ではないんだよね。「tiny_videoedit」とかね、結構大げさな名前を付けてもバチがあたらんもんになる。どーすっかなぁ? 「軽量動画編集」ってどんだけ需要あるかね? (というかちゃんと探せば結構見つかると思うしねぇ?)
2020-10-21続き:
文章だけだと伝わらないと思うので、現状の Rev41 時点を使ってみてるの図:
当然ながらデスクトップを録画した録画ファイルから Youtube にアップロードするためのビデオにするための「必要部分切り取り」にこの ffchopreview.py 自身を使った。というかまさにこの作業のために欲しくなった道具なんだけどね。
2020-10-25:
文章だけだと伝わらないと思うので、現状の Rev77 時点を使ってみてるの図:
かなり雑なままガシガシ拡張したので、スクリプトはかなりムゴいことになってる、けど、まぁワタシ的日常使いモノとして、わりかしええもんかなと。無論なにも知識のない人がこれをみて「すげー」と思うようなシロモノではなくて、「なんだよ、こいつで直接動画編集できねーんでやんの」と言われる類のもんではありまするが。そういった「ちゃんとした完成品」もすぐに目指せるところにはいるけれど、これはこのままにしときたい。テンプレートエンジンを組み込んでコンソール出力をカスタマイズ出来るようにしたことで、少なくとも「ffmpeg を使うシェルスクリプトを生成出来るぜ」というものにはなってて、「ワタシは当面これだけでも存分にありがたい」ので。
2020-10-31:
上の動画で「input has no video stream」をやってみせてるけれど、Rev95 までのものはあんまし使い物にならんかった、すまん。映像をひとコマ取り出す、ってシンプルなタスクと違って、これは「音声の統計(など)処理のサマライズを可視化…、した映像をひとコマ取り出す」わけなので、つまりは「切り取る前」が結構大事なわけよ。ゼロ秒音声の統計可視化、ではなくて、「ある程度時間の音声を観測してそれを可視化した結果」てことね。
ちょっとスナップショットをとるのにややコストがかかるようにはなったけど、Rev96の改造で、ワタシが期待するものになった。何秒余計に観測するかは微妙なんだけど、showcqt のスクロールを基準に3秒くらいがいいかと思う。欲しい時刻の3秒前からの音声可視化を静止画にする、てことよ。一応 audio_visualize_duration_if_no_input_vstream でコントロール出来るようにしといたが、あまり大きくしちゃうとサムネイル取得のたんびに結構時間かかっちゃうのでほどほどに。とはいえ「ahistogram」で可視化したい場合は、多少大きめの方が便利かとは思う。
じっくり考えればわかるんだけれど、これ、「どの範囲の音声を可視化したか」によって変わるわけね、なので「trim前の全体を可視化してから切り取る」のと、この道具での切り取り部分の絵は違う。そりゃそうだよね、要は「履歴の可視化」みたいなもんだから。trim で切り取っちゃったらその「履歴がない」ことになるわけ。
なお、この機能は元は「ビデオストリームのない音声メディア(MP3とかWAV)」を想定して作り始めたものなんだけれど、実際映像+音声の普通のビデオで使っても大いに役に立つのよ。ワタシの「video_split_by_blank.py」が「いまいち上手に区切り位置を見つけられない」ってときに補助的に使いたいんであれば、audio_visualize_if_no_input_vstream を使って「音の切れ目」を探すのに使えなくはない。会話の切れ目とかね、たとえば。