前回は、FlameでのACES環境とCDLの扱いについて整理しました。
その中で、.cccファイルを使ったショット単位のCDL適用について触れましたが、
Flame標準機能だけでは自動適用が難しい場面があります。
今回は、.cccファイルからCDL情報を取得し、Lookノードへ適用するPython Hookについて整理します。
なぜPythonが必要か
Flameでは、EDLを使った場合、ショット単位でCDLが適用されますが、.cccファイルのみ渡されるケースでは、
自動適用の仕組みがありません。
そのため、
- ショットごとに手動で設定する必要がある
- ミスが発生しやすい
といった問題が出てきます。
実装と処理
.cccファイルには複数のCDL情報が含まれており、それぞれにCCCIDが割り当てられています。
このCCCIDとFlameのsource_nameを照合することで、ショットとCDLを紐付けることができます。
処理の流れは以下の通りです。
- .cccファイルを読み込み
- CCCIDとCDL値を辞書として保持
- タイムラインセグメントを取得
- source_nameとCCCIDを照合
- Lookノードを取得または新規作成
- CDL値(Slope / Offset / Power / Saturation)を適用
既存のLookノードがある場合は再利用し、無い場合のみ新規作成するようにしています。
この処理をベースにスクリプトを組んでいます。
まず、.cccファイルからCCCIDごとのCDL値を取得、CCCIDとショットの紐付きを確認しましたが、
最初は一致せず、マッチしない状態でした。
ポイントは2つありました。
一つは、.cccのCCCIDをFlameのショットと一致させる方法です。
最初は、source_name / file_path / tape_nameのいずれを使うか検証しましたが、
今回の素材では、source_nameをそのまま使うことで、CCCIDと一致することを確認しました。
実際の確認ログは以下の通りです。
=== SEGMENT CANDIDATES ===
source_name
raw : A001C002_0904242F
norm: A001C002_0904242F
file_path
raw : /Volumes/.../A001C002_0904242F.exr
norm: A001C002_0904242F
tape_name
raw : A001C002_0904242F
norm: A001C002_0904242F
=== MATCH RESULT ===
MATCHED FROM : source_name
CCCID : A001C002_0904242F
もう一つは、複数あるタイムラインセグメントへの適用方法です。
current_segment.parentからトラックを取得し、track.segments を使うことで、
トラック内の全セグメントに対して処理を行えることを確認しました。
実際の確認ログは以下の通りです。
=== TRACK MATCH TEST START ===
SEGMENT:
MATCHED FROM : source_name
CCCID : A001C002_0904242F
SEGMENT:
MATCHED FROM : source_name
CCCID : B004C0023_106298
=== TRACK MATCH TEST END ===
つまり今回は、
- CCCIDとsource_nameの一致(キーの特定)
- track.segmentsによる複数ショット処理(適用範囲)
両方が成立したことで、EDLなしでも実用的な運用が可能になります。
■ 補足:Lookノード Working Spaceについて
今回の実装では、LookノードにCDL値を適用することはできますが、
Working Space(カラースペース)をPythonから直接設定することができませんでした。
そのため、LookノードのWorking Spaceについては、あらかじめACEScctを設定した状態で
ノードセットアップとして保存し、新規作成時にそのノードセットアップを読み込む形にしています。


