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 @@
11# Clover's Creative Control
22
3This is a set of tools to let additional hardware surfaces integrate with
4creative applications on a Mac device. In addition to the primary keybinding
5system, this project can be used as a library to use the control primitves
6directly (either to build your own hardware integrations, or to control the
7software).
8
9These tools only work on macOS. I don't have interest in maintaining other
10configurations.
3This is a set of tools to let additional hardware devices integrate with
4creative applications on a Mac device. Additionally, this repo contains a lot of
5my own tools and scripts I use in the Music/Video creative processes.
6
7In addition to the primary keybinding system and personal software
8configurations, this project can be used as a library to use the control
9primitives directly (either to build your own hardware integrations, or to
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
1216## Hardware
1317