0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonでDavinciResolveのタイムラインを自動生成する【無料版・XML】

Last updated at Posted at 2025-07-13

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ファイルのサンプルになります。

timeline.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・outの設定
    <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を出力できます。

タイムラインデータのサンプル.py
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と二人三脚で作成したコードなので、もしかしたら不可解な点もあるかもしれません。参考程度にしていただければ幸いです。

XMLの出力コード.py
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ファイルとは異なり非常にシンプルな構成です。以下のサンプルからわかる通り、字幕の表示時刻と字幕テキスト・インデックスで構成されます。

字幕サンプル.srt
1
00:00:00,000 --> 00:00:26,133
【待合室 RS-ST-1 幕間】
我らが主とし、付き従うは偉大なるイェラガンド。
雲はその羽であり、風はその翼である。
主は我らに陽光と慈雨を与え、また血肉と毛皮を与えてくださる。
我らが主とし、敬愛するは仁愛溢るるイェラガンド。
山々はその骨であり、百川はその尾である。
我らは主の背を歩み、その懐で穏やかに眠る。

2
00:00:26,133 --> 00:00:51,833
【待合室 RS-ST-1 幕間】
我らが主とし、賛美するは慈悲深きイェラガンド。
主は、我らが悩み苦しむ時には優しく慰めてくださる。
主は、我らが受難する時には力を尽くしてお救いくださる。
イェラガンドは敬虔なる民を、そして山石と百獣を守り、災いから遠ざけ、永久
の静かなる安寧を約束してくださるのだ。
――『イェラガンド』

以下のようなコードで、字幕を生成することができます。

字幕データのサンプル.py
outlineData = [
    {
    "start_end": [0, 2638],
    "text": "今回はPythonでDavinciResolveの字幕を動的に生成していきます"
    },
    {
    "start_end": [2639, 5120],
    "text": "まず用意するのはパソコンとマウスです"
    },
]
字幕データの出力.py
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

screenshot_davinciresolve_read.png

3. 字幕のインポート
同じくタスクバーから、ファイル>読み込み>字幕>autogenerated_subtitles.srtを選択

4. 字幕の配置
エディットに移動して、メディア欄のautogenerated_subtitlesをタイムラインにドラッグ&ドロップ

5. 字幕の位置調整
.srtファイルには字幕の位置データが含まれていないため、手動で調整が必要

6. 字幕を動画に焼き付ける設定に変更
字幕を映像として出力したい場合は、デリバーに移動して設定を変更する

screenshot_davinciresolve_wright.png

以上の手順で、Pythonから生成した字幕とタイムラインをDavinciResolveに適用することができます。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?