Pythonでタイムラインデータを動的に出力し、無料版DavinciResolveで読み込むまでの手法についてまとめます。
この手法で字幕付き読み上げ動画を作成した際の詳細については、以下の記事でまとめています。
目的と解決手段
目的
Pythonなどのプログラムを用いてDavinciResolveのタイムラインを動的に出力する
本記事では、上記を可能にすることを目的としています。DavinciResolveの有料版であればPythonとの連携で実施できる内容ですが、今回は無料版でのシンプルな実現を目指しました。
解決手段
XMLと.srtファイルをPythonで書き出し、DavinciResolveでインポートする
タイムラインと字幕データを記録できるテキストファイルをPythonで生成し、DavinciResolveでインポートすることで動的なタイムライン生成を実現しました。タイムラインデータとしてはXMLファイル(Final Cut Pro 7形式)を、字幕データとしては.srtファイルを利用します。
PC環境
OS:MacOS(arm64)
CPU:Apple M3
メモリ:16GB
実行環境
VSCode上でJupyterNotebookを操作
Pythonバージョン:3.9.6
DavinciResolveバージョン:20.0
目次
XMLファイル(Final Cut Pro 7形式)の仕様
Final Cut Pro 7形式とは、動画編集ソフトFinal Cut Proから、他の動画編集ソフトへタイムラインデータをエクスポートするためのXML形式です。画像や音声・動画ファイルのタイムラインへの配置データや、各種フィルタ設定どが記録できます。
本来は動画編集ソフト同士のタイムラインデータの相互エクスポートに用いられるファイルですが、実態としてはただのXMLファイルであるためPythonなどでプログラムから生成することができます。
もちろん、形式が崩れているとインポートがうまくいかないため、Final Cut Pro 7形式の仕様を理解しながらコードを書かなければなりません。本章では、DavinciResolveで読み込むことができるXMLファイルの最小構成についてまとめます。
XMLファイルの例
極端な話ですが、DavinciResolveでエクスポートしたXMLファイルであればそのままDavinciResolveでインポートすることもできます。しかし、エクスポートされたファイルを見てみると余分な設定も多く含まれており、全てをPythonで出力するのは少々冗長です。
そこで、DavinciResolveで読み込むことができる最小構成を整理しました。以下が、最小構成に近い形でまとめたXMLファイルのサンプルになります。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xmeml>
<xmeml version="5">
<sequence>
<name>Timeline 1</name>
<rate>
<timebase>30</timebase>
</rate>
<media>
<video>
<track>
<clipitem>
<start>0</start>
<end>782</end>
<in>0</in>
<out>782</out>
<file id="background1 media">
<pathurl>file://[ファイルのパス.png]</pathurl>
</file>
</clipitem>
<clipitem>
<start>782</start>
<end>1541</end>
<in>0</in>
<out>757</out>
<file id="background1 media"/>
</clipitem>
</track>
<track>
<clipitem id="movie1">
<start>782</start>
<end>1539</end>
<in>23</in>
<out>780</out>
<file id="movie1 media">
<pathurl>file://[ファイルのパス.mp4]</pathurl>
<timecode>
<string>00:00:00:00</string>
</timecode>
</file>
</clipitem>
</track>
<format>
<samplecharacteristics>
<width>1920</width>
<height>1080</height>
<pixelaspectratio>square</pixelaspectratio>
</samplecharacteristics>
</format>
</video>
<audio>
<track>
<clipitem>
<start>0</start>
<end>771</end>
<in>0</in>
<out>771</out>
<file id="audio1.wav media">
<pathurl>file://file://[ファイルのパス.wav]</pathurl>
</file>
<sourcetrack>
<mediatype>audio</mediatype>
<trackindex>1</trackindex>
</sourcetrack>
</clipitem>
<clipitem>
<start>782</start>
<end>1539</end>
<in>23</in>
<out>780</out>
<file id="arknightsSample.mp4 media"/>
<link>
<linkclipref>movie1</linkclipref>
<mediatype>video</mediatype>
</link>
<comments/>
</clipitem>
</track>
</audio>
</media>
</sequence>
</xmeml>
以下で、私が整理できている範囲でのタグの情報を捕捉しています。
タグ | 内容 |
---|---|
name | sequence直下で、タイムラインの名称を登録 |
rate | フレームレートの指定 <timebase>タグでfpsを指定する |
video | 画像・動画の配置領域 |
track | レイヤー |
clipitem | タイムライン上のオブジェクト |
start | オブジェクトの配置フレーム(開始端) |
end | オブジェクトの配置フレーム(終了端) |
in | オブジェクトの再生開始フレーム |
out | オブジェクトの再生終了フレーム |
file | オブジェクトとして読み込むファイル idを指定することで二度目以降に再利用可能 |
pathurl | 読み込むファイルのパス |
timecode | 動画ファイルの場合はfile直下で指定が必須 動画ファイルの開始時刻を登録する 基本は00:00:00:00 |
link | オブジェクト同士を同期させるためのデータ 動画の映像と音声をリンクさせる時などに使用 |
linkclipref | リンク先のオブジェクトのclipitem.id |
sourcetrack | 音声ファイルの場合はclipitem直下で指定が必須 内容は謎だがそのまま書けばOK |
samplecharacteristics | タイムラインの画角サイズを指定する |
タイムラインと動画・音声ファイルのフレームレートが異なる場合や、タイムラインの表示領域と画像/動画のサイズが異なる場合などは、さらに追加のタグ設定が必要になるかもしれません。フェードイン・フェードアウトなどの設定方法についても現状では未検証です。
fileオブジェクトの再利用
fileオブジェクトについては、同じファイルについて二度以上タイムラインに表示する場合は、初回で設定したidを付与することでパスの指定を省略できます。idの文字列については特に指定はなく自由であるように見えていますが、自分の場合は「ファイル名 media」とすると上手く動作しました。
<file id="movie1 media"/>
in-outの指定
<in>・<out>についてですが、特に調整もなく動画や音声ファイルを頭から再生したい場合は以下のように設定すれば問題ありません。inを0、outをend-startで計算します。
<in>0</in>
<out>オブジェクトの長さ</out>
動画や音声の最初数秒を切り取りたい場合などは、切り取りたい分だけズラす必要があります。
timecodeの必要性
timecodeについてですが、普通の動画であれば動画の始まりは00:00であるため、一般的な動画素材のみを扱う場合は不要かもしれません。カット編集などの結果生まれる再生開始時間が00:00ではない動画については、正確な再生開始時間を指定する必要があります。
動画の映像/音声のリンク
動画については、映像と音声をそれぞれ別のオブジェクトとして読み込む必要があります。start-end, in-outなどの内容は基本的に同じ内容で問題なく、fileについても省略形でOKです。
さらに映像と音声をタイムライン上で結びつけておく設定にしたい場合は、<link>タグの記述が必要です。映像側の<clipitem>タグでidを設定した上で、<linkclipref>を記述します。
XMLを出力するためのPythonコード
XMLを出力するためのPythonコードのサンプルを紹介します。
以下のようなタイムラインデータを与えることで、XMLを出力できます。
timelineData = [
{
"fileType": "movie",
"filepath": "[ファイルの絶対パス.mp4]",
"start_end": [0, 2638],
"in_out": [0, 2638],
"timecode": "00:37:27:09",
"track": 0
},
{
"fileType": "img",
"filepath": "[ファイルの絶対パス.png]",
"start_end": [2638, 3410],
"in_out": [0, None],
"track": 1
},
{
"fileType": "audio",
"filepath": "[ファイルの絶対パス.wav]",
"start_end": [2638, None],
"in_out": [0, None],
"track": 0
}
]
オブジェクトの詳細は以下のとおりです。同一レイヤの素材については基本的に時系列順で並べておく必要があります。
キー | 内容 | 備考 |
---|---|---|
fileType | ファイルの形式 | "movie" or "img" or "audio" |
filepath | ファイルの絶対パス | 「file://」は不要 |
start_end | オブジェクトの配置フレーム | movie,audioについてはend=Noneも可 →素材の長さから自動でendを計算 |
in_out | オブジェクトの再生フレーム | out=Noneも可 →out-in+startで自動で計算 |
track | 配置レイヤ | 0~指定 |
timecode | 動画ファイルの開始時刻 | 00:00から始まらない場合は指定必須 |
上記のオブジェクトに対して、以下のコードを実行することでXMLファイルが出力されます。ChatGPTと二人三脚で作成したコードなので、もしかしたら不可解な点もあるかもしれません。参考程度にしていただければ幸いです。
import os
from collections import defaultdict
from moviepy.editor import VideoFileClip, AudioFileClip
output_dir = "output"
fps = 30
file_registry = set()
clip_id_counter = {"movie": 1}
# === XMLヘッダー構造 ===
header = '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xmeml>
<xmeml version="5">
<sequence>
<name>Timeline 1 (Resolve)</name>
<rate>
<timebase>30</timebase>
</rate>
<media>
'''
footer_video = '''
<format>
<samplecharacteristics>
<width>1920</width>
<height>1080</height>
<pixelaspectratio>square</pixelaspectratio>
</samplecharacteristics>
</format>
'''
footer_end = ''' </media>
</sequence>
</xmeml>
'''
# === ユーティリティ関数 ===
def get_media_duration_frames(path, fps=30):
try:
ext = os.path.splitext(path)[1].lower()
if ext in [".mp4", ".mov", ".avi", ".mkv"]:
with VideoFileClip(path) as clip:
return int(clip.duration * fps)
elif ext in [".wav", ".mp3", ".aac"]:
with AudioFileClip(path) as clip:
return int(clip.duration * fps)
except Exception as e:
print(f"Erroe:{path} の長さ取得に失敗: {e}")
return 0
def build_timecode_string(frames, fps=30):
total_sec = frames // fps
h = total_sec // 3600
m = (total_sec % 3600) // 60
s = total_sec % 60
f = frames % fps
return f"{h:02}:{m:02}:{s:02}:{f:02}"
def build_file_tag(filepath, filetype, duration_frames, timecode=None):
filename = os.path.basename(filepath)
tag_id = f"{filename} media"
pathurl = f"file://{filepath}"
if tag_id in file_registry:
return f'<file id="{tag_id}"/>'
else:
file_registry.add(tag_id)
if filetype == "movie":
tc_string = timecode if timecode else "00:00:00:00"
return f'''<file id="{tag_id}">
<pathurl>{pathurl}</pathurl>
<timecode>
<string>{tc_string}</string>
</timecode>
</file>'''
else:
return f'''<file id="{tag_id}">
<pathurl>{pathurl}</pathurl>
</file>'''
# === トラックごとのclipitem ===
video_tracks = defaultdict(list)
audio_tracks = defaultdict(list)
# === 変換開始 ===
for item in timelineData:
filetype = item["fileType"]
filepath = os.path.abspath(item["filepath"])
filename = os.path.basename(filepath)
tag_id = f"{filename} media"
track_num = item.get("track", 0)
start, end = item["start_end"]
in_frame, out_frame = item["in_out"]
# duration補完
if end is None:
duration_frames = get_media_duration_frames(filepath)
end = start + duration_frames
item["start_end"][1] = end
else:
duration_frames = end - start
if out_frame is None:
out_frame = end - start + in_frame
item["in_out"][1] = out_frame
# ファイルタグ作成
file_tag = build_file_tag(filepath, filetype, duration_frames, timecode=item.get("timecode", None))
# 映像 clipitem
if filetype == "movie":
movie_id = f"movie{clip_id_counter['movie']}"
clip_id_counter["movie"] += 1
video_clip = f''' <clipitem id="{movie_id}">
<start>{start}</start>
<end>{end}</end>
<in>{in_frame}</in>
<out>{out_frame}</out>
{file_tag}
</clipitem>\n'''
video_tracks[track_num].append(video_clip)
# 動画の音声も同時に追加(trackindex=1)
audio_clip = f''' <clipitem>
<start>{start}</start>
<end>{end}</end>
<in>{in_frame}</in>
<out>{out_frame}</out>
<file id="{tag_id}"/>
<link>
<linkclipref>{movie_id}</linkclipref>
<mediatype>video</mediatype>
</link>
<comments/>
<sourcetrack>
<mediatype>audio</mediatype>
<trackindex>1</trackindex>
</sourcetrack>
</clipitem>\n'''
audio_tracks[0].append(audio_clip)
# 画像 clipitem
elif filetype == "img":
clip = f''' <clipitem>
<start>{start}</start>
<end>{end}</end>
<in>{in_frame}</in>
<out>{out_frame}</out>
{file_tag}
</clipitem>\n'''
video_tracks[track_num].append(clip)
# 単独音声ファイル
elif filetype == "audio":
clip = f''' <clipitem>
<start>{start}</start>
<end>{end}</end>
<in>{in_frame}</in>
<out>{out_frame}</out>
{file_tag}
<sourcetrack>
<mediatype>audio</mediatype>
<trackindex>1</trackindex>
</sourcetrack>
</clipitem>\n'''
audio_tracks[track_num].append(clip)
# === XML出力構築 ===
xml_content = header
# Video
xml_content += " <video>\n"
for t in sorted(video_tracks):
xml_content += " <track>\n"
xml_content += "".join(video_tracks[t])
xml_content += " </track>\n"
xml_content += footer_video
xml_content += " </video>\n"
# Audio
xml_content += " <audio>\n"
for t in sorted(audio_tracks):
xml_content += " <track>\n"
xml_content += "".join(audio_tracks[t])
xml_content += " </track>\n"
xml_content += " </audio>\n"
xml_content += footer_end
# === 書き出し ===
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, "autogenerated_timeline.xml")
with open(output_path, "w", encoding="utf-8") as f:
f.write(xml_content)
print(f"Final Cut Pro XML を出力しました: {output_path}")
動画ファイルを読み込んだ際には、同時に音声データもタイムラインに配置する仕様になっています。音声データの配置レイヤは一番上のレイヤで固定です。
.srtファイルの仕様
.srtファイルとは、動画編集ソフトで一般的に使用できる字幕データです。字幕の内容と配置時刻、さらに少しばかりの文字装飾に対応しています。
こちらはXMLファイルとは異なり非常にシンプルな構成です。以下のサンプルからわかる通り、字幕の表示時刻と字幕テキスト・インデックスで構成されます。
1
00:00:00,000 --> 00:00:26,133
【待合室 RS-ST-1 幕間】
我らが主とし、付き従うは偉大なるイェラガンド。
雲はその羽であり、風はその翼である。
主は我らに陽光と慈雨を与え、また血肉と毛皮を与えてくださる。
我らが主とし、敬愛するは仁愛溢るるイェラガンド。
山々はその骨であり、百川はその尾である。
我らは主の背を歩み、その懐で穏やかに眠る。
2
00:00:26,133 --> 00:00:51,833
【待合室 RS-ST-1 幕間】
我らが主とし、賛美するは慈悲深きイェラガンド。
主は、我らが悩み苦しむ時には優しく慰めてくださる。
主は、我らが受難する時には力を尽くしてお救いくださる。
イェラガンドは敬虔なる民を、そして山石と百獣を守り、災いから遠ざけ、永久
の静かなる安寧を約束してくださるのだ。
――『イェラガンド』
以下のようなコードで、字幕を生成することができます。
outlineData = [
{
"start_end": [0, 2638],
"text": "今回はPythonでDavinciResolveの字幕を動的に生成していきます"
},
{
"start_end": [2639, 5120],
"text": "まず用意するのはパソコンとマウスです"
},
]
import os
from pydub import AudioSegment
output_dir = "output"
fps = 30 # フレームレート
srt_lines = []
def sec_to_srt_time(sec):
hours = int(sec // 3600)
minutes = int((sec % 3600) // 60)
seconds = int(sec % 60)
milliseconds = int((sec - int(sec)) * 1000)
return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
for idx, item in enumerate(outlineData):
# item["frameRange"] = [500*idx, 500*idx+499] # 字幕表示テスト用
start_time, end_time = map(lambda f: f/fps, item["start_end"])
start_str = sec_to_srt_time(start_time)
end_str = sec_to_srt_time(end_time)
srt_lines.append(f"{idx}")
srt_lines.append(f"{start_str} --> {end_str}")
srt_lines.append(f"{item['text']}")
srt_lines.append("") # 空行
srt_output_path = os.path.join(output_dir, "autogenerated_subtitles.srt")
with open(srt_output_path, "w", encoding="utf-8") as f:
f.write("\n".join(srt_lines))
print(f"SRT字幕ファイルを出力しました: {srt_output_path}")
文字装飾
また、.srtファイルは以下のようなタグで簡易な装飾を施すことができます。
タグ | 内容 |
---|---|
<b></b> | 太字 |
<i></i> | イタリック |
<u></u> | 下線 |
その他にも.srtファイルがサポートしているタグは色々あるようですが、DavinciResolveで読み込んだ時に反映されないものも多いようです。
DavinciResolveにインポート
作成したXMLと.srtをDavinciResolveにインポートしていきます。インポート手順は以下の通りです。
1. 新規プロジェクトの作成
2. タイムラインのインポート
上部のタスクバーから、ファイル>読み込み>タイムライン>autogenerated_timeline.xmlを選択>OK
3. 字幕のインポート
同じくタスクバーから、ファイル>読み込み>字幕>autogenerated_subtitles.srtを選択
4. 字幕の配置
エディットに移動して、メディア欄のautogenerated_subtitlesをタイムラインにドラッグ&ドロップ
5. 字幕の位置調整
.srtファイルには字幕の位置データが含まれていないため、手動で調整が必要
6. 字幕を動画に焼き付ける設定に変更
字幕を映像として出力したい場合は、デリバーに移動して設定を変更する
以上の手順で、Pythonから生成した字幕とタイムラインをDavinciResolveに適用することができます。
参考文献
-
.srtファイルの仕様
https://docs.fileformat.com/ja/video/srt/ -
Arknights Story Text Reader(字幕サンプル)
https://050644zf.github.io/ArknightsStoryTextReader/#/ja_JP/event/act30side