ffmpeg-pythonを使うと、各種動画生成が簡単に行えます。
このモジュールは動画処理を外部のffmpegプログラムに任せており、ffmpeg向けの命令群を構築して、ffmpeg.run
でffmpegに投げています。
ffmpeg.get_args()
でffmpegに渡す引数をダンプできます。
このうち-filter_complex
で渡しているのが、中核処理のコマンド(filter
)です。
数百種類あります。
FFmpeg Filters Documentation
これらのコマンドは、「入力、処理、結果」というシンプルな形式になっています。
引数ダンプを見てもデータの流れが分かりづらかったので、Graphvizのdot言語に変換する Pythonスクリプトを書きました。
入力と出力先を指定するシンプルなI/Fです。
filter2dot.py
#!/usr/bin/env python3
import argparse
import re
import sys
def main(argv: list[str]) -> int:
# parse argument
parser = argparse.ArgumentParser(description="")
parser.add_argument("file")
parser.add_argument("outfile")
args = parser.parse_args(argv)
src2node = {}
node2dst = {}
node2cmd = {}
with open(args.file, encoding="utf8") as f:
for line in f:
if m := re.fullmatch(
r"(.+\])([^\[\]]+)(\[.+)", line.strip().rstrip(";")
):
srctxt, cmd, dsttxt = m.group(1, 2, 3)
srcs = [t.strip("[]") for t in srctxt.split("][")]
dsts = [t.strip("[]") for t in dsttxt.split("][")]
nodename = f"{''.join(srcs)}-{''.join(dsts)}"
for src in srcs:
src0 = src.split(":")[0]
src2node[src0] = nodename
if src0.isdecimal():
node2dst[src0] = [src0]
node2cmd[src0] = src
node2dst[nodename] = dsts
node2cmd[nodename] = (
cmd.replace(":", "\\n").replace("=", "\\n", 1)
)
with open(args.outfile, "w", encoding="utf8") as f:
print("digraph {", file=f)
print(" rankdir=TB;", file=f)
print(" // node", file=f)
for node, cmd in node2cmd.items():
print(f' "{node}"[label="{cmd}"];', file=f)
print(" // edge", file=f)
for node, dsts in node2dst.items():
for dst in dsts:
if dst in src2node:
print(
f' "{node}" -> "{src2node[dst]}" [label="{dst}"];',
file=f,
)
else:
print(f' // "{node}" -> "??" [label="{dst}"]', file=f)
print("}", file=f)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
以下は入出力例です。なお長いコマンド群の頭を抜き出しているので、1つに合流しきれていません。
入力例
[0]geq=a=255:b=255:g=200:r=200[s0];
[s0]split=1[s1];
[1]geq=a=255:b=255:g=200:r=200[s2];
[s2]split=1[s3];
[s3]trim=end=305.893922[s4];
[2:v]split=1[s5];
[s5]setsar=1/1[s6];
[s6]scale=768:-1[s7];
[s7]loop=loop=-1:size=32767:start=0[s8];
[s4][s8]overlay=eof_action=pass:shortest=False:x=-268:y=302[s9];
[3:v]split=1[s10];
[s10]setsar=1/1[s11];
[s11]scale=633:-1[s12];
[s12]loop=loop=-1:size=32767:start=0[s13];
[s9][s13]overlay=eof_action=pass:shortest=False:x=1478:y=324[s14];
[4]geq=a=0:b=0:g=0:r=0[s15];
[s15]split=2[s16][s17];
[s17]split=2[s18][s19];
[5]geq=a=0:b=0:g=0:r=0[s20];
[s20]split=2[s21][s22];
[s21]trim=end=3.897333[s23];
[6]geq=a=178:b=0:g=0:r=0[s24];
[s24]split=2[s25][s26];
出力例
digraph {
rankdir=TB;
// node
"0"[label="0"];
"0-s0"[label="geq\na=255\nb=255\ng=200\nr=200"];
"s0-s1"[label="split\n1"];
"1"[label="1"];
"1-s2"[label="geq\na=255\nb=255\ng=200\nr=200"];
"s2-s3"[label="split\n1"];
"s3-s4"[label="trim\nend=305.893922"];
"2"[label="2:v"];
"2:v-s5"[label="split\n1"];
"s5-s6"[label="setsar\n1/1"];
"s6-s7"[label="scale\n768\n-1"];
"s7-s8"[label="loop\nloop=-1\nsize=32767\nstart=0"];
"s4s8-s9"[label="overlay\neof_action=pass\nshortest=False\nx=-268\ny=302"];
"3"[label="3:v"];
"3:v-s10"[label="split\n1"];
"s10-s11"[label="setsar\n1/1"];
"s11-s12"[label="scale\n633\n-1"];
"s12-s13"[label="loop\nloop=-1\nsize=32767\nstart=0"];
"s9s13-s14"[label="overlay\neof_action=pass\nshortest=False\nx=1478\ny=324"];
"4"[label="4"];
"4-s15"[label="geq\na=0\nb=0\ng=0\nr=0"];
"s15-s16s17"[label="split\n2"];
"s17-s18s19"[label="split\n2"];
"5"[label="5"];
"5-s20"[label="geq\na=0\nb=0\ng=0\nr=0"];
"s20-s21s22"[label="split\n2"];
"s21-s23"[label="trim\nend=3.897333"];
"6"[label="6"];
"6-s24"[label="geq\na=178\nb=0\ng=0\nr=0"];
"s24-s25s26"[label="split\n2"];
// edge
"0" -> "0-s0" [label="0"];
"0-s0" -> "s0-s1" [label="s0"];
// "s0-s1" -> "??" [label="s1"]
"1" -> "1-s2" [label="1"];
"1-s2" -> "s2-s3" [label="s2"];
"s2-s3" -> "s3-s4" [label="s3"];
"s3-s4" -> "s4s8-s9" [label="s4"];
"2" -> "2:v-s5" [label="2"];
"2:v-s5" -> "s5-s6" [label="s5"];
"s5-s6" -> "s6-s7" [label="s6"];
"s6-s7" -> "s7-s8" [label="s7"];
"s7-s8" -> "s4s8-s9" [label="s8"];
"s4s8-s9" -> "s9s13-s14" [label="s9"];
"3" -> "3:v-s10" [label="3"];
"3:v-s10" -> "s10-s11" [label="s10"];
"s10-s11" -> "s11-s12" [label="s11"];
"s11-s12" -> "s12-s13" [label="s12"];
"s12-s13" -> "s9s13-s14" [label="s13"];
// "s9s13-s14" -> "??" [label="s14"]
"4" -> "4-s15" [label="4"];
"4-s15" -> "s15-s16s17" [label="s15"];
// "s15-s16s17" -> "??" [label="s16"]
"s15-s16s17" -> "s17-s18s19" [label="s17"];
// "s17-s18s19" -> "??" [label="s18"]
// "s17-s18s19" -> "??" [label="s19"]
"5" -> "5-s20" [label="5"];
"5-s20" -> "s20-s21s22" [label="s20"];
"s20-s21s22" -> "s21-s23" [label="s21"];
// "s20-s21s22" -> "??" [label="s22"]
// "s21-s23" -> "??" [label="s23"]
"6" -> "6-s24" [label="6"];
"6-s24" -> "s24-s25s26" [label="s24"];
// "s24-s25s26" -> "??" [label="s25"]
// "s24-s25s26" -> "??" [label="s26"]
}