authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-15 21:08:18-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-15 21:09:14-07:00
logc7e1a06c390d968e00840aceead85c877a0f2abb
treed3c7896caffb45c5f518c587144505ba8c1e32e5
parent08668fa9c705518672db653cf2c647a29dd56c19
signaturebadge-check Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: add some other vibe scripts i happen to have


3 files changed, 220 insertions(+), 8 deletions(-)

bin/exr_flip_z.py created+66
...@@ -0,0 +1,66 @@
1# Fusion uses negative Z values for the depth buffer, while Blender denotes
2# this with positive values. For "Depth Merge" and other nodes to work
3# correctly, Blender's output must be flipped. I am unaware of how to do
4# this in Blender itself, hence this simple post processor.
5import OpenEXR
6import Imath
7import numpy as np
8import argparse
9import os
10
11Z_FLIPPED_METADATA_KEY = "zBufferFlipped"
12
13def invert_z_buffer(exr_input_path):
14 exr_file = OpenEXR.InputFile(exr_input_path)
15
16 header = exr_file.header()
17 channels = header['channels'];
18 part_names = channels.keys()
19
20 if Z_FLIPPED_METADATA_KEY in header:
21 print(f"Skipping {exr_input_path}")
22 exr_file.close()
23 return
24
25 processed_parts = {}
26
27 for part_name in part_names:
28 pixel_type = header['channels'][part_name].type
29 if pixel_type == Imath.PixelType(Imath.PixelType.HALF):
30 dtype = np.float16
31 elif pixel_type == Imath.PixelType(Imath.PixelType.FLOAT):
32 dtype = np.float32
33 else:
34 raise ValueError(f"Unsupported pixel type {pixel_type} for channel {part_name}.")
35
36 channel_data = exr_file.channel(part_name, pixel_type)
37 channel_data_array = np.frombuffer(channel_data, dtype=dtype)
38
39 if "Depth.Z" in part_name:
40 channel_data_array = -channel_data_array
41
42 processed_parts[part_name] = channel_data_array.tobytes()
43
44 header[Z_FLIPPED_METADATA_KEY] = 1
45
46 exr_output = OpenEXR.OutputFile(exr_input_path, header)
47
48 exr_output.writePixels(processed_parts)
49
50 exr_file.close()
51 exr_output.close()
52
53 print(f"Processed: {exr_input_path}")
54
55if __name__ == "__main__":
56 parser = argparse.ArgumentParser(description="Invert the Z-buffer in multipart EXR files.")
57
58 parser.add_argument('exr_files', nargs='+', help="List of EXR files to process.")
59
60 args = parser.parse_args()
61
62 for exr_file in args.exr_files:
63 if os.path.exists(exr_file):
64 invert_z_buffer(exr_file)
65 else:
66 print(f"File not found: {exr_file}")
bin/import_quicktime_to_fusion.py created+142
...@@ -0,0 +1,142 @@
1#!/usr/bin/env python3
2import subprocess
3import os
4import sys
5import re
6import glob
7import pyperclip
8import time
9
10def run_command(cmd, shell=False):
11 """Run a command and return its output, exit on failure"""
12 try:
13 if shell:
14 result = subprocess.run(cmd, shell=True, check=True, text=True, capture_output=True)
15 else:
16 result = subprocess.run(cmd, check=True, text=True, capture_output=True)
17 return result.stdout.strip()
18 except subprocess.CalledProcessError as e:
19 print(f"Error executing command: {cmd}")
20 print(f"Error message: {e.stderr}")
21 sys.exit(1)
22 except Exception as e:
23 print(f"Unexpected error running command: {e}")
24 sys.exit(1)
25
26def find_file(base_name):
27 """Find a file with the given base name in the specified directory structure"""
28 search_path = "/Volumes/Project/*/Film/**/*"
29
30 try:
31 # Expand the glob pattern to find all matching files
32 matching_files = []
33 for project_dir in glob.glob("/Volumes/Project/*/"):
34 for root, dirs, files in os.walk(os.path.join(project_dir, "Film")):
35 # Skip hidden directories
36 dirs[:] = [d for d in dirs if not d.startswith('.')]
37 for file in files:
38 if file == base_name and not file.startswith('.'):
39 matching_files.append(os.path.join(root, file))
40
41 if not matching_files:
42 print(f"Error: Could not find file '{base_name}' in {search_path}")
43 sys.exit(1)
44 elif len(matching_files) > 1:
45 print(f"Warning: Found multiple matches for '{base_name}'. Using the first one.")
46
47 return matching_files[0]
48 except Exception as e:
49 print(f"Error searching for file: {e}")
50 sys.exit(1)
51
52def activate_app(app_name):
53 """Activate an application by name"""
54 try:
55 script = f'tell application "{app_name}" to activate'
56 subprocess.run(["osascript", "-e", script], check=True)
57 except Exception as e:
58 print(f"Error activating {app_name}: {e}")
59 sys.exit(1)
60
61def main():
62 # Step 1: Run Apple Script to get frame and name from QuickTime Player
63 print("Step 1: Getting frame and name from QuickTime Player...")
64 applescript = '''
65 tell application "QuickTime Player" to tell document 1
66 set t to current time
67 step forward
68 set k to current time
69 set r to 1 / (k - t)
70 step backward
71 return "" & (round (r * t) rounding down) & ":" & name
72 end tell
73 '''
74
75 try:
76 result = subprocess.run(["osascript", "-e", applescript],
77 check=True, text=True, capture_output=True)
78 frame_and_name = result.stdout.strip()
79
80 if not frame_and_name or ":" not in frame_and_name:
81 print("Error: AppleScript did not return expected output")
82 sys.exit(1)
83
84 target_frame, name = frame_and_name.split(":", 1)
85 target_frame = int(target_frame)
86
87 print(f"Target frame: {target_frame}")
88 print(f"File name: {name}")
89 except Exception as e:
90 print(f"Error running AppleScript: {e}")
91 sys.exit(1)
92
93 # Step 2: Find the file on disk
94 print("\nStep 2: Finding file on disk...")
95 file_path = find_file(name)
96 print(f"Found file at: {file_path}")
97
98 # Step 3: Run Fusion script to get current frame
99 print("\nStep 3: Getting current frame from Fusion...")
100 fusion_script_cmd = "'/Applications/Blackmagic Fusion 19/Fusion.app/Contents/Libraries/fuscript' -x 'print(\"[[\"..Fusion().CurrentComp.CurrentTime..\"]]\")'"
101 fusion_output = run_command(fusion_script_cmd, shell=True)
102
103 # Extract the frame number from the output
104 match = re.search(r'\[\[(\d+)\]\]', fusion_output)
105 if not match:
106 print(f"Error: Could not parse frame number from Fusion output: {fusion_output}")
107 sys.exit(1)
108
109 current_frame = int(match.group(1))
110 print(f"Current frame: {current_frame}")
111
112 # Step 4: Compute TRIM_IN and EXTEND_FIRST
113 print("\nStep 4: Computing TRIM_IN and EXTEND_FIRST...")
114 trim_in = 0
115 extend_first = 0
116
117 if target_frame > current_frame:
118 trim_in = target_frame - current_frame
119 print(f"Target frame is AFTER current frame. Setting TRIM_IN to {trim_in}")
120 else:
121 extend_first = current_frame - target_frame
122 print(f"Target frame is BEFORE current frame. Setting EXTEND_FIRST to {extend_first}")
123
124 # Step 5: Create the Fusion loader text and copy to clipboard
125 print("\nStep 5: Creating Fusion loader text and copying to clipboard...")
126 fusion_text = f'''{{Tools = ordered() {{Loader = Loader {{Clips = {{Clip {{ID = "Clip1",Filename = "{file_path}",FormatID = "QuickTimeMovies",Length = 9999999,Multiframe = true,TrimIn = {trim_in},TrimOut = 9999999,ExtendFirst = {extend_first},ExtendLast = 0,Loop = 1,AspectMode = 0,Depth = 0,TimeCode = 0,GlobalStart = 0,GlobalEnd = 9999999}}}},CtrlWZoom = false,Inputs = {{["Gamut.SLogVersion"] = Input {{ Value = FuID {{ "SLog2" }}, }}}},}}}},ActiveTool = "Loader"}}'''
127
128 try:
129 pyperclip.copy(fusion_text)
130 print("Text copied to clipboard:")
131 print(fusion_text)
132 except Exception as e:
133 print(f"Error copying to clipboard: {e}")
134 sys.exit(1)
135
136 # Finally, activate Fusion but don't paste
137 print("\nActivating Fusion...")
138 activate_app("Fusion")
139 print("Script completed successfully!")
140
141if __name__ == "__main__":
142 main()
readme.md+12-8
...@@ -1,13 +1,17 @@...@@ -1,13 +1,17 @@
1# Clover's Creative Control1# Clover's Creative Control
22
3This is a set of tools to let additional hardware surfaces integrate with3This is a set of tools to let additional hardware devices integrate with
4creative applications on a Mac device. In addition to the primary keybinding4creative applications on a Mac device. Additionally, this repo contains a lot of
5system, this project can be used as a library to use the control primitves5my own tools and scripts I use in the Music/Video creative processes.
6directly (either to build your own hardware integrations, or to control the6
7software).7In addition to the primary keybinding system and personal software
88configurations, this project can be used as a library to use the control
9These tools only work on macOS. I don't have interest in maintaining other9primitives directly (either to build your own hardware integrations, or to
10configurations.10control the software). This can be done by installing this repo as a `pnpm` git
11dependency in your project.
12
13**NOTE**: These tools only work on macOS. I don't have interest in maintaining
14other configurations.
1115
12## Hardware16## Hardware
1317