#------------------------------------------------------------------------------- # Funnel Phonemes - Reduce the phonemes to ones that impact mouth movement # This is a technique taught in Jason Osipa's "Stop Starring" book. The Tongue # Movement is ignored. To create tongue movement, consider using an analysis # actor that outputs curves for the tongue. # # Owner: Doug Perkowski # # Copyright (c) 2002-2011 OC3 Entertainment, Inc. #------------------------------------------------------------------------------- from FxStudio import * from FxPhonemes import * import re # This class transforms the phonemes into a string in the format: # : : # Example: # SIL:0.0 W:0.0399999991059 EH:0.140000000596 L:0.219999998808 K:0.25 # Regular expressions can than be performed on the string to modify/merge phonemes # The string is then re-parsed back into a class PhonemeListModifier: def __init__(self): self.phonList = FxPhonemes.PhonemeWordList(getSelectedAnimGroupName(), getSelectedAnimName()) print str(len(phonList.phonemes)) + " original phonemes." self.pString = "" self.pStringPrior = "" self.pStringPost = "" timeRange = getVisibleTimeRange() for phoneme in phonList.phonemes: if phoneme.startTime < timeRange[0]: self.pStringPrior += " " + FxPhonemes.PHONEME_REGISTRY.entries[phoneme.phonemeId].facefxCoding + ":" + str(phoneme.startTime) + " " elif phoneme.endTime > timeRange[1]: self.pStringPost += " " + FxPhonemes.PHONEME_REGISTRY.entries[phoneme.phonemeId].facefxCoding + ":" + str(phoneme.startTime) + " " else: self.pString += " " + FxPhonemes.PHONEME_REGISTRY.entries[phoneme.phonemeId].facefxCoding + ":" + str(phoneme.startTime) + " " # Regular expressions are applied to the phoneme string here. def ModifyString(self): #Simple replacements so that mapping self.pString = re.sub("\s(P|B|M):"," B:", self.pString); self.pString = re.sub("\s(F|V):"," V:", self.pString); self.pString = re.sub("\s(T|D|L|N|NG|TS):"," T:", self.pString); self.pString = re.sub("\s(K|G|RU|CX|X|GH):"," K:", self.pString); self.pString = re.sub("\s(TH|DH):"," TH:", self.pString); self.pString = re.sub("\s(S|Z):"," S:", self.pString); self.pString = re.sub("\s(E|EN):"," E:", self.pString); self.pString = re.sub("\s(A|AA|AAN):"," A:", self.pString); self.pString = re.sub("\s(AO|AON|O|ON|UW|EU|OE|OEN|UU|UH|OY|OW):"," UW:", self.pString); self.pString = re.sub("\s(AH|IH|AX|UX|AE|EY|AW|AY):"," IH:", self.pString); self.pString = re.sub("\s(ER|AXR|EXR):"," ER:", self.pString); #Here is the bulk of the funneling. Remove consonants that don't move the mouth much. We are ignoring the effect of the tongue here. #PBMFV are the importnat ones not to funnel. W to a certain extent as wewll. self.pString = re.sub(r" (?PIY|AH|IH|AX|UX|AE|EY|AW|AY|AO|AON|O|ON|UW|EU|OE|OEN|UU|UH|OY|OW|E|EN|ER|AXR|EXR|EH|A|AA|AAN):(?P\d+.\d+)\W+(?PT|D|L|N|NG|TS|K|G|RU|CX|X|GH|S|Z|TH|DH):\d+.\d+ ", " \g:\g ", self.pString); # This regular expression matches any given repeated phoneme, and replaces it with just one phoneme of the same class. self.pString = re.sub(r" (?P\w+):(?P\d+.\d+)\W+(?P=phoneme1):\d+.\d+ ", " \g:\g ", self.pString); # We only run this rule if we want to funnel further while the above don't do anything numOriginalPhonemes = len(self.phonList.phonemes) if self.pString.count(":") == numOriginalPhonemes: # This is the same rule reversed, so that we keep the second phoneme (the vowel). self.pString = re.sub(r" (?PT|D|L|N|NG|TS|K|G|RU|CX|X|GH|S|Z|TH|DH):(?P\d+.\d+)\W+(?PIY|AH|IH|AX|UX|AE|EY|AW|AY|AO|AON|O|ON|UW|EU|OE|OEN|UU|UH|OY|OW|E|EN|ER|AXR|EXR|EH|A|AA|AAN):\d+.\d+ ", " \g:\g ", self.pString); # This function parses the phoneme string and modifies the cached phonList appropriately def FinalizePhonWordList(self): phonemeLookup = {} i = 0 for entry in FxPhonemes.PHONEME_REGISTRY.entries: phonemeLookup[entry.facefxCoding] = i i+=1 finalEndTime = 0 if len(phonList.phonemes) > 0: finalEndTime = phonList.phonemes[len(phonList.phonemes)-1].endTime self.phonList.phonemes = [] recombined = self.pStringPrior + self.pString + self.pStringPost phonemes = re.split("\s+", recombined) #The first result is an empty string and we need to "look back" one phoneme, so start the index at 2 pIndex = 2 while pIndex < len(phonemes)-1: info = re.split(":", phonemes[pIndex-1]) info2 = re.split(":", phonemes[pIndex]) phonType = info[0] startTime = info[1] endTime = info2[1] self.phonList.phonemes.append(Phoneme((phonemeLookup[phonType], float(startTime), float(endTime)))) pIndex+=1 #get the last phoneme info2 = re.split(":", phonemes[pIndex-1]) phonType = info2[0] startTime = info2[1] endTime = finalEndTime self.phonList.phonemes.append(Phoneme((phonemeLookup[phonType], float(startTime), float(endTime)))) print str(len(self.phonList.phonemes)) + " reduced phonemes." # Using the cached phonList, delete the list in Studio and recreate it here. # The Words are combined at this stage. def UpdateStudio(self): issueCommand('batch'); issueCommand('phonList -clear;'); phonemeIndex = 0 wordIndex = 0 word = "" for phoneme in self.phonList.phonemes: issueCommand('phonList -append -phoneme "%s" -startTime "%f" -endTime "%f";'%(FxPhonemes.PHONEME_REGISTRY.entries[phoneme.phonemeId].facefxCoding, phoneme.startTime, phoneme.endTime)) if wordIndex < len(phonList.words): if phoneme.endTime - phonList.words[wordIndex].endTime > .001: # If we have passed a word boundary without finding an equivalent phoneme boundry, then the phoneme boundary # was eliminated, and we join the words. if wordIndex + 1 < len(phonList.words): wordIndex+=1 word = word + " " + phonList.words[wordIndex].word elif word != "": issueCommand('phonList -group -startIndex "%d" -endIndex "%d" -wordText "%s";'%(wordStartIndex, phonemeIndex, word)) if abs(phoneme.startTime - phonList.words[wordIndex].startTime) < .001: word = phonList.words[wordIndex].word wordStartIndex = phonemeIndex if abs(phoneme.endTime - phonList.words[wordIndex].endTime) < .001: issueCommand('phonList -group -startIndex "%d" -endIndex "%d" -wordText "%s";'%(wordStartIndex, phonemeIndex, word)) word = "" wordIndex+=1 phonemeIndex += 1 issueCommand('execBatch -editedcurves'); phonList = FxPhonemes.PhonemeWordList(getSelectedAnimGroupName(), getSelectedAnimName()) if phonList.getNumPhonemes() == 0: raise RuntimeError, 'No phonemes were present, or no animation was selected, or no actor was loaded.' phonListMod = PhonemeListModifier() phonListMod.ModifyString(); phonListMod.FinalizePhonWordList() phonListMod.UpdateStudio()