jedypod

fcpxml_to_nuke

Jun 6th, 2012
103
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 17.44 KB | None | 0 0
  1. '''
  2. FCP XML to Nuke
  3. v1.1 - Made support for Premiere FCPXML format more robust. Premiere FCP XML stores all sequences in the project, whereas
  4. Final Cut Pro's XML format stores only a single sequence. I added a box that lets you choose what sequence
  5. of the Premiere XML file to process, and it should work more reliably now.
  6. v1.0 - Initial release.
  7.  
  8. This script takes a Final Cut XML file with a single flattened video track, and builds Nuke scripts for each clip in the timeline.
  9. It is intended as a simple way to automate workflows between FCP/Premiere and Nuke.
  10. It creates a Nuke script with global first and last frame set, a frameRange node with the proper framerange, and a Write node
  11. set to the output path.
  12. There is an option for creating subdirectories for every Nuke script created. Handles are also an option.
  13. It can parse reel number and clip number from Red and Alexa footage, or can use the clip filename as the base naming for the output files.
  14.  
  15. This script was somewhat inspired by compflows.blogspot.com, but has been written from scratch and is a bit more flexible (although it only goes from XML->NukeScripts and not back at the moment).
  16. This has only been tested on OSX, but in theory should be cross-platform compatible. Comments and suggestions are welcome!
  17.  
  18.  
  19. # The menu.py example entry below adds this script in a folder called "Scripts" in your toolbar.
  20. import fcpxml_to_nuke
  21. nuke.toolbar('Nodes').addMenu('Scripts').addCommand('FCP XML to Nuke', 'fcpxml_to_nuke.process_xml()')
  22. '''
  23.  
  24. import nuke, os
  25. from xml.dom.minidom import parse
  26.  
  27. def process_xml():
  28.     '''
  29.     Imports an FCP XML file, locates each clip in the timeline on Track 1,
  30.     and for each clip, builds a nuke script for that file and puts it in the output directory specified
  31.  
  32.     New Features that would be nice to have:
  33.     Customized naming patterns based on reel/clip number.
  34.     Handle FCP XML from Premiere or FCP / FCPX ( Are there differences in the XML structure for these? )
  35.     Choose additional output naming and directory formatting patterns
  36.     '''
  37.  
  38.     # Build the Nuke Panel where locations and stuff is specified.
  39.     p = nuke.Panel("FCP XML Import")
  40.     xml_file = 'XML File to Process'
  41.     output_dir = 'Directory to Output Nuke Scripts' #"set dir for outputting nuke scripts"
  42.     subdirectories = 'Create subdirectories for each script?'
  43.     render_dir = 'Render Directory for All Write Nodes' #"set render dir for write nodes"
  44.     handle_length = "0 5 10 15 20 30 35 40"
  45.     clip_name_format = "Bypass RED Alexa"
  46.  
  47.     p.addFilenameSearch("FCP XML File", xml_file)
  48.     p.addBooleanCheckBox("Create subdirectories for each script", subdirectories)
  49.     p.addFilenameSearch("Output Directory", output_dir)
  50.     p.addFilenameSearch("Render Directory", render_dir)
  51.     p.addEnumerationPulldown("Handle Length", handle_length)
  52.     p.addEnumerationPulldown("Clip Name Format", clip_name_format)
  53.     p.setWidth(600)
  54.     if not p.show():
  55.         return
  56.  
  57.     # Assign vars from Nuke Panel user-entered data
  58.     xml_file    = p.value("FCP XML File")
  59.     output_dir  = p.value("Output Directory")
  60.     subdirectories = p.value("Create subdirectories for each script")
  61.     render_dir  = p.value("Render Directory")
  62.     handle_length = int( p.value("Handle Length") )
  63.     clip_name_format = p.value("Clip Name Format")
  64.  
  65.     # Create paths for render directory if it does not exist
  66.     if not os.path.isdir(render_dir):
  67.         os.mkdir(render_dir)
  68.     if not os.path.isdir(output_dir):
  69.         os.mkdir(output_dir)
  70.  
  71.  
  72.     ############################
  73.     # Begin Parsing XML File
  74.     ############################
  75.     dom = parse( xml_file )
  76.  
  77.     # Prompt user to choose which sequence to process, because Premiere's XML export includes all sequences.
  78.     # Much of this complexity is to handle sequences with spaces in their names, because the nuke.panel
  79.     # addEnumerationPulldown is space-demarcated. The logic below handles spaces in sequence names and allows the user
  80.     # to choose which sequence to process, and stores the sequence to process as a variable to access
  81.  
  82.     sequences = []
  83.     i = 0
  84.     # Makes a list of all sequence names
  85.     for seq in dom.getElementsByTagName('sequence'):
  86.         seqname = seq.getElementsByTagName('name')[0].firstChild.data
  87.         sequences.append( [seqname] )
  88.         # If a sequence name has space characters, replace them with underscores for the Nuke enumeration pulldown panel
  89.         if seqname.find(' ') != -1:
  90.             sequences[i].append( seqname.replace(' ', '_') )
  91.         i += 1
  92.     print "Sequences are: ", sequences
  93.     # ??? Do all of the below messy work to prepare for deciding between multiple sequences, including dealing with spaces in sequence names
  94.     # but only if there is more than one sequence in the XML file. If not, we'll just set the 'row' var to 0, and roll with that.
  95.     if len(sequences) > 1:
  96.         # Makes a " " demarcated string with all sequence names for the nuke enumeration pulldown panel
  97.         seq_enum = ''
  98.         i = 0
  99.         for seq in sequences:
  100.             if len(sequences[i]) == 1:
  101.                 seq_enum += sequences[i][0] + ' '
  102.             else:
  103.                 seq_enum += sequences[i][1] + ' '
  104.             i += 1
  105.  
  106.         # Create a Nuke panel for the user to choose which sequence to process
  107.         seq_panel = nuke.Panel("Choose Sequence To Process")
  108.         seq_panel.addEnumerationPulldown("Choose Sequence", seq_enum)
  109.         seq_panel.setWidth(400)
  110.         if not seq_panel.show():
  111.             return
  112.         chosen_sequence = seq_panel.value("Choose Sequence")
  113.  
  114.         # Gets the index of the chosen sequence in the list of sequences, stores the sequence XML object as a variable
  115.         for row, i in enumerate(sequences):
  116.                 try:
  117.                     column = i.index( chosen_sequence )
  118.                 except ValueError:
  119.                     continue
  120.                 break
  121.     else:
  122.         row = 0
  123.     chosen_seqobj = dom.getElementsByTagName('sequence')[row]
  124.     print "Chosen sequence is: ", chosen_seqobj.getElementsByTagName('name')[0].firstChild.data
  125.  
  126.     seq_res_x = int( chosen_seqobj.getElementsByTagName('format')[0].getElementsByTagName('width')[0].firstChild.data )
  127.     seq_res_y = int( chosen_seqobj.getElementsByTagName('format')[0].getElementsByTagName('height')[0].firstChild.data )
  128.     print "Sequence resolution is ", seq_res_x,"x",seq_res_y
  129.  
  130.     # Set optional effect parameters to False
  131.     timeremap_value = False
  132.     scale_value = False
  133.     x_move = False
  134.     y_move = False
  135.  
  136.     seq_clip_number = 1
  137.     track = chosen_seqobj.getElementsByTagName('track')[0]
  138.     for clip in track.getElementsByTagName('clipitem'):
  139.         masterclipid    = clip.getElementsByTagName('masterclipid')[0].firstChild.data
  140.         clip_name       = clip.getElementsByTagName("name")[0].firstChild.data
  141.         in_point        = int( clip.getElementsByTagName('in')[0].firstChild.data )
  142.         out_point       = int( clip.getElementsByTagName('out')[0].firstChild.data )
  143.         clip_duration   = int( clip.getElementsByTagName("duration")[0].firstChild.data )
  144.        
  145.         # Fetch the pathurl of the clip, which is stored in a seperate node in the master-clip
  146.         for path in dom.getElementsByTagName('pathurl'):
  147.             if path.firstChild.data.split('/')[-1] == clip_name:
  148.                 file_path = path.firstChild.data.split("file://localhost")[1].replace("%20", " ")
  149.         print clip_name, in_point, out_point, clip_duration, file_path
  150.  
  151.         # Get all effects applied to this clip
  152.         for effect in clip.getElementsByTagName('effect'):
  153.             effect_name = effect.childNodes[1].firstChild.data
  154.             if effect_name == 'Time Remap':
  155.                 # Loop through all parameters of the effect
  156.                 for param in effect.getElementsByTagName('parameter'):
  157.                     param_id = param.childNodes[1].firstChild.data
  158.                     if param_id == 'speed':
  159.                         timeremap_value = float( param.getElementsByTagName('value')[0].firstChild.data )
  160.                         print effect_name, param_id, timeremap_value
  161.  
  162.             if effect_name == 'Basic Motion':
  163.                 for param in effect.getElementsByTagName('parameter'):
  164.                     param_id = param.childNodes[1].firstChild.data
  165.                     if param_id == 'scale':
  166.                         scale_value = float( param.getElementsByTagName('value')[0].firstChild.data )
  167.                         print effect_name, param_id, scale_value
  168.                     if param_id == 'center':
  169.                         # !!! This is calculating the move from the sequence resolution center, not the clip resolution center, as it should.
  170.                         # Should this be done with a reformat node instead? This is a hard problem.
  171.                         x_move = seq_res_x * float( param.getElementsByTagName('value')[0].childNodes[1].firstChild.data )
  172.                         y_move = seq_res_y * float( param.getElementsByTagName('value')[0].childNodes[3].firstChild.data )
  173.                         print effect_name, param_id, x_move, y_move
  174.  
  175.        
  176.         # Gets the shot name, which is the formatted clip_name with clip# and reel#, with the sequence clipnumber.
  177.         # uses camera type (Red, Alexa, etc), and the clip_name string (the filename of the clip used in FCP)
  178.         # Also takes the seq_clip_number for returning the correct shot_name (the name that will be used to name the nuke script)
  179.         if clip_name_format == 'RED':
  180.             # This works for Red footage of format: A###_C###_RANDOMDATA
  181.             reel_number = clip_name.split('_')[0][1:]
  182.             clip_number = clip_name.split('_')[1][1:]
  183.  
  184.         if clip_name_format == 'Alexa':
  185.             # Alexa footage is A###C###_######_R####
  186.             reel_number = int(clip_name.split('C')[0][1:])
  187.             clip_number = int( clip_name.split('C')[1].split('_')[0] )
  188.  
  189.         if clip_name_format == 'Bypass':
  190.             shot_name = "%02d0_%s" %(seq_clip_number, os.path.splitext(clip_name)[0])
  191.         else:
  192.             # shot_name is the string that defines the name that the nuke script is saved to. seq_clip_number+0_A{reelnumber}_C{clipnumber}
  193.             shot_name = "%02d0_A%sC%s" %(seq_clip_number, reel_number, clip_number)
  194.  
  195.  
  196.         ############################
  197.         # Build Nuke Script
  198.         ############################
  199.        
  200.         # if the subdirectories checkbox is checked, set the output_shotdir to be a subdirectory named with the shot_name
  201.         if subdirectories:
  202.             output_shotdir = output_dir
  203.             output_shotdir = os.path.join(output_dir, shot_name)
  204.         else:
  205.             output_shotdir = output_dir
  206.         # If the output_shotdir does not exist, create it (auto-creates subdirectories)
  207.         if not os.path.isdir(output_shotdir):
  208.                 os.mkdir(output_shotdir)
  209.  
  210.         # Compute Handles and set first_frame and last_frame
  211.         first_frame = in_point - handle_length
  212.         last_frame = out_point-1 + handle_length
  213.         if timeremap_value:
  214.             first_frame = round(first_frame/(100/timeremap_value))
  215.             last_frame = round(last_frame/(100/timeremap_value))
  216.  
  217.         if seq_res_x == 1920 and seq_res_y == 1080:
  218.             fcp_xml_resolution = 'HD'
  219.         else:
  220.             fcp_xml_resolution = 'from_xml'
  221.  
  222.         #!!! This creates a nuke script by appending text to the .nk file instead of using nuke.nodeCopy(), which is slow and messy, and so that root node settings can be added.
  223.         # The strings that are written are triple-quoted. newlines are created with '\n'. {} chars in the string have to be doubled so as not to throw a KeyError
  224.         nuke_file = os.path.join(output_shotdir, "%s_v001.nk"%(shot_name))
  225.         nuke_script = open(nuke_file, 'a+')
  226.         # Create Root node
  227.         nuke_script.write('''Root {{\n inputs 0\n name {0}\n project_directory \"\[python \{{nuke.script_directory()\}}]\"\n first_frame {1}\n last_frame {2}\n format \"{3} {4} 0 0 {3} {4} 1 {5}\"\n proxy_type scale\n}}\n'''.format(nuke_file, first_frame, last_frame, seq_res_x, seq_res_y, fcp_xml_resolution))
  228.         # Create Read node
  229.         nuke_script.write('''Read {{\n inputs 0\n file {0}\n first 0\n last {1}\n frame_mode offset\n frame 1\n origlast {1}\n origset true\n name Read1\n selected true\n xpos -425\n ypos -40\n}}\n'''.format(file_path,clip_duration))
  230.         # Create FrameRange node
  231.         nuke_script.write('''FrameRange {{\n first_frame {0}\n last_frame {1}\n name FrameRange1\n label "\\[knob first_frame]-\\[knob last_frame]"\n selected true\n}}\n'''.format(first_frame, last_frame))
  232.         # Create a Transform node with pans and scales, if they exist
  233.         if  scale_value or x_move or y_move:
  234.             if not scale_value:
  235.                 scale_value = 1
  236.             if not x_move:
  237.                 x_move = 0
  238.             if not y_move:
  239.                 y_move = 0
  240.             nuke_script.write( '''Transform {{
  241. translate {{{0} {1}}}
  242. scale {2}
  243. center {{{3} {4}}}
  244. name Transform1
  245. selected true
  246. }}
  247. '''.format( x_move, y_move, scale_value/100, seq_res_x/2, seq_res_y/2 ) )
  248.         # Create TimeRemap if there is retiming on the clip
  249.         if timeremap_value:
  250.             nuke_script.write('''Group {{
  251. name RetimeFromFrame
  252. selected true
  253. addUserKnob {{20 Retime t "Retime From Frame Parameters"}}
  254. addUserKnob {{41 StartFrame l "SourceStart Frame" t "The source frame from which retiming starts. For example, if you have a clip that you are using a range from frames 200-300 in, and you want to retime that clip to be 50\% speed, you would set this to be 200. \\n\\nThis gizmo references the root.first_frame value to determine the \\\"in-point\\\" of the clip." T RetimeControls.StartFrame}}
  255. addUserKnob {{41 PlaybackSpeed l "Playback Speed" t "Retime speed as a fraction of one. That is, 0.5 = 50\% speed, 2 = 200\% speed." T RetimeControls.PlaybackSpeed}}
  256. }}
  257. Input {{
  258.  inputs 0
  259.  name Input1
  260.  xpos 0
  261. }}
  262. TimeOffset {{
  263.  time_offset {{{{-RetimeControls.StartFrame/RetimeScreen.timingSpeed}}}}
  264.  name Retime_TimeOffset
  265.  tile_color 0xff0000ff
  266.  xpos 0
  267.  ypos 132
  268. }}
  269. OFXuk.co.thefoundry.time.oflow_v100 {{
  270.  method Motion
  271.  timing Speed
  272.  timingFrame 1
  273.  timingSpeed {{{{RetimeControls.PlaybackSpeed}}}}
  274.  filtering Normal
  275.  warpMode Normal
  276.  correctLuminance false
  277.  automaticShutterTime false
  278.  shutterTime 0
  279.  shutterSamples 1
  280.  vectorDetail 0.2
  281.  smoothness 0.5
  282.  blockSize 6
  283.  Tolerances 0
  284.  weightRed 0.3
  285.  weightGreen 0.6
  286.  weightBlue 0.1
  287.  showVectors false
  288.  cacheBreaker false
  289.  name RetimeScreen
  290.  tile_color 0xff0000ff
  291.  selected true
  292.  xpos 0
  293.  ypos 156
  294. }}
  295. TimeOffset {{
  296.  time_offset {{{{root.first_frame}}}}
  297.  name GlobalStart_Offset
  298.  tile_color 0xff0000ff
  299.  xpos 0
  300.  ypos 180
  301. }}
  302. Output {{
  303.  name Output1
  304.  xpos 0
  305.  ypos 393
  306. }}
  307. NoOp {{
  308.  inputs 0
  309.  name RetimeControls
  310.  xpos -174
  311.  ypos 126
  312.  addUserKnob {{20 User}}
  313.  addUserKnob {{7 PlaybackSpeed l "Playback Speed" R 0 100}}
  314.  PlaybackSpeed {0}
  315.  addUserKnob {{3 StartFrame l "Start Frame" t "Offset video start frame"}}
  316.  StartFrame {1}
  317. }}
  318. end_group\n'''.format( timeremap_value/100, first_frame ))
  319.  
  320.         # Create Write node
  321.         nuke_script.write('''Write {{\n file {0}\n file_type mov\n codec apch\n fps 23.976\n checkHashOnRead false\n name Write1\n selected true\n}}\n'''.format('{0}_v001.mov'.format(os.path.join(render_dir, shot_name)) ) )
  322.         # Create Viewer node
  323.         nuke_script.write('''Viewer {\n name Viewer1\n selected true\n}\n''')
  324.         # Create Backdrop node
  325.         nuke_script.write('''BackdropNode {{\n inputs 0\n name BackdropNode1\n tile_color 0x26434dff\n label \"<img src=\\\"Read.png\\\"> Read Plate <br/><font size=1> {0} <br/> {1}-{2}<br/>{3} frame handles\"\n note_font_size 30\n selected true\n xpos -500\n ypos -150\n bdwidth 234\n bdheight 254\n}}\n'''.format(shot_name, in_point, out_point, handle_length))
  326.  
  327.  
  328.  
  329.  
  330.         seq_clip_number += 1
  331.         # Reset option effect parameters to false for next clip iteration
  332.         timeremap_value = False
  333.         scale_value = False
  334.         x_move = False
  335.         y_move = False
  336.         ### End of for loop which processes each clip in timeline.
  337.  
  338.  
  339.     nuke.message('All clips processed successfully!')
  340.     return
  341.  
  342.  
  343.  
  344. '''
  345.         ### ??? ALTERNATIVE METHOD FOR CREATING THE SCRIPT FILES
  346.         ### This approach uses the script that import_xml is executed from as a base for creating nodes, and then using nuke.nodeCopy() to 'paste' the data into each script file.
  347.         ### The approach I ended up using instead just uses python to format .nk scripts as text, inputting the variables where relevant. It is much faster and more efficient.
  348.         for node in nuke.allNodes():
  349.             node.setSelected(True)
  350.         nuke.nodeDelete()
  351.  
  352.         # Create Read node
  353.         read = nuke.createNode("Read")
  354.         read.knob("file").setValue(file_path)
  355.         read.knob("frame_mode").setValue('offset')
  356.         read.knob("frame").setValue('1')
  357.         read.knob("first").setValue(0)
  358.         read.knob("last").setValue(clip_duration)
  359.  
  360.         # Create a NoOp node to hold shot info
  361.         #!!! This is not needed
  362.         #shotInfo = nuke.createNode('NoOp')
  363.         #shotInfo.knob('name').setValue(shot_name+"_info")
  364.         #shotInfo.addKnob( nuke.String_Knob("The original name of the clip", "clip_name", clip_name) )
  365.         #shotInfo.addKnob( nuke.String_Knob("Base name of the nuke script", "shot_name", shot_name) )
  366.  
  367.         # Create FrameRange node
  368.         frame_range = nuke.createNode("FrameRange")
  369.         frame_range.knob('label').setValue('[knob first_frame]-[knob last_frame]')
  370.         frame_range.knob('first_frame').setValue( in_point - handle_length )
  371.         frame_range.knob('last_frame').setValue( out_point-1 + handle_length )
  372.        
  373.         # Create Write node
  374.         write = nuke.createNode("Write")
  375.         write.knob('file').setValue('{0}{1}_v001.mov'.format(render_dir, shot_name))
  376.         #write.knob("file_type").setValue("mov")
  377.         #write.knob("mov.codec").setValue("apch")
  378.         #write.knob("mov.fps").setValue("23.976")
  379.  
  380.         # Create Viewer Node
  381.         nuke.createNode('Viewer')
  382.  
  383.         # Informational Backdrop Node
  384.         bd_node = nuke.createNode("BackdropNode")
  385.         bd_node.knob("tile_color").setValue(0x26434dff)
  386.         bd_node.knob("note_font_size").setValue(30)
  387.         bd_node.knob("bdwidth").setValue(234)
  388.         bd_node.knob("bdheight").setValue(254)
  389.         bd_node.knob("label").setValue('<img src=\"Read.png\"> Read Plate <br/><font size=1> %s <br/> %s-%s'%(shot_name,int(in_point),int(out_point)))
  390.  
  391.         # Set root script values
  392.         #nuke.toNode('root').knob('project_directory').setValue('[python {nuke.script_directory()}]')
  393.         #nuke.toNode('root').knob('first_frame').setValue( in_point-handle_length)
  394.         #nuke.toNode('root').knob('first_frame').setValue( (out_point-1) + handle_length)
  395.         #nuke.toNode('root').knob('format').setValue('HD')
  396.  
  397.         # Select all created nodes and copy them into a new script
  398.         for node in nuke.allNodes():
  399.             node.setSelected(True)
  400.         nuke.nodeCopy(nuke_file)
  401.         '''
Advertisement
Add Comment
Please, Sign In to add comment