前編では、DaVinci Resolveから書き出したOTIOファイルをFlame 2027で読み込み、Match Criteriaの挙動を確認しました。

Source Timecodeではリンクできましたが、Name / File NameではStrict ONからリンクすることができませんでした。

また、ConformリストのNameカラムには、ファイルネームTCと拡張子が付加された名前が表示されていました。

今回は、OTIOファイルの中身を確認しながら、その原因を調べていきます。

OTIOファイルの中身を確認

Rocky LinuxやmacOSでは、fileコマンドを使うことで、ファイルの種類を確認することができます。

OTIOファイルはバイナリ形式ではなく、テキストファイルとして保存されていることが分かります。

実際に中身を確認してみましょう。

先頭を見ると、

「キー : 値」の形式で情報が記録されています。

JSONファイル形式でよく見られる構造です。OTIOファイルがJSONファイルとして読み込めるか確認してみましょう。

dictは、Pythonの辞書型(dictionary)のことです。
OTIOファイルがJSONファイル形式で記録されているため、Pythonから辞書型として読み込むことができました。

OTIOファイル内には、タイムラインやクリップなどの情報が記録されています。
まずは、どのような情報が含まれているのか確認してみます。

出力結果は長くなりますが、実際の検索結果をそのまま掲載しています。

[admin@Rocky Documents]$ grep -n OTIO_SCHEMA edit.otio
2:    "OTIO_SCHEMA": "Timeline.1",
10:        "OTIO_SCHEMA": "RationalTime.1",
15:        "OTIO_SCHEMA": "Stack.1",
24:                "OTIO_SCHEMA": "Track.1",
37:                        "OTIO_SCHEMA": "Clip.2",
43:                            "OTIO_SCHEMA": "TimeRange.1",
45:                                "OTIO_SCHEMA": "RationalTime.1",
50:                                "OTIO_SCHEMA": "RationalTime.1",
57:                                "OTIO_SCHEMA": "Effect.1",
72:                                "OTIO_SCHEMA": "Effect.1",
87:                                "OTIO_SCHEMA": "Effect.1",
149:                                "OTIO_SCHEMA": "Effect.1",
164:                                "OTIO_SCHEMA": "Effect.1",
179:                                "OTIO_SCHEMA": "Effect.1",
194:                                "OTIO_SCHEMA": "Effect.1",
209:                                "OTIO_SCHEMA": "Effect.1",
228:                                "OTIO_SCHEMA": "ImageSequenceReference.1",
232:                                    "OTIO_SCHEMA": "TimeRange.1",
234:                                        "OTIO_SCHEMA": "RationalTime.1",
239:                                        "OTIO_SCHEMA": "RationalTime.1",
258:                        "OTIO_SCHEMA": "Clip.2",
264:                            "OTIO_SCHEMA": "TimeRange.1",
266:                                "OTIO_SCHEMA": "RationalTime.1",
271:                                "OTIO_SCHEMA": "RationalTime.1",
278:                                "OTIO_SCHEMA": "Effect.1",
293:                                "OTIO_SCHEMA": "Effect.1",
308:                                "OTIO_SCHEMA": "Effect.1",
370:                                "OTIO_SCHEMA": "Effect.1",
385:                                "OTIO_SCHEMA": "Effect.1",
400:                                "OTIO_SCHEMA": "Effect.1",
415:                                "OTIO_SCHEMA": "Effect.1",
430:                                "OTIO_SCHEMA": "Effect.1",
449:                                "OTIO_SCHEMA": "ImageSequenceReference.1",
453:                                    "OTIO_SCHEMA": "TimeRange.1",
455:                                        "OTIO_SCHEMA": "RationalTime.1",
460:                                        "OTIO_SCHEMA": "RationalTime.1",
479:                        "OTIO_SCHEMA": "Clip.2",
485:                            "OTIO_SCHEMA": "TimeRange.1",
487:                                "OTIO_SCHEMA": "RationalTime.1",
492:                                "OTIO_SCHEMA": "RationalTime.1",
499:                                "OTIO_SCHEMA": "Effect.1",
514:                                "OTIO_SCHEMA": "Effect.1",
529:                                "OTIO_SCHEMA": "Effect.1",
591:                                "OTIO_SCHEMA": "Effect.1",
606:                                "OTIO_SCHEMA": "Effect.1",
621:                                "OTIO_SCHEMA": "Effect.1",
636:                                "OTIO_SCHEMA": "Effect.1",
651:                                "OTIO_SCHEMA": "Effect.1",
670:                                "OTIO_SCHEMA": "ImageSequenceReference.1",
674:                                    "OTIO_SCHEMA": "TimeRange.1",
676:                                        "OTIO_SCHEMA": "RationalTime.1",
681:                                        "OTIO_SCHEMA": "RationalTime.1",
700:                        "OTIO_SCHEMA": "Clip.2",
706:                            "OTIO_SCHEMA": "TimeRange.1",
708:                                "OTIO_SCHEMA": "RationalTime.1",
713:                                "OTIO_SCHEMA": "RationalTime.1",
720:                                "OTIO_SCHEMA": "LinearTimeWarp.1",
727:                                "OTIO_SCHEMA": "Effect.1",
742:                                "OTIO_SCHEMA": "Effect.1",
757:                                "OTIO_SCHEMA": "Effect.1",
819:                                "OTIO_SCHEMA": "Effect.1",
834:                                "OTIO_SCHEMA": "Effect.1",
849:                                "OTIO_SCHEMA": "Effect.1",
864:                                "OTIO_SCHEMA": "Effect.1",
879:                                "OTIO_SCHEMA": "Effect.1",
898:                                "OTIO_SCHEMA": "ImageSequenceReference.1",
902:                                    "OTIO_SCHEMA": "TimeRange.1",
904:                                        "OTIO_SCHEMA": "RationalTime.1",
909:                                        "OTIO_SCHEMA": "RationalTime.1",
931:                "OTIO_SCHEMA": "Track.1",
946:                        "OTIO_SCHEMA": "Gap.1",
950:                            "OTIO_SCHEMA": "TimeRange.1",
952:                                "OTIO_SCHEMA": "RationalTime.1",
957:                                "OTIO_SCHEMA": "RationalTime.1",
[admin@Rocky Documents]$ 

この中から今回注目するのは、Clip.2(37行目)ImageSequenceReference.1(228行目)です。
まずは、37行目付近を確認してみます。

[admin@Rocky Documents]$ nl -ba edit.otio | sed -n '30,60p'
    30	                "name": "Video 1",
    31	                "source_range": null,
    32	                "effects": [],
    33	                "markers": [],
    34	                "enabled": true,
    35	                "children": [
    36	                    {
    37	                        "OTIO_SCHEMA": "Clip.2",
    38	                        "metadata": {
    39	                            "Resolve_OTIO": {}
    40	                        },
    41	                        "name": "A002C001_2206227A[141603-141626].exr", ----------> シーケンスセグメント名
    42	                        "source_range": {
    43	                            "OTIO_SCHEMA": "TimeRange.1",
    44	                            "duration": {
    45	                                "OTIO_SCHEMA": "RationalTime.1",
    46	                                "rate": 23.976023976023979,
    47	                                "value": 24.0
    48	                            },
    49	                            "start_time": {
    50	                                "OTIO_SCHEMA": "RationalTime.1",
    51	                                "rate": 23.976023976023979,
    52	                                "value": 141603.0
    53	                            }
    54	                        },
    55	                        "effects": [
    56	                            {
    57	                                "OTIO_SCHEMA": "Effect.1",
    58	                                "metadata": {
    59	                                    "Resolve_OTIO": {
    60	                                        "Display Type": 1,
[admin@Rocky Documents]$ 

41行目の”name"に、前編で確認したNameカラムと同じ値が記録されています。

OTIOファイルを複製

今回は動作確認のため、41行目の”name”から、ファイルネームTCと拡張子を手動で削除します。

手動修正後、Flameで確認

手動で修正したedit_rename.otioをインポート

シーケンスセグメントを確認すると、ファイルネームTCと拡張子の表示なし

Match Criteria > Name / Strict ONからリンクすることができる

DaVinci Resolveから書き出したOTIOファイルの”name”に含まれるファイルネームTCと拡張子が、
Match Criteria > Name / Strict ON選択時にリンクできない原因の一つであることを確認することができました。

Pythonスクリプトで自動化

手動で修正できることは確認しましたが、実案件で毎回OTIOファイルを編集するのは現実的ではありません。

以前の記事では、DaVinci Resolveから書き出したEDL/XMLを解析し、.cccファイルを自動生成するPythonスクリプトを作成しました。

今回も考え方は同じです。

OTIOファイルはJSON形式のテキストファイルなので、シーケンスセグメント名として記録されている”name”からファイルネームTCと拡張子を削除するスクリプトを作成します。

resolve_otio_strip_clip_ext.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import json
import re
from pathlib import Path

EXT_RE = re.compile(r"\.(exr|dpx|tif|tiff|jpg|jpeg|png|mov|mxf|mp4)$", re.IGNORECASE)
FRAME_RANGE_RE = re.compile(r"\[[-0-9]+\s*-\s*[-0-9]+\]$")


def clean_name(name: str, strip_frame_range: bool = False) -> str:
    """
    Resolve OTIO が作る Clip.name / media_reference.name から拡張子を外す。
    strip_frame_range=True の場合は末尾の [1001-1024] も外す。
    """
    name = EXT_RE.sub("", name)

    if strip_frame_range:
        name = FRAME_RANGE_RE.sub("", name)
        name = name.rstrip("._- ")

    return name


def process_otio(data: dict, strip_frame_range: bool = False) -> int:
    changed = 0

    def walk(obj):
        nonlocal changed

        if isinstance(obj, dict):
            schema = obj.get("OTIO_SCHEMA", "")

            # Flame の File Name リンクで見える可能性が高いクリップ名
            if schema.startswith("Clip") and isinstance(obj.get("name"), str):
                old = obj["name"]
                new = clean_name(old, strip_frame_range)
                if new != old:
                    obj["name"] = new
                    changed += 1

            # Resolve の ImageSequenceReference 側の表示名
            # 注意: name_suffix は実ファイル列の拡張子なので残す
            media_refs = obj.get("media_references")
            if isinstance(media_refs, dict):
                for mr in media_refs.values():
                    if isinstance(mr, dict) and isinstance(mr.get("name"), str):
                        old = mr["name"]
                        new = clean_name(old, strip_frame_range)
                        if new != old:
                            mr["name"] = new
                            changed += 1

            for value in obj.values():
                walk(value)

        elif isinstance(obj, list):
            for value in obj:
                walk(value)

    walk(data)
    return changed


def main():
    parser = argparse.ArgumentParser(
        description="Remove file extensions from Resolve OTIO clip names for Flame relinking."
    )
    parser.add_argument("input_otio", help="Input .otio file")
    parser.add_argument("output_otio", help="Output .otio file")
    parser.add_argument(
        "--strip-frame-range",
        action="store_true",
        help="Also remove trailing frame range like [1001-1024]",
    )
    args = parser.parse_args()

    input_path = Path(args.input_otio)
    output_path = Path(args.output_otio)

    data = json.loads(input_path.read_text(encoding="utf-8"))
    changed = process_otio(data, strip_frame_range=args.strip_frame_range)

    output_path.write_text(
        json.dumps(data, indent=4, ensure_ascii=False),
        encoding="utf-8",
    )

    print(f"Changed fields : {changed}")
    print(f"Input          : {input_path}")
    print(f"Output         : {output_path}")


if __name__ == "__main__":
    main()

ヘルプを確認

[admin@Rocky Documents]$ python3 resolve_otio_strip_clip_ext.py --help
usage: resolve_otio_strip_clip_ext.py [-h] [--strip-frame-range] input_otio output_otio

Remove file extensions from Resolve OTIO clip names for Flame relinking.

positional arguments:
  input_otio           Input .otio file
  output_otio          Output .otio file

optional arguments:
  -h, --help           show this help message and exit
  --strip-frame-range  Also remove trailing frame range like [1001-1024] ----------> ファイルネームTCを削除するオプション
[admin@Rocky Documents]$ 

コマンド実行

[admin@Rocky Documents]$ python3 resolve_otio_strip_clip_ext.py --strip-frame-range edit.otio edit_strip.otio
Changed fields : 8
Input          : edit.otio
Output         : edit_strip.otio
[admin@Rocky Documents]$ 

“name”検索

[admin@Rocky Documents]$ grep -n '"name":' edit_strip.otio | grep -E 'A00|B00' ----------> フィルネームTC/拡張子削除できているか確認
41:                        "name": "A002C001_2206227A", ----------> Clip.2
230:                                "name": "A002C001_2206227A", ----------> ImageSequenceReference
262:                        "name": "B004C0023",
451:                                "name": "B004C0023",
483:                        "name": "A003C013_220415_R07B",
672:                                "name": "A003C013_220415_R07B",
704:                        "name": "A005C0013",
900:                                "name": "A005C0013",
[admin@Rocky Documents]$ sed -n '41p;262p;483p;704p' edit_strip.otio ----------> 行番号がわかっているので、Clip.2だけ検索
                        "name": "A002C001_2206227A",
                        "name": "B004C0023",
                        "name": "A003C013_220415_R07B",
                        "name": "A005C0013",
[admin@Rocky Documents]$ 

Clip.2とImageSequenceReferenceの両方に”name”が存在するため、同じクリップ名が2回ずつ表示されています。

strip_clip_ext.py後、結果を確認

ファイルネームTCと拡張子を削除したedit_strip.ocioをインポート

NameカラムからフィルネームTCと拡張子が削除されていることを確認
Match Criteria > Name / Strict ONからリンクすることができる

File Nameカラムを確認

Name / Strict ON によるリンクは成功しました。

しかし、Conformリストを確認すると、File Nameカラムには依然として末尾に「d」が表示されています。

Nameカラムには

が表示されていますが、File Nameカラムには末尾に「d」が追加されています。

今回修正したのはClip.2オブジェクト内の”name”です。
そのため、File Nameカラムが参照している情報は別の場所に存在している可能性があります。

“name”検索時に、OTIOファイル内で同じクリップ名が2回ずつ出現していたのを覚えているでしょうか。

次は ImageSequenceReference.1を確認してみます。

ImageSequenceReferenceを確認

228行目付近を確認

[admin@Rocky Documents]$ nl -ba edit_strip.otio | sed -n '220,250p'
   220	                                "name": "",
   221	                                "effect_name": "Resolve Effect"
   222	                            }
   223	                        ],
   224	                        "markers": [],
   225	                        "enabled": true,
   226	                        "media_references": {
   227	                            "DEFAULT_MEDIA": {
   228	                                "OTIO_SCHEMA": "ImageSequenceReference.1",
   229	                                "metadata": {},
   230	                                "name": "A002C001_2206227A",
   231	                                "available_range": {
   232	                                    "OTIO_SCHEMA": "TimeRange.1",
   233	                                    "duration": {
   234	                                        "OTIO_SCHEMA": "RationalTime.1",
   235	                                        "rate": 23.976023976023978,
   236	                                        "value": 24.0
   237	                                    },
   238	                                    "start_time": {
   239	                                        "OTIO_SCHEMA": "RationalTime.1",
   240	                                        "rate": 23.976023976023978,
   241	                                        "value": 141603.0
   242	                                    }
   243	                                },
   244	                                "available_image_bounds": null,
   245	                                "target_url_base": "/Volumes/StorageMedia/Cafu_Share/ACES2065-1/OTIO",
   246	                                "name_prefix": "A002C001_2206227A",
   247	                                "name_suffix": ".exr",
   248	                                "start_frame": 141603,
   249	                                "frame_step": 1,
   250	                                "rate": 23.976023976023978,
[admin@Rocky Documents]$ 

Clip.2にも”name”があり、ImageSequenceReferenceにも”name”があります。

まずはImageSequenceReferenceの内容を確認してみます。

Clip.2ではリンク用のクリップ情報が記録されていましたが、
ImageSequenceReferenceには実際のファイルシーケンス情報が記録されています。

“name”以外にも、

などが存在していることが確認できます。

[admin@Rocky Documents]$ grep -n 'name_prefix\|name_suffix\|start_frame' edit_strip.otio
246:                                "name_prefix": "A002C001_2206227A",
247:                                "name_suffix": ".exr",
248:                                "start_frame": 141603,
467:                                "name_prefix": "B004C0023.",
468:                                "name_suffix": ".exr",
469:                                "start_frame": 106298,
688:                                "name_prefix": "A003C013_220415_R07B",
689:                                "name_suffix": ".exr",
690:                                "start_frame": 261342,
916:                                "name_prefix": "A005C0013.",
917:                                "name_suffix": ".exr",
918:                                "start_frame": 374113,
[admin@Rocky Documents]$ 

File Nameカラムに表示される末尾の「d」は、Clip.2の”name”を修正しても変化しませんでした。

ImageSequenceReference側には、”name_prefix”や”name_suffix”が存在しているため、
こちらの情報がFile Nameカラムに影響している可能性があります。

そこで、ImageSequenceReference側の情報も含めて処理できるように、resolve_otio_strip_clip_ext.pyをベースに機能を追加しました。

resolve_otio_clean_for_flame.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import json
import re
from pathlib import Path

EXT_RE = re.compile(r"\.(exr|dpx|tif|tiff|jpg|jpeg|png|mov|mxf|mp4)$", re.IGNORECASE)
FRAME_RANGE_RE = re.compile(r"\[[-0-9]+\s*-\s*[-0-9]+\]$")


def clean_name(name: str, strip_frame_range: bool = False) -> str:
    name = EXT_RE.sub("", name)
    if strip_frame_range:
        name = FRAME_RANGE_RE.sub("", name)
        name = name.rstrip("._- ")
    return name


def round_near_integer(value, epsilon=1e-4):
    """Resolve OTIO may write values like 261341.99999999997. Flame can read this as 1 frame early.
    Round only when the value is effectively an integer.
    """
    if isinstance(value, (int, float)):
        nearest = round(value)
        if abs(value - nearest)  tuple[int, int]:
    name_changed = 0
    time_changed = 0

    def walk(obj):
        nonlocal name_changed, time_changed

        if isinstance(obj, dict):
            schema = obj.get("OTIO_SCHEMA", "")

            if schema.startswith("Clip") and isinstance(obj.get("name"), str):
                old = obj["name"]
                new = clean_name(old, strip_frame_range)
                if new != old:
                    obj["name"] = new
                    name_changed += 1

            media_refs = obj.get("media_references")
            if isinstance(media_refs, dict):
                for mr in media_refs.values():
                    if isinstance(mr, dict) and isinstance(mr.get("name"), str):
                        old = mr["name"]
                        new = clean_name(old, strip_frame_range)
                        if new != old:
                            mr["name"] = new
                            name_changed += 1

            if fix_near_integer_time and schema.startswith("RationalTime") and isinstance(obj.get("value"), (int, float)):
                old = obj["value"]
                new = round_near_integer(old)
                if new != old:
                    obj["value"] = new
                    time_changed += 1

            for value in obj.values():
                walk(value)

        elif isinstance(obj, list):
            for value in obj:
                walk(value)

    walk(data)
    return name_changed, time_changed


def main():
    parser = argparse.ArgumentParser(
        description="Clean Resolve OTIO clip names and near-integer frame values for Flame relinking."
    )
    parser.add_argument("input_otio", help="Input .otio file")
    parser.add_argument("output_otio", help="Output .otio file")
    parser.add_argument("--strip-frame-range", action="store_true", help="Remove trailing frame range like [1001-1024]")
    parser.add_argument("--fix-near-integer-time", action="store_true", help="Round values like 261341.99999999997 to 261342.0")
    args = parser.parse_args()

    input_path = Path(args.input_otio)
    output_path = Path(args.output_otio)

    data = json.loads(input_path.read_text(encoding="utf-8"))
    name_changed, time_changed = process_otio(
        data,
        strip_frame_range=args.strip_frame_range,
        fix_near_integer_time=args.fix_near_integer_time,
    )

    output_path.write_text(json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8")

    print(f"Changed name fields : {name_changed}")
    print(f"Rounded time values : {time_changed}")
    print(f"Input               : {input_path}")
    print(f"Output              : {output_path}")


if __name__ == "__main__":
    main()

ヘルプを確認

[admin@Rocky Documents]$ python3 resolve_otio_clean_for_flame.py --help
usage: resolve_otio_clean_for_flame.py [-h] [--strip-frame-range] [--fix-near-integer-time] [--flatten-for-strict-file-name]
                                       input_otio output_otio

Clean Resolve OTIO names and near-integer frame values for Flame relinking.

positional arguments:
  input_otio            Input .otio file
  output_otio           Output .otio file

optional arguments:
  -h, --help            show this help message and exit
  --strip-frame-range   Remove trailing frame range like [1001-1024]
  --fix-near-integer-time
                        Round values like 261341.99999999997 to 261342.0
  --flatten-for-strict-file-name
                        Flatten ImageSequenceReference naming so Flame File Name Strict matching 
               does not see a trailing printf-style 'd' ----------> 末尾の「d」を除去する検証用オプション
[admin@Rocky Documents]$ 

コマンド実行

[admin@Rocky Documents]$ python3 resolve_otio_clean_for_flame.py --flatten-for-strict-file-name edit_strip.otio edit_flatten.otio
Changed name fields : 10
Rounded time values : 0
Input               : edit_strip.otio
Output              : edit_flatten.otio
[admin@Rocky Documents]$ 

clean_for_flame.py後、File Nameリンクを確認

File Nameから「d」を削除したedit_flatten.otioをインポート

File Nameカラムから「d」が削除されていることを確認
Match Criteria > File Name / Strict ONからリンクすることがなぜかできない

結果

File Nameカラムから末尾の「d」は削除されましたが、Match Criteria > File Name / Strict ONによるリンク結果に変化はありませんでした。

現時点では、File Name / Strict ONでリンクできない原因は特定できていません。

まとめ

今回の検証では、DaVinci Resolveから書き出したOTIOファイルに含まれるClip.2およびImageSequenceReferenceの構造を確認し、Name / Strict ONからリンクできない原因の一部を特定することができました。

一方で、File Name / Strict ONについては未解決の部分が残っています。

原因を特定できていませんが、引き続きOTIOファイルの構造やFlameのリンクロジックを調査していきたいと思います。