# 新規 Look 作成時に読み込むテンプレート LOOK_SETUP_ACESCCT = "/Volumes/StorageMedia/Apple_Project/Import_CDL/setups/look/ACEScct.look_node"
この方法により、
- CDL値はショット単位で適用
- Working Spaceはプロジェクト単位で統一
という役割分担で運用することができます。
スクリプト構成
今回のスクリプトは、以下のような構成で整理しています。
・load_ccc()
.cccファイルを読み込み、CCCIDとCDL値を取得
・normalize()
名前を比較用に整形
・find_look_fx()
既存のLookノードを取得
・create_look_fx()
Lookノードが無い場合に新規作成
・apply_cdl()
CDL値をLookノードに適用
・list_ccc_files()
指定ディレクトリ内の.cccファイル一覧を取得
・get_timeline_custom_ui_actions()
Flameの右クリックメニュー(Timeline Hook)を生成
処理としては、
.cccの読み込み
↓
CCCIDとショットの照合
↓
Lookノードの取得または作成
↓
CDL適用
という流れになります。
ccc_apply_timeline_hook.py
import xml.etree.ElementTree as ET
import flame
import os
import re
# .ccc を置いておくフォルダ
CCC_DIR = "/Volumes/StorageMedia/Apple_Project/Import_CDL/setups/colour_mgmt/Resolve_CDL"
# 新規 Look 作成時に読み込むテンプレート
LOOK_SETUP_ACESCCT = "/Volumes/StorageMedia/Apple_Project/Import_CDL/setups/look/ACEScct.look_node"
def normalize(name):
if name is None:
return ""
name = os.path.basename(str(name))
name = re.sub(r"\.[^.]+$", "", name) # 拡張子削除
name = re.sub(r"_\d+-\d+_?$", "", name) # Resolve由来の末尾範囲を削除
return name
def load_ccc(path):
tree = ET.parse(path)
root = tree.getroot()
data = {}
for cc in root.iter():
if cc.tag.split("}")[-1] != "ColorCorrection":
continue
cid = normalize(cc.get("id"))
slope = None
offset = None
power = None
sat = None
for elem in cc.iter():
tag = elem.tag.split("}")[-1]
txt = elem.text.strip() if elem.text else None
if tag == "Slope":
slope = txt
elif tag == "Offset":
offset = txt
elif tag == "Power":
power = txt
elif tag == "Saturation":
sat = txt
if cid and slope and offset and power and sat:
data[cid] = {
"slope": [float(x) for x in slope.split()],
"offset": [float(x) for x in offset.split()],
"power": [float(x) for x in power.split()],
"sat": float(sat),
}
return data
def find_look_fx(seg):
for fx in seg.effects:
try:
if str(fx.type) == "Look":
return fx
except Exception:
pass
return None
def create_look_fx(seg):
fx = seg.create_effect("Look")
if os.path.isfile(LOOK_SETUP_ACESCCT):
try:
fx.load_setup(LOOK_SETUP_ACESCCT)
print("LOADED SETUP:", LOOK_SETUP_ACESCCT)
except Exception as e:
print("WARNING: failed to load setup:", e)
else:
print("WARNING: setup file not found:", LOOK_SETUP_ACESCCT)
return fx
def get_or_create_look_fx(seg):
fx = find_look_fx(seg)
if fx is not None:
return fx, False # reused
fx = create_look_fx(seg)
return fx, True # created
def apply_cdl(seg, ccc):
shot_name = normalize(seg.source_name)
if shot_name not in ccc:
print("NO MATCH:", shot_name)
return False, shot_name
values = ccc[shot_name]
fx, created = get_or_create_look_fx(seg)
fx.slope_red = values["slope"][0]
fx.slope_green = values["slope"][1]
fx.slope_blue = values["slope"][2]
fx.offset_red = values["offset"][0]
fx.offset_green = values["offset"][1]
fx.offset_blue = values["offset"][2]
fx.power_red = values["power"][0]
fx.power_green = values["power"][1]
fx.power_blue = values["power"][2]
fx.saturation = values["sat"]
if created:
print("CREATED + APPLIED:", shot_name)
else:
print("REUSED + APPLIED:", shot_name)
return True, shot_name
def selection_has_segments(selection):
for item in selection:
try:
if str(item.type) == "Video Segment":
return True
except Exception:
pass
return False
def execute_apply_ccc_from_file(selection, ccc_file):
if not os.path.isfile(ccc_file):
print("CCC file not found:", ccc_file)
return
print("CCC FILE:", ccc_file)
ccc = load_ccc(ccc_file)
applied = []
no_match = []
print("\n=== APPLY START ===")
for item in selection:
try:
if str(item.type) != "Video Segment":
continue
except Exception:
continue
ok, shot = apply_cdl(item, ccc)
if ok:
applied.append(shot)
else:
no_match.append(shot)
print("\n=== SUMMARY ===")
print("Applied:", len(applied))
print("No Match:", len(no_match))
if no_match:
print("Unmatched shots:")
for shot in no_match:
print(" -", shot)
print("\n=== DONE ===")
def make_execute_callback(ccc_file):
def _execute(selection):
execute_apply_ccc_from_file(selection, ccc_file)
return _execute
def list_ccc_files():
if not os.path.isdir(CCC_DIR):
return []
files = []
for name in sorted(os.listdir(CCC_DIR)):
if name.lower().endswith(".ccc"):
files.append(os.path.join(CCC_DIR, name))
return files
def get_timeline_custom_ui_actions():
actions = []
ccc_files = list_ccc_files()
if not ccc_files:
return [
{
"name": "ccc_files",
"actions": [
{
"name": "No .ccc files found",
"isVisible": selection_has_segments,
"execute": lambda selection: print("No .ccc files found in:", CCC_DIR),
"minimumVersion": "2023"
}
]
}
]
for ccc_file in ccc_files:
display_name = os.path.basename(ccc_file)
actions.append(
{
"name": f"{display_name}",
"isVisible": selection_has_segments,
"execute": make_execute_callback(ccc_file),
"minimumVersion": "2023"
}
)
return [
{
"name": "ccc_Tools",
"actions": actions
}
]
まとめ
今回の方法により、.cccファイルのみでもショット単位でのCDL適用を自動化することができました。
Flame標準機能では難しい部分も、Python Hookを使うことで実務レベルで対応可能になります。
次は、View TransformとContext Variableを使った、
表示側でのCDL制御について整理します。