""" FaceFX Studio Python Scripting Interface. This module provides read access to a multitude of information about the current actor loaded in FaceFX Studio. The module also provides a wrapper around executing FaceFX commands, to get data back into FaceFX Studio once it has been modified in Python. related modules: FxAnimation -- provides wrappers around animation data from Studio FxAudio -- provides a wrapper around the selected audio in Studio FxFaceGraph -- provides wrappers around Face Graph data from Studio FxPhonemes -- provides wrappers around phoneme data from Studio FxRandom -- provides random number generating functions FxUtil -- provides a few utility functions classes: FaceFxException -- the error raised if any function fails Owner: Jamie Redmond Copyright (c) 2002-2011 OC3 Entertainment, Inc. """ import httplib import urllib import threading import xml.dom.minidom import warnings import wx class FaceFXError(Exception): """ The error produced if any function fails in the FxStudio module. instance variables: message - a human-readable description of what went wrong. """ def __init__(self, msg): self.message = msg def __str__(self): return self.message class FaceFXDeprecatedWarning(DeprecationWarning): """ The warning produced if any function in FaceFX modules is deprecated. instance variables: message - a human-readable description of what went wrong. """ def __init__(self, msg): self.message = msg def __str__(self): return self.message #------------------------------------------------------------------------------- # These functions issue commands to FaceFX Studio but use a cleaner and easier # to use syntax than calling FxStudio.issueCommand() directly. #------------------------------------------------------------------------------- def echo(msg): """ Deprecated, but here for compatibility with old scripts. Use msg() instead.""" if not issueCommand('print -message "{0}"'.format(msg)): raise FaceFXError('FxStudio.echo() failed!') # Note that there is also a Unicode version: FxStudio.msgW(). def msg(msg): """ Print msg to FaceFX Studio's console.""" if not issueCommand('print -message "{0}"'.format(msg)): raise FaceFXError('FxStudio.msg() failed!') # Note that there is also a Unicode version: FxStudio.warnW(). def warn(msg): """ Print msg to FaceFX Studio's console (as a warning).""" if not issueCommand('warn -message "{0}"'.format(msg)): raise FaceFXError('FxStudio.warn() failed!') # Note that there is also a Unicode version: FxStudio.errorW(). def error(msg): """ Print msg to FaceFX Studio's console (as an error).""" if not issueCommand('error -message "{0}"'.format(msg)): raise FaceFXError('FxStudio.error() failed!') # Note that there is also a Unicode version: FxStudio.devW(). def dev(msg): """ Print msg to FaceFX Studio's developer console (as a developer trace). Note that the developer console is only visible in special developer builds.""" if not issueCommand('dev -message "{0}"'.format(msg)): raise FaceFXError('FxStudio.dev() failed!') def msgBox(msg): """ Displays an informational message box in FaceFX Studio or prints a message in command line mode.""" displayMessageBox(msg, 'info') def warnBox(msg): """ Displays a warning message box in FaceFX Studio or prints a message in command line mode.""" displayMessageBox(msg, 'warning') def errorBox(msg): """ Displays an error message box in FaceFX Studio or prints a message in command line mode.""" displayMessageBox(msg, 'error') def setConsoleVariable(cvarName, cvarValue): """ Sets (or creates) a console variable in FaceFX Studio. If the variable does not exist, it is created. Console variables can be queried for their current value by using FxStudio.getConsoleVariable(cvar), which will return a string with the current value or None if the variable does not exist. Note that the default value of the console variable can be queried by using FxStudio.getConsoleVariableDefault(cvar) which behaves exactly like FxStudio.getConsoleVariable(). FxUtil.isConsoleVariableSetToDefault() is a convenient helper method. keywordArguments: cvarName -- the name of the console variable cvarValue -- the value of the console variable """ if None == getConsoleVariableImpl(cvarName): print 'cvar {0} does not exist and will be created automatically with the value {1}'.format(cvarName, cvarValue) retCode = issueCommand('set -name "{0}" -value "{1}"'.format( cvarName, cvarValue)) if False == retCode: raise FaceFXError('Console variable {0} could not be created.'.format( cvarName)) def setConsoleVariableFast(cvarName, cvarValue): """ Sets a console variable in FaceFX Studio without any error checking or command execution. Useful for progress display console variables or when you know the console variable exists and don't want to issue a command through the command system to set it. keywordArguments: cvarName -- the name of the console variable cvarValue -- the value of the console variable """ setConsoleVariableFastImpl(cvarName, str(cvarValue)) def getConsoleVariable(cvarName): """ Returns a console variable value in FaceFX Studio. If the console variable does not exist, a FaceFXError is raised. """ retVal = getConsoleVariableImpl(cvarName) if None == retVal: raise FaceFXError('cvar {0} does not exist.'.format(cvarName)) return retVal def getConsoleVariableDefault(cvarName): """Returns a console variable default value in FaceFX Studio. If the console variable does not exist, a FaceFXError is raised. """ retVal = getConsoleVariableDefaultImpl(cvarName) if None == retVal: raise FaceFXError('cvar {0} does not exist.'.format(cvarName)) return retVal def getConsoleVariableAsSwitch(cvarName): """Returns a console variable value as a switch value. Returns True if the switch is enabled and False if it is not. If the console variable does not exist, a FaceFXError is raised. """ retVal = getConsoleVariableAsSwitchImpl(cvarName) if None == retVal: raise FaceFXError('cvar {0} does not exist.'.format(cvarName)) return retVal def getDirectory(dir): """Returns the specified directory.""" varmap = {'app' : 'g_appdirectory', 'user' : 'g_userdirectory', 'settings' : 'g_settingsdirectory', 'log' : 'g_logdirectory', 'template' : 'g_templatedirectory', 'scripts' : 'g_scriptsdirectory', 'appscripts' : 'g_appscriptsdirectory', 'analysisactors' : 'g_analysisactorsdirectory', 'appanalysisactors' : 'g_appanalysisactorsdirectory', 'analysislanguages' : 'g_analysislanguagesdirectory', 'appanalysislanguages' : 'g_appanalysislanguagesdirectory', 'themes' : 'g_themesdirectory', 'appthemes' : 'g_appthemesdirectory'} try: return getConsoleVariable(varmap[dir.lower()]) except(KeyError): raise FaceFXError('{0} is not a valid directory for FxStudio.getDirectory()'.format(dir)) def getAppDirectory(): """Returns the FaceFX Studio application directory.""" warnings.warn('getAppDirectory() has been deprecated. Please update your code to use getDirectory(\'app\') instead.', FaceFXDeprecatedWarning) return getConsoleVariable("g_appdirectory") def getUserDirectory(): """Returns the user directory.""" warnings.warn('getUserDirectory() has been deprecated. Please update your code to use getDirectory(\'user\') instead.', FaceFXDeprecatedWarning) return getConsoleVariable("g_userdirectory") def getSDKVersion(): """Returns the FaceFX SDK version.""" return getConsoleVariable("g_sdkversion") def getLicenseeName(): """Returns the licensee name.""" return getConsoleVariable("g_licenseename") def getLicenseeProjectName(): """Returns the licensee project name.""" return getConsoleVariable("g_licenseeprojectname") def getAppIconPath(): """Returns the application icon path.""" return getDirectory('app') + 'res\\FxStudioApp.ico' # Creates a new FaceFX Actor in FaceFX Studio. def createNewActor(actorName): """ Creates a new FaceFX Actor in FaceFX Studio. keyword arguments: actorName -- the desired name for the actor. """ if not issueCommand('newActor -name "{0}"'.format(actorName)): raise FaceFXError('Actor could not be created.') def loadActor(actorFile): """ Loads the specified .facefx file (FaceFX Actor) into FaceFX Studio. """ if not issueCommand('loadActor -file "{0}"'.format(actorFile)): raise FaceFXError('Actor in file {0} could not be loaded'.format( actorFile)) def closeActor(): """Closes the currently loaded .facefx file (FaceFX Actor) in FaceFX Studio. """ if not issueCommand("closeActor"): raise FaceFXError('Actor could not be closed.') def saveActor(actorFile): """Saves the currently loaded .facefx file (FaceFX Actor) in FaceFX Studio. """ if not issueCommand('saveActor -file "{0}"'.format(actorFile)): raise FaceFXError('Could not save actor to {0}'.format(actorFile)) def selectAnimation(groupName, animName): """Selects the specified animation in the actor loaded in FaceFX Studio keyword arguments: groupName -- name of the animation group the animation resides in animName -- name of the animation to select. """ if not issueCommand('select -type "animgroup" -names "{0}"'.format(groupName)): raise FaceFXError('Could not select groupName "{0}"'.format(groupName)) if not issueCommand('select -type "anim" -names "{0}"'.format(animName)): raise FaceFXError('Could not select animName "{0}"'.format(animName)) # Sets the current time in FaceFX Studio. def setCurrentTime(time): """Sets the current time in FaceFX Studio.""" if not issueCommand('currentTime -new {0}'.format(time)): raise FaceFXError('Setting current time failed.') #------------------------------------------------------------------------------- # These functions are helper functions that return additional data about the # FaceFX Actor that is currently loaded in FaceFX Studio. #------------------------------------------------------------------------------- def getSelectedAnimGroupName(): """Returns the name of the currently selected animation group in FaceFX Studio.""" return getSelectedAnimation()[0] def getSelectedAnimName(): """Returns the name of the currently selected animation in FaceFX Studio.""" return getSelectedAnimation()[1] def getNumFaceGraphNodes(): """Returns the number of Face Graph nodes contained in the actor loaded in FaceFX Studio.""" return len(getFaceGraphNodeNames()) def getNumAnimationGroups(): """Returns the number of animation groups contained in the actor loaded in FaceFX Studio.""" return len(getAnimationNames()) def getNumAnimationsInGroup(groupName): """Returns the number of animations in the specified group contained in the actor loaded in FaceFX Studio.""" numAnimations = 0 animations = getAnimationNames() for animationGroup in animations: if animationGroup[0] == groupName: numAnimations = len(animationGroup[1]) return numAnimations def getNumTotalAnimations(): """Returns the total number of animations contained in the actor loaded in FaceFX Studio.""" numTotalAnimations = 0 animations = getAnimationNames() for animationGroup in animations: numTotalAnimations += len(animationGroup[1]) return numTotalAnimations #------------------------------------------------------------------------------- # Perform an update check. #------------------------------------------------------------------------------- def checkForUpdates(is_startup_check): UpdateChecker(getConsoleVariable('g_productname'), getConsoleVariable('g_productversion'), is_startup_check).start() #------------------------------------------------------------------------------- # These functions are called internally by FaceFX Studio. Do not remove them # but feel free to modify them if you'd like to alter their behaviour; just # make sure to adhere to the specifications for each one. #------------------------------------------------------------------------------- def shouldCompressAnimation(groupName, animName): """This is called internally by FaceFX Studio to determine if the animation should be compressed or not. Feel free to alter this check to do whatever you want, just make sure that you set the po_should_compress_animation hidden console variable to either yes or no. keyword arguments: groupName -- name of the animation group the animation resides in animName -- name of the animation to compress. """ # By default all animations should be compressed. setConsoleVariableFast('po_should_compress_animation', 'yes') try: animDict = getAnimPythonDictionary(groupName, animName) except Exception, e: raise FaceFXError('{0}'.format(e)) if animDict is not None: # If the animation has a Python dictionary, check to see if there # is a settings/compress key. try: shouldCompress = animDict['settings/compress'] if type(shouldCompress) is bool: # There was a settings/compress key and it had an object of type bool, so use it for the setting. # Note that since the default above is 'yes' the only state we care about here is False so if # the value is False set the console variable to 'no'. if shouldCompress is False: setConsoleVariableFast('po_should_compress_animation', 'no') else: # There was a settings/compress key but it was not a bool type! raise FaceFXError('shouldCompressAnimation("{0}", "{1}"): the animation has a Python dictionary with a settings/compress key but the object type is not bool!'.format(groupName, animName)) except KeyError: # If the dictionary does not contain a settings/compress key # simply ignore the error telling us that the key does not # exist. pass except Exception, e: # Any other error is pretty serious. raise FaceFXError('{0}'.format(e)) #------------------------------------------------------------------------------- # These functions are deprecated completely and throw when called. #------------------------------------------------------------------------------- def registerCallback(callback_name, callable_obj): raise FaceFXError('registerCallback() has been deprecated. Please update your code to use connectSignal().') def unregisterCallback(callback_name): raise FaceFXError('unregisterCallback() has been deprecated. Please update your code to use disconnectSignal().') def getCallback(callback_name): raise FaceFXError('getCallback() has been deprecated.') # Defines a menu in FaceFX Studio. Menus can be added as submenu items to other # menus, or docked into the main menu bar in FaceFX Studio via MenuBarMenu. class Menu: def __init__(self): self._menu = _new_Menu() self._ownsmenu = True self.bindings = [] def __del__(self): # This iterates through a *copy* of self.bindings since we will be # removing items in a loop. for binding in self.bindings[:]: self.unbind(binding[0], binding[1]) if self._ownsmenu: _delete_Menu(self._menu) self._menu = None def appendItem(self, id, caption, enabled_icon_path='', disabled_icon_path='', enabled_icon=None, disabled_icon=None): # Pass the icon objects if both are non-None. if enabled_icon is not None and disabled_icon is not None: _Menu_appendItem(self._menu, id, caption, enabled_icon=enabled_icon, disabled_icon=disabled_icon) else: # If both icons are None, fall back on the icon paths. if enabled_icon is None and disabled_icon is None: # Pass in the icon paths if both are non-default. if enabled_icon_path is not '' and disabled_icon_path is not '': _Menu_appendItem(self._menu, id, caption, enabled_icon_path, disabled_icon_path) else: # If both icon paths are default, don't pass them. if enabled_icon_path is '' and disabled_icon_path is '': _Menu_appendItem(self._menu, id, caption) else: # Otherwise one is default and the other is not, so let that case trigger the # exception inside _Menu_appendItem to display the appropriate error message # to the user. _Menu_appendItem(self._menu, id, caption, enabled_icon_path) else: # Otherwise one is None and the other is not, so let that case trigger the # exception inside _Menu_appendItem to display the appropriate error message # to the user. _Menu_appendItem(self._menu, id, caption, enabled_icon=enabled_icon) def appendSubmenu(self, id, caption, submenu): _Menu_appendSubmenu(self._menu, id, caption, submenu._menu) submenu._ownsmenu = False def appendSeparator(self): _Menu_appendSeparator(self._menu) def enableItem(self, id, enabled): _Menu_enableItem(self._menu, id, enabled) def checkItem(self, id, checked): _Menu_checkItem(self._menu, id, checked) def bind(self, handler, id): # Don't allow duplicate bindings. if 0 == self.bindings.count((handler, id)): getMainWindow().Bind(wx.EVT_MENU, handler=handler, id=id) self.bindings.append((handler, id)) def unbind(self, handler, id): # Since we don't allow duplicate bindings, make sure there are none # before unbinding. if 1 == self.bindings.count((handler, id)): getMainWindow().Unbind(wx.EVT_MENU, handler=handler, id=id) self.bindings.remove((handler, id)) # Special type of menu that is docked in the main menu bar of FaceFX Studio. class MenuBarMenu(Menu): def __init__(self, caption): Menu.__init__(self) self.caption = caption _addMenuToMenuBar(self._menu, self.caption) def __del__(self): _removeMenuFromMenuBar(self.caption) Menu.__del__(self) #------------------------------------------------------------------------------- # Update checking infrastructure. #------------------------------------------------------------------------------- class UpdateAvailableDialog(wx.Dialog): def __init__(self, update_info, is_startup_check): self.update_type = update_info['type'] self.product = update_info['product'] self.current_version = update_info['current_version'] self.update_version = update_info['update_version'] self.update_url = update_info['info_url'] title = '' bold_text = '' if self.update_type == 'update': title = 'Update Available' bold_text = 'A free update of {0} is available!'.format(self.product) else: title = 'Upgrade Available' bold_text = 'An upgrade of {0} is available for purchase!'.format(self.product) wx.Dialog.__init__(self, getMainWindow(), wx.ID_ANY, title, style=wx.CAPTION) main_sizer = wx.BoxSizer(wx.VERTICAL) bmp_and_text_sizer = wx.BoxSizer(wx.HORIZONTAL) bmp = wx.StaticBitmap(self) bmp.SetIcon(wx.Icon(getAppIconPath(), wx.BITMAP_TYPE_ICO)) bmp_and_text_sizer.Add(bmp, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 5) text_sizer = wx.BoxSizer(wx.VERTICAL) bold_line = wx.StaticText(self, wx.ID_ANY, bold_text) bold_line.SetFont(wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD)) text_sizer.Add(bold_line, 0, wx.ALL, 5) text_sizer.Add(wx.StaticText(self, wx.ID_ANY, '{0} {1} is now available (you have {2}).'.format(self.product, self.update_version, self.current_version)), 0, wx.ALL, 5) bmp_and_text_sizer.Add(text_sizer, 0, wx.ALL, 5) main_sizer.Add(bmp_and_text_sizer, 0, wx.ALL, 5) main_sizer.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.EXPAND | wx.ALL, 5) button_sizer = wx.BoxSizer(wx.HORIZONTAL) if is_startup_check: button_sizer.Add(wx.Button(self, 1, 'Skip This Version'), 0, wx.ALL | wx.ALIGN_LEFT, 5) self.Bind(wx.EVT_BUTTON, self.on_skip_this_version, id=1) button_sizer.AddStretchSpacer() # The Remind Me Later button intentionally has the id wx.ID_OK because it does nothing. button_sizer.Add(wx.Button(self, wx.ID_OK, 'Remind Me Later'), 0, wx.ALL | wx.ALIGN_RIGHT, 5) else: button_sizer.AddStretchSpacer() button_sizer.Add(wx.Button(self, wx.ID_OK, 'Close'), 0, wx.ALL | wx.ALIGN_RIGHT, 5) more_information_button = wx.Button(self, 2, 'More Information...') button_sizer.Add(more_information_button, 0, wx.ALL | wx.ALIGN_RIGHT, 5) self.Bind(wx.EVT_BUTTON, self.on_more_information, id=2) main_sizer.Add(button_sizer, 0, wx.ALL | wx.EXPAND, 0) more_information_button.SetFocus() self.SetSizerAndFit(main_sizer) self.Layout() self.CentreOnScreen() def on_skip_this_version(self, event): if self.update_type == 'update': setConsoleVariable('g_skipupdateversion', self.update_version) else: setConsoleVariable('g_skipupgradeversion', self.update_version) self.EndModal(wx.ID_OK) def on_more_information(self, event): import webbrowser webbrowser.open(self.update_url) self.EndModal(wx.ID_OK) class UpToDateDialog(wx.Dialog): def __init__(self, product, current_version): wx.Dialog.__init__(self, getMainWindow(), wx.ID_ANY, '', style=wx.CAPTION) main_sizer = wx.BoxSizer(wx.VERTICAL) bmp_and_text_sizer = wx.BoxSizer(wx.HORIZONTAL) bmp = wx.StaticBitmap(self) bmp.SetIcon(wx.Icon(getAppIconPath(), wx.BITMAP_TYPE_ICO)) bmp_and_text_sizer.Add(bmp, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 5) text_sizer = wx.BoxSizer(wx.VERTICAL) bold_line = wx.StaticText(self, wx.ID_ANY, 'You\'re up-to-date!') bold_line.SetFont(wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD)) text_sizer.Add(bold_line, 0, wx.ALL, 5) text_sizer.Add(wx.StaticText(self, wx.ID_ANY, '{0} {1} is the most recent version available.'.format(product, current_version)), 0, wx.ALL, 5) bmp_and_text_sizer.Add(text_sizer, 0, wx.ALL, 5) main_sizer.Add(bmp_and_text_sizer, 0, wx.ALL, 5) main_sizer.Add(wx.StaticLine(self, wx.ID_ANY), 0, wx.EXPAND | wx.ALL, 5) button_sizer = wx.BoxSizer(wx.HORIZONTAL) ok_button = wx.Button(self, wx.ID_OK, 'OK') button_sizer.Add(ok_button, 0, wx.ALL, 5) main_sizer.Add(button_sizer, 0, wx.ALL | wx.ALIGN_RIGHT, 0) ok_button.SetFocus() self.SetSizerAndFit(main_sizer) self.Layout() self.CentreOnScreen() def DisplayUpdateCheckResults(product, current_version, update_info, upgrade_info, is_startup_check): if update_info != None and ((is_startup_check and getConsoleVariable('g_skipupdateversion') != update_info['update_version']) or not is_startup_check): dlg = UpdateAvailableDialog(update_info, is_startup_check) dlg.ShowModal() dlg.Destroy() if upgrade_info != None and ((is_startup_check and getConsoleVariable('g_skipupgradeversion') != upgrade_info['update_version']) or not is_startup_check): dlg = UpdateAvailableDialog(upgrade_info, is_startup_check) dlg.ShowModal() dlg.Destroy() if update_info == None and upgrade_info == None and is_startup_check == False: dlg = UpToDateDialog(product, current_version) dlg.ShowModal() dlg.Destroy() def DisplayUpdateCheckError(e, is_startup_check): if not is_startup_check: error('[Update Check Error]: {0}'.format(e)) wx.MessageBox('An error was encountered while checking for updates. Please check the error console for details.', 'Error', wx.ICON_EXCLAMATION) class UpdateChecker(threading.Thread): def __init__(self, product_name, product_version, is_startup_check): threading.Thread.__init__(self) self.product = product_name self.current_version = product_version self.is_startup_check = is_startup_check def run(self): try: lsconnection = httplib.HTTPSConnection("license.facefx.com") url = "/STATELESS/UPDATES/?name=" + self.product + "&ver=" + self.current_version lsconnection.request("GET", urllib.quote(url, safe="%/:=&?~#+!$,;'@()*[]")) response = lsconnection.getresponse() if response.status != 200: wx.CallAfter(DisplayUpdateCheckError, 'Request Failed! The response status was: {0}'.format(response.status), self.is_startup_check) else: data = response.read() xmldoc = xml.dom.minidom.parseString(data) response = xmldoc.getElementsByTagName('response') status = response[0].attributes['status'].value if status != 'success': message = response[0].getElementsByTagName('message')[0].childNodes[0].data wx.CallAfter(DisplayUpdateCheckError, 'Request Failed! The server response was: {0}'.format(message), self.is_startup_check) else: update = response[0].getElementsByTagName('update') update_info = None if len(update[0].childNodes) > 0: update_info = {} update_info['type'] = 'update' update_info['product'] = update[0].getElementsByTagName('product')[0].childNodes[0].data update_info['current_version'] = self.current_version update_info['update_version'] = update[0].getElementsByTagName('version')[0].childNodes[0].data update_info['info_url'] = update[0].getElementsByTagName('info_url')[0].childNodes[0].data upgrade = response[0].getElementsByTagName('upgrade') upgrade_info = None if len(upgrade[0].childNodes) > 0: upgrade_info = {} upgrade_info['type'] = 'upgrade' upgrade_info['product'] = upgrade[0].getElementsByTagName('product')[0].childNodes[0].data upgrade_info['current_version'] = self.current_version upgrade_info['update_version'] = upgrade[0].getElementsByTagName('version')[0].childNodes[0].data upgrade_info['info_url'] = upgrade[0].getElementsByTagName('info_url')[0].childNodes[0].data wx.CallAfter(DisplayUpdateCheckResults, self.product, self.current_version, update_info, upgrade_info, self.is_startup_check) except Exception, e: wx.CallAfter(DisplayUpdateCheckError, 'Request Failed! Exception: {0}'.format(e), self.is_startup_check)