Files
gtav-src/tools_ng/bin/audio/FaceFx 2012/Scripts/FunnelPhonemes.py
T
2025-09-29 00:52:08 +02:00

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()