135 lines
7.2 KiB
Python
Executable File
135 lines
7.2 KiB
Python
Executable File
#-------------------------------------------------------------------------------
|
|
# 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:
|
|
# <phoneme>:<start-time> <phoneme>:<start-time>
|
|
# 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" (?P<phoneme1>IY|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<time1>\d+.\d+)\W+(?P<phoneme2>T|D|L|N|NG|TS|K|G|RU|CX|X|GH|S|Z|TH|DH):\d+.\d+ ", " \g<phoneme1>:\g<time1> ", 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<phoneme1>\w+):(?P<time1>\d+.\d+)\W+(?P=phoneme1):\d+.\d+ ", " \g<phoneme1>:\g<time1> ", 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" (?P<phoneme1>T|D|L|N|NG|TS|K|G|RU|CX|X|GH|S|Z|TH|DH):(?P<time1>\d+.\d+)\W+(?P<phoneme2>IY|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<phoneme2>:\g<time1> ", 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()
|
|
|