diff --git a/bin/exr_flip_z.py b/bin/exr_flip_z.py new file mode 100755 index 0000000000000000000000000000000000000000..497f0375ec70c8ef2158bbbc468fc02fe8d5f64f --- /dev/null +++ b/bin/exr_flip_z.py @@ -0,0 +1,66 @@ +# Fusion uses negative Z values for the depth buffer, while Blender denotes +# this with positive values. For "Depth Merge" and other nodes to work +# correctly, Blender's output must be flipped. I am unaware of how to do +# this in Blender itself, hence this simple post processor. +import OpenEXR +import Imath +import numpy as np +import argparse +import os + +Z_FLIPPED_METADATA_KEY = "zBufferFlipped" + +def invert_z_buffer(exr_input_path): + exr_file = OpenEXR.InputFile(exr_input_path) + + header = exr_file.header() + channels = header['channels']; + part_names = channels.keys() + + if Z_FLIPPED_METADATA_KEY in header: + print(f"Skipping {exr_input_path}") + exr_file.close() + return + + processed_parts = {} + + for part_name in part_names: + pixel_type = header['channels'][part_name].type + if pixel_type == Imath.PixelType(Imath.PixelType.HALF): + dtype = np.float16 + elif pixel_type == Imath.PixelType(Imath.PixelType.FLOAT): + dtype = np.float32 + else: + raise ValueError(f"Unsupported pixel type {pixel_type} for channel {part_name}.") + + channel_data = exr_file.channel(part_name, pixel_type) + channel_data_array = np.frombuffer(channel_data, dtype=dtype) + + if "Depth.Z" in part_name: + channel_data_array = -channel_data_array + + processed_parts[part_name] = channel_data_array.tobytes() + + header[Z_FLIPPED_METADATA_KEY] = 1 + + exr_output = OpenEXR.OutputFile(exr_input_path, header) + + exr_output.writePixels(processed_parts) + + exr_file.close() + exr_output.close() + + print(f"Processed: {exr_input_path}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Invert the Z-buffer in multipart EXR files.") + + parser.add_argument('exr_files', nargs='+', help="List of EXR files to process.") + + args = parser.parse_args() + + for exr_file in args.exr_files: + if os.path.exists(exr_file): + invert_z_buffer(exr_file) + else: + print(f"File not found: {exr_file}") diff --git a/bin/import_quicktime_to_fusion.py b/bin/import_quicktime_to_fusion.py new file mode 100755 index 0000000000000000000000000000000000000000..11c4b37b72b0a97f38d7a4a97b1807bba922a10f --- /dev/null +++ b/bin/import_quicktime_to_fusion.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import subprocess +import os +import sys +import re +import glob +import pyperclip +import time + +def run_command(cmd, shell=False): + """Run a command and return its output, exit on failure""" + try: + if shell: + result = subprocess.run(cmd, shell=True, check=True, text=True, capture_output=True) + else: + result = subprocess.run(cmd, check=True, text=True, capture_output=True) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error executing command: {cmd}") + print(f"Error message: {e.stderr}") + sys.exit(1) + except Exception as e: + print(f"Unexpected error running command: {e}") + sys.exit(1) + +def find_file(base_name): + """Find a file with the given base name in the specified directory structure""" + search_path = "/Volumes/Project/*/Film/**/*" + + try: + # Expand the glob pattern to find all matching files + matching_files = [] + for project_dir in glob.glob("/Volumes/Project/*/"): + for root, dirs, files in os.walk(os.path.join(project_dir, "Film")): + # Skip hidden directories + dirs[:] = [d for d in dirs if not d.startswith('.')] + for file in files: + if file == base_name and not file.startswith('.'): + matching_files.append(os.path.join(root, file)) + + if not matching_files: + print(f"Error: Could not find file '{base_name}' in {search_path}") + sys.exit(1) + elif len(matching_files) > 1: + print(f"Warning: Found multiple matches for '{base_name}'. Using the first one.") + + return matching_files[0] + except Exception as e: + print(f"Error searching for file: {e}") + sys.exit(1) + +def activate_app(app_name): + """Activate an application by name""" + try: + script = f'tell application "{app_name}" to activate' + subprocess.run(["osascript", "-e", script], check=True) + except Exception as e: + print(f"Error activating {app_name}: {e}") + sys.exit(1) + +def main(): + # Step 1: Run Apple Script to get frame and name from QuickTime Player + print("Step 1: Getting frame and name from QuickTime Player...") + applescript = ''' + tell application "QuickTime Player" to tell document 1 + set t to current time + step forward + set k to current time + set r to 1 / (k - t) + step backward + return "" & (round (r * t) rounding down) & ":" & name + end tell + ''' + + try: + result = subprocess.run(["osascript", "-e", applescript], + check=True, text=True, capture_output=True) + frame_and_name = result.stdout.strip() + + if not frame_and_name or ":" not in frame_and_name: + print("Error: AppleScript did not return expected output") + sys.exit(1) + + target_frame, name = frame_and_name.split(":", 1) + target_frame = int(target_frame) + + print(f"Target frame: {target_frame}") + print(f"File name: {name}") + except Exception as e: + print(f"Error running AppleScript: {e}") + sys.exit(1) + + # Step 2: Find the file on disk + print("\nStep 2: Finding file on disk...") + file_path = find_file(name) + print(f"Found file at: {file_path}") + + # Step 3: Run Fusion script to get current frame + print("\nStep 3: Getting current frame from Fusion...") + fusion_script_cmd = "'/Applications/Blackmagic Fusion 19/Fusion.app/Contents/Libraries/fuscript' -x 'print(\"[[\"..Fusion().CurrentComp.CurrentTime..\"]]\")'" + fusion_output = run_command(fusion_script_cmd, shell=True) + + # Extract the frame number from the output + match = re.search(r'\[\[(\d+)\]\]', fusion_output) + if not match: + print(f"Error: Could not parse frame number from Fusion output: {fusion_output}") + sys.exit(1) + + current_frame = int(match.group(1)) + print(f"Current frame: {current_frame}") + + # Step 4: Compute TRIM_IN and EXTEND_FIRST + print("\nStep 4: Computing TRIM_IN and EXTEND_FIRST...") + trim_in = 0 + extend_first = 0 + + if target_frame > current_frame: + trim_in = target_frame - current_frame + print(f"Target frame is AFTER current frame. Setting TRIM_IN to {trim_in}") + else: + extend_first = current_frame - target_frame + print(f"Target frame is BEFORE current frame. Setting EXTEND_FIRST to {extend_first}") + + # Step 5: Create the Fusion loader text and copy to clipboard + print("\nStep 5: Creating Fusion loader text and copying to clipboard...") + 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"}}''' + + try: + pyperclip.copy(fusion_text) + print("Text copied to clipboard:") + print(fusion_text) + except Exception as e: + print(f"Error copying to clipboard: {e}") + sys.exit(1) + + # Finally, activate Fusion but don't paste + print("\nActivating Fusion...") + activate_app("Fusion") + print("Script completed successfully!") + +if __name__ == "__main__": + main() diff --git a/readme.md b/readme.md index 75602bd48e247da3f1863ec551d41d564250efbd..1357e9aea40a21de719896a5e75c9ac2501eee49 100644 --- a/readme.md +++ b/readme.md @@ -1,13 +1,17 @@ # Clover's Creative Control -This is a set of tools to let additional hardware surfaces integrate with -creative applications on a Mac device. In addition to the primary keybinding -system, this project can be used as a library to use the control primitves -directly (either to build your own hardware integrations, or to control the -software). +This is a set of tools to let additional hardware devices integrate with +creative applications on a Mac device. Additionally, this repo contains a lot of +my own tools and scripts I use in the Music/Video creative processes. -These tools only work on macOS. I don't have interest in maintaining other -configurations. +In addition to the primary keybinding system and personal software +configurations, this project can be used as a library to use the control +primitives directly (either to build your own hardware integrations, or to +control the software). This can be done by installing this repo as a `pnpm` git +dependency in your project. + +**NOTE**: These tools only work on macOS. I don't have interest in maintaining +other configurations. ## Hardware