@ -0,0 +1,737 @@
/ / P o l y f i l l f o r O b j e c t . a s s i g n
"function" != typeof Object . assign && ( Object . assign = function ( a , b ) { "use strict" ; if ( null == a ) throw new TypeError ( "Cannot convert undefined or null to object" ) ; for ( var c = Object ( a ) , d = 1 ; d < arguments.length ; d + + ) { var e = arguments [ d ] ; if ( null ! = e ) for ( var f in e ) Object.prototype.hasOwnProperty.call ( e , f ) & & ( c [ f ] = e [ f ] ) } return c } ) ;
/ / P o l y f i l l f o r A r r a y . i s A r r a y
Array . isArray || ( Array . isArray = function ( r ) { return "[object Array]" === Object . prototype . toString . call ( r ) } ) ;
/ / P o l y f i l l f o r A r r a y . p r o t o t y p e . m a p
Array . prototype . map || ( Array . prototype . map = function ( r ) { var t , n , o ; if ( null == this ) throw new TypeError ( "this is null or not defined" ) ; var e = Object ( this ) , i = e . length >>> 0 ; if ( "function" != typeof r ) throw new TypeError ( r + " is not a function" ) ; for ( arguments . length > 1 && ( t = arguments [ 1 ] ) , n = new Array ( i ) , o = 0 ; o < i ; ) { var a , p ; o in e & & ( a = e [ o ] , p = r.call ( t , a , o , e ) , n [ o ] = p ) , o + + } return n } ) ;
/ / P o l y f i l l f o r A r r a y . p r o t o t y p e . e v e r y
Array . prototype . every || ( Array . prototype . every = function ( r , t ) { "use strict" ; var e , n ; if ( null == this ) throw new TypeError ( "this is null or not defined" ) ; var o = Object ( this ) , i = o . length >>> 0 ; if ( "function" != typeof r ) throw new TypeError ; for ( arguments . length > 1 && ( e = t ) , n = 0 ; n < i ; ) { var y ; if ( n in o & & ( y = o [ n ] , ! r.call ( e , y , n , o ) ) ) return ! 1 ; n + + } return ! 0 } ) ;
/ / P o l y f i l l f o r A r r a y . p r o t o t y p e . f i n d
Array . prototype . find || ( Array . prototype . find = function ( r ) { if ( null === this ) throw new TypeError ( "Array.prototype.find called on null or undefined" ) ; if ( "function" != typeof r ) throw new TypeError ( "callback must be a function" ) ; for ( var n = Object ( this ) , t = n . length >>> 0 , o = arguments [ 1 ] , e = 0 ; e < t ; e + + ) { var f = n [ e ] ; if ( r.call ( o , f , e , n ) ) return f }} ) ;
/ / P o l y f i l l f o r A r r a y . p r o t o t y p e . f i l t e r
Array . prototype . filter || ( Array . prototype . filter = function ( r ) { "use strict" ; if ( void 0 === this || null === this ) throw new TypeError ; var t = Object ( this ) , e = t . length >>> 0 ; if ( "function" != typeof r ) throw new TypeError ; for ( var i = [ ] , o = arguments . length >= 2 ? arguments [ 1 ] : void 0 , n = 0 ; n < e ; n + + ) if ( n in t ) { var f = t [ n ] ; r.call ( o , f , n , t ) & & i.push ( f ) } return i } ) ;
/ / P o l y f i l l f o r A r r a y . p r o t o t y p e . f o r E a c h
Array . prototype . forEach || ( Array . prototype . forEach = function ( a , b ) { var c , d ; if ( null === this ) throw new TypeError ( " this is null or not defined" ) ; var e = Object ( this ) , f = e . length >>> 0 ; if ( "function" != typeof a ) throw new TypeError ( a + " is not a function" ) ; for ( arguments . length > 1 && ( c = b ) , d = 0 ; d < f ; ) { var g ; d in e & & ( g = e [ d ] , a.call ( c , g , d , e ) ) , d + + }} ) ;
/ / P o l y f i l l f o r A r r a y . p r o t o t y p e . i n c l u d e s
Array . prototype . includes || ( Array . prototype . includes = function ( r , t ) { if ( null == this ) throw new TypeError ( '"this" is null or not defined' ) ; var e = Object ( this ) , n = e . length >>> 0 ; if ( 0 === n ) return ! 1 ; for ( var i = 0 | t , o = Math . max ( i >= 0 ? i : n - Math . abs ( i ) , 0 ) ; o < n ; ) { if ( function ( r , t ) { return r = = = t | | " number " = = typeof r & & " number " = = typeof t & & isNaN ( r ) & & isNaN ( t ) } ( e [ o ] , r ) ) return ! 0 ; o + + } return ! 1 } ) ;
/ / P o l y f i l l f o r A r r a y . p r o t o t y p e . i n d e x O f
Array . prototype . indexOf || ( Array . prototype . indexOf = function ( r , t ) { var n ; if ( null == this ) throw new TypeError ( '"this" is null or not defined' ) ; var e = Object ( this ) , i = e . length >>> 0 ; if ( 0 === i ) return - 1 ; var o = 0 | t ; if ( o >= i ) return - 1 ; for ( n = Math . max ( o >= 0 ? o : i - Math . abs ( o ) , 0 ) ; n < i ; ) { if ( n in e & & e [ n ] = = = r ) return n ; n + + } return - 1 } ) ;
/ / P o l y f i l l f o r A r r a y . p r o t o t y p e . s o m e
Array . prototype . some || ( Array . prototype . some = function ( r ) { "use strict" ; if ( null == this ) throw new TypeError ( "Array.prototype.some called on null or undefined" ) ; if ( "function" != typeof r ) throw new TypeError ; for ( var e = Object ( this ) , o = e . length >>> 0 , t = arguments . length >= 2 ? arguments [ 1 ] : void 0 , n = 0 ; n < o ; n + + ) if ( n in e & & r.call ( t , e [ n ] , n , e ) ) return ! 0 ; return ! 1 } ) ;
/ / P o l y f i l l f o r S t r i n g . p r o t o t y p e . t r i m
String . prototype . trim || ( String . prototype . trim = function ( ) { return this . replace ( /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g , "" ) } ) ;
/ / P o l y f i l l f o r J S O N
"object" != typeof JSON && ( JSON = { } ) , function ( ) { "use strict" ; function f ( a ) { return a < 10 ? " 0 " + a : a } function this_value ( ) { return this.valueOf ( ) } function quote ( a ) { return rx_escapable.lastIndex = 0 , rx_escapable.test ( a ) ? ' " ' + a.replace ( rx_escapable , function ( a ) { var b = meta [ a ] ; return " string " = = typeof b ? b : " \ \ u " + ( " 0000 " + a.charCodeAt ( 0 ) .toString ( 16 ) ) .slice ( - 4 ) } ) + ' " ' : ' " ' + a + ' " ' } function str ( a , b ) { var c , d , e , f , h , g = gap , i = b [ a ] ; switch ( i & & " object " = = typeof i & & " function " = = typeof i.toJSON & & ( i = i.toJSON ( a ) ) , " function " = = typeof rep & & ( i = rep.call ( b , a , i ) ) , typeof i ) { case " string " : return quote ( i ) ; case " number " : return isFinite ( i ) ? String ( i ) : " null " ; case " boolean " : case " null " : return String ( i ) ; case " object " : if ( ! i ) return " null " ; if ( gap + = indent , h = [ ] , " [ object Array ] " = = = Object.prototype.toString.apply ( i ) ) { for ( f = i.length , c = 0 ; c < f ; c + = 1 ) h [ c ] = str ( c , i ) | | " null " ; return e = 0 = = = h.length ? " [ ] " : gap ? " [ \ n " + gap + h.join ( " , \ n " + gap ) + " \ n " + g + " ] " : " [ " + h.join ( " , " ) + " ] " , gap = g , e } if ( rep & & " object " = = typeof rep ) for ( f = rep.length , c = 0 ; c < f ; c + = 1 ) " string " = = typeof rep [ c ] & & ( d = rep [ c ] , ( e = str ( d , i ) ) & & h.push ( quote ( d ) + ( gap ? " : " : " : " ) + e ) ) ; else for ( d in i ) Object.prototype.hasOwnProperty.call ( i , d ) & & ( e = str ( d , i ) ) & & h.push ( quote ( d ) + ( gap ? " : " : " : " ) + e ) ; return e = 0 = = = h.length ? " {} " : gap ? " { \ n " + gap + h.join ( " , \ n " + gap ) + " \ n " + g + " } " : " { " + h.join ( " , " ) + " } " , gap = g , e }} var rx_one = / ^ [ \ ] , : {} \ s ] * $ / , rx_two = / \ \ ( ? : [ " \ \ \ / bfnrt ] | u [ 0 - 9a - fA - F ] { 4 } ) / g , rx_three = / " [ ^ " \ \ \ n \ r ] * " | true | false | null | - ? \ d + ( ? : \ . \ d * ) ? ( ? : [ eE ] [ + \ - ] ? \ d + ) ? / g , rx_four = / ( ? : ^ | : | , ) ( ? : \ s * \ [ ) + / g , rx_escapable = / [ \ \ " \ u0000 - \ u001f \ u007f - \ u009f \ u00ad \ u0600 - \ u0604 \ u070f \ u17b4 \ u17b5 \ u200c - \ u200f \ u2028 - \ u202f \ u2060 - \ u206f \ ufeff \ ufff0 - \ uffff ] / g , rx_dangerous = / [ \ u0000 \ u00ad \ u0600 - \ u0604 \ u070f \ u17b4 \ u17b5 \ u200c - \ u200f \ u2028 - \ u202f \ u2060 - \ u206f \ ufeff \ ufff0 - \ uffff ] / g ; " function " ! = typeof Date.prototype.toJSON & & ( Date.prototype.toJSON = function ( ) { return isFinite ( this.valueOf ( ) ) ? this.getUTCFullYear ( ) + " - " + f ( this.getUTCMonth ( ) + 1 ) + " - " + f ( this.getUTCDate ( ) ) + " T " + f ( this.getUTCHours ( ) ) + " : " + f ( this.getUTCMinutes ( ) ) + " : " + f ( this.getUTCSeconds ( ) ) + " Z " : null } , Boolean.prototype.toJSON = this_value , Number.prototype.toJSON = this_value , String.prototype.toJSON = this_value ) ; var gap , indent , meta , rep ; " function " ! = typeof JSON.stringify & & ( meta = { "\b" : "\\b" , "\t" : "\\t" , "\n" : "\\n" , "\f" : "\\f" , "\r" : "\\r" , '"' : '\\"' , "\\" : "\\\\" } , JSON.stringify = function ( a , b , c ) { var d ; if ( gap = "" , indent = "" , " number " = = typeof c ) for ( d = 0 ; d < c ; d + = 1 ) indent + = " " ; else " string " = = typeof c & & ( indent = c ) ; if ( rep = b , b & & " function " ! = typeof b & & ( " object " ! = typeof b | | " number " ! = typeof b.length ) ) throw new Error ( " JSON.stringify " ) ; return str ( " " , { " " : a } ) } ) , " function " ! = typeof JSON.parse & & ( JSON.parse = function ( text , reviver ) { function walk ( a , b ) { var c , d , e = a [ b ] ; if ( e & & " object " = = typeof e ) for ( c in e ) Object.prototype.hasOwnProperty.call ( e , c ) & & ( d = walk ( e , c ) , void 0 ! = = d ? e [ c ] = d : delete e [ c ] ) ; return reviver.call ( a , b , e ) } var j ; if ( text = String ( text ) , rx_dangerous.lastIndex = 0 , rx_dangerous.test ( text ) & & ( text = text.replace ( rx_dangerous , function ( a ) { return " \ \ u " + ( " 0000 " + a.charCodeAt ( 0 ) .toString ( 16 ) ) .slice ( - 4 ) } ) ) , rx_one.test ( text.replace ( rx_two , " @ " ) .replace ( rx_three , " ] " ) .replace ( rx_four , " " ) ) ) return j = eval ( " ( " + text + " ) " ) , " function " = = typeof reviver ? walk ( { " " : j } , " " ) : j ; throw new SyntaxError ( " JSON.parse " ) } ) } ( ) ;
function last ( array ) {
return array [ array . length - 1 ] ;
}
function createGuid ( ) {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( /[xy]/g , function ( c ) {
var r = Math . random ( ) * 16 | 0 ;
var v = c == 'x' ? r : ( r & 0x3 | 0x8 ) ;
return v . toString ( 16 ) ;
} ) ;
}
function toArray ( list ) {
var result = [ ] ;
for ( var i = 0 ; i < list . length ; i ++ ) {
result . push ( list [ i ] ) ;
}
return result ;
}
function toArrayBase1 ( list ) {
var result = [ ] ;
for ( var i = 1 ; i <= list . length ; i ++ ) {
result . push ( list [ i ] ) ;
}
return result ;
}
function pad ( n , width , z ) {
z = z || '0' ;
n = String ( n ) ;
return n . length >= width ? n : new Array ( width - n . length + 1 ) . join ( z ) + n ;
}
/ / C h e c k s w h e t h e r s c r i p t s a r e a l l o w e d t o w r i t e f i l e s b y c r e a t i n g a n d d e l e t i n g a d u m m y f i l e
function canWriteFiles ( ) {
try {
var file = new File ( ) ;
file . open ( 'w' ) ;
file . writeln ( '' ) ;
file . close ( ) ;
file . remove ( ) ;
return true ;
} catch ( e ) {
return false ;
}
}
function frameToTime ( frameNumber , compItem ) {
return frameNumber * compItem . frameDuration ;
}
function timeToFrame ( time , compItem ) {
return time * compItem . frameRate ;
}
/ / T o p r e v e n t r o u n d i n g e r r o r s
var epsilon = 0.001 ;
function isFrameVisible ( compItem , frameNumber ) {
if ( ! compItem ) return false ;
var time = frameToTime ( frameNumber + epsilon , compItem ) ;
var videoLayers = toArrayBase1 ( compItem . layers ) . filter ( function ( layer ) {
return layer . hasVideo ;
} ) ;
var result = videoLayers . find ( function ( layer ) {
return layer . activeAtTime ( time ) ;
} ) ;
return Boolean ( result ) ;
}
var appName = 'Rhubarb Lip Sync' ;
var settingsFilePath = Folder . userData . fullName + '/rhubarb-ae-settings.json' ;
function readTextFile ( fileOrPath ) {
var filePath = fileOrPath . fsName || fileOrPath ;
var file = new File ( filePath ) ;
function check ( ) {
if ( file . error ) throw new Error ( 'Error reading file "' + filePath + '": ' + file . error ) ;
}
try {
file . open ( 'r' ) ; check ( ) ;
file . encoding = 'UTF-8' ; check ( ) ;
var result = file . read ( ) ; check ( ) ;
return result ;
} finally {
file . close ( ) ; check ( ) ;
}
}
function writeTextFile ( fileOrPath , text ) {
var filePath = fileOrPath . fsName || fileOrPath ;
var file = new File ( filePath ) ;
function check ( ) {
if ( file . error ) throw new Error ( 'Error writing file "' + filePath + '": ' + file . error ) ;
}
try {
file . open ( 'w' ) ; check ( ) ;
file . encoding = 'UTF-8' ; check ( ) ;
file . write ( text ) ; check ( ) ;
} finally {
file . close ( ) ; check ( ) ;
}
}
function readSettingsFile ( ) {
try {
return JSON . parse ( readTextFile ( settingsFilePath ) ) ;
} catch ( e ) {
return { } ;
}
}
function writeSettingsFile ( settings ) {
try {
writeTextFile ( settingsFilePath , JSON . stringify ( settings , null , 2 ) ) ;
} catch ( e ) {
alert ( 'Error persisting settings. ' + e . message ) ;
}
}
var osIsWindows = ( system . osName || $ . os ) . match ( /windows/i ) ;
/ / D e p e n d i n g o n t h e o p e r a t i n g s y s t e m , t h e s y n t a x f o r e s c a p i n g c o m m a n d - l i n e a r g u m e n t s d i f f e r s .
function cliEscape ( argument ) {
return osIsWindows
? '"' + argument + '"'
: "'" + argument . replace ( /'/g , "'\\''" ) + "'" ;
}
function exec ( command ) {
return system . callSystem ( command ) ;
}
function execInWindow ( command ) {
if ( osIsWindows ) {
system . callSystem ( 'cmd /C "' + command + '"' ) ;
} else {
/ / I d i d n ' t t h i n k i t c o u l d b e s o c o m p l i c a t e d o n O S X t o o p e n a n e w T e r m i n a l w i n d o w ,
/ / e x e c u t e a c o m m a n d , t h e n c l o s e t h e T e r m i n a l w i n d o w .
/ / I f y o u k n o w a b e t t e r s o l u t i o n , l e t m e k n o w !
var escapedCommand = command . replace ( /"/g , '\\"' ) ;
var appleScript = ' \
tell application "Terminal" \
-- Quit terminal \
-- Yes , that \ ' s undesirable if there was an open window before . \
-- But all solutions I could find were at least as hacky . \
quit \
-- Open terminal \
activate \
-- Run command in new tab \
set newTab to do script ( "' + escapedCommand + '" ) \
-- Wait until command is done \
tell newTab \
repeat while busy \
delay 0.1 \
end repeat \
end tell \
quit \
end tell ' ;
exec ( 'osascript -e ' + cliEscape ( appleScript ) ) ;
}
}
var rhubarbPath = osIsWindows ? 'rhubarb.exe' : '/usr/local/bin/rhubarb' ;
/ / E x t e n d S c r i p t ' s r e s o u r c e s t r i n g s a r e a p a i n t o w r i t e .
/ / T h i s f u n c t i o n a l l o w s t h e m t o b e w r i t t e n i n J S O N n o t a t i o n , t h e n c o n v e r t s t h e m i n t o t h e r e q u i r e d
/ / f o r m a t .
/ / F o r i n s t a n c e , t h i s s t r i n g : ' { " _ _ t y p e _ _ " : " S t a t i c T e x t " , " t e x t " : " H e l l o w o r l d " } '
/ / i s c o n v e r t e d t o t h i s : ' S t a t i c T e x t { " t e x t " : " H e l l o w o r l d " } ' .
/ / T h i s c o d e r e l i e s o n t h e f a c t t h a t , c o n t r a r y t o t h e l a n g u a g e s p e c i f i c a t i o n , a l l m a j o r J a v a S c r i p t
/ / i m p l e m e n t a t i o n s k e e p o b j e c t p r o p e r t i e s i n i n s e r t i o n o r d e r .
function createResourceString ( tree ) {
var result = JSON . stringify ( tree , null , 2 ) ;
result = result . replace ( /(\{\s*)"__type__":\s*"(\w+)",?\s*/g , '$2 $1' ) ;
return result ;
}
/ / O b j e c t c o n t a i n i n g f u n c t i o n s t o c r e a t e c o n t r o l d e s c r i p t i o n t r e e s .
/ / F o r i n s t a n c e , ` c o n t r o l s . S t a t i c T e x t ( { t e x t : ' H e l l o w o r l d ' } ) `
/ / r e t u r n s ` { _ _ t y p e _ _ : S t a t i c T e x t , t e x t : ' H e l l o w o r l d ' } ` .
var controlFunctions = ( function ( ) {
var controlTypes = [
/ / S t r a n g e l y , ' d i a l o g ' a n d ' p a l e t t e ' n e e d t o s t a r t w i t h a l o w e r - c a s e c h a r a c t e r
[ 'Dialog' , 'dialog' ] , [ 'Palette' , 'palette' ] ,
'Panel' , 'Group' , 'TabbedPanel' , 'Tab' , 'Button' , 'IconButton' , 'Image' , 'StaticText' ,
'EditText' , 'Checkbox' , 'RadioButton' , 'Progressbar' , 'Slider' , 'Scrollbar' , 'ListBox' ,
'DropDownList' , 'TreeView' , 'ListItem' , 'FlashPlayer'
] ;
var result = { } ;
controlTypes . forEach ( function ( type ) {
var isArray = Array . isArray ( type ) ;
var key = isArray ? type [ 0 ] : type ;
var value = isArray ? type [ 1 ] : type ;
result [ key ] = function ( options ) {
return Object . assign ( { _ _type _ _ : value } , options ) ;
} ;
} ) ;
return result ;
} ) ( ) ;
/ / R e t u r n s t h e p a t h o f a p r o j e c t i t e m w i t h i n t h e p r o j e c t
function getItemPath ( item ) {
if ( item === app . project . rootFolder ) {
return '/' ;
}
var result = item . name ;
while ( item . parentFolder !== app . project . rootFolder ) {
result = item . parentFolder . name + ' / ' + result ;
item = item . parentFolder ;
}
return '/ ' + result ;
}
/ / S e l e c t s t h e i t e m w i t h i n a n i t e m c o n t r o l w h o s e t e x t m a t c h e s t h e s p e c i f i e d t e x t .
/ / I f n o s u c h i t e m e x i s t s , s e l e c t s t h e f i r s t i t e m , i f p r e s e n t .
function selectByTextOrFirst ( itemControl , text ) {
var targetItem = toArray ( itemControl . items ) . find ( function ( item ) {
return item . text === text ;
} ) ;
if ( ! targetItem && itemControl . items . length ) {
targetItem = itemControl . items [ 0 ] ;
}
if ( targetItem ) {
itemControl . selection = targetItem ;
}
}
function getWaveFileProjectItems ( ) {
var result = toArrayBase1 ( app . project . items ) . filter ( function ( item ) {
var isAudioFootage = item instanceof FootageItem && item . hasAudio && ! item . hasVideo ;
if ( ! isAudioFootage ) return false ;
var isWaveFile = item . file && item . file . exists && item . file . name . match ( /\.wav$/i ) ;
return isWaveFile ;
} ) ;
return result ;
}
var mouthShapeNames = 'ABCDEFGHX' . split ( '' ) ;
var basicMouthShapeCount = 6 ;
var mouthShapeCount = mouthShapeNames . length ;
var basicMouthShapeNames = mouthShapeNames . slice ( 0 , basicMouthShapeCount ) ;
var extendedMouthShapeNames = mouthShapeNames . slice ( basicMouthShapeCount ) ;
function getMouthCompHelpTip ( ) {
var result = 'A composition containing the mouth shapes, one drawing per frame. They must be '
+ 'arranged as follows:\n' ;
mouthShapeNames . forEach ( function ( mouthShapeName , i ) {
var isOptional = i >= basicMouthShapeCount ;
result += '\n00:' + pad ( i , 2 ) + '\t' + mouthShapeName + ( isOptional ? ' (optional)' : '' ) ;
} ) ;
return result ;
}
function createExtendedShapeCheckboxes ( ) {
var result = { } ;
extendedMouthShapeNames . forEach ( function ( shapeName ) {
result [ shapeName . toLowerCase ( ) ] = controlFunctions . Checkbox ( {
text : shapeName ,
helpTip : 'Controls whether to use the optional ' + shapeName + ' shape.'
} ) ;
} ) ;
return result ;
}
function createDialogWindow ( ) {
var resourceString ;
with ( controlFunctions ) {
resourceString = createResourceString (
Dialog ( {
text : appName ,
settings : Group ( {
orientation : 'column' ,
alignChildren : [ 'left' , 'top' ] ,
audioFile : Group ( {
label : StaticText ( {
text : 'Audio file:' ,
/ / I f I d o n ' t e x p l i c i t l y a c t i v a t e a c o n t r o l , A f t e r E f f e c t s h a s t r o u b l e
/ / w i t h k e y b o a r d f o c u s , s o I c a n ' t t y p e i n t h e t e x t e d i t f i e l d b e l o w .
active : true
} ) ,
value : DropDownList ( {
helpTip : 'An audio file containing recorded dialog.\n'
+ 'This field shows all audio files of type .wav that exist in '
+ 'your After Effects project.'
} )
} ) ,
dialogText : Group ( {
label : StaticText ( { text : 'Dialog text (optional):' } ) ,
value : EditText ( {
properties : { multiline : true } ,
characters : 60 ,
minimumSize : [ 0 , 100 ] ,
helpTip : 'For better animation results, you can specify the text of '
+ 'the recording here. This field is optional.'
} )
} ) ,
mouthComp : Group ( {
label : StaticText ( { text : 'Mouth composition:' } ) ,
value : DropDownList ( { helpTip : getMouthCompHelpTip ( ) } )
} ) ,
extendedMouthShapes : Group (
Object . assign (
{ label : StaticText ( { text : 'Extended mouth shapes:' } ) } ,
createExtendedShapeCheckboxes ( )
)
) ,
targetFolder : Group ( {
label : StaticText ( { text : 'Target folder:' } ) ,
value : DropDownList ( {
helpTip : 'The project folder in which to create the animation '
+ 'composition. The composition will be named like the audio file.'
} )
} ) ,
frameRate : Group ( {
label : StaticText ( { text : 'Frame rate:' } ) ,
value : EditText ( {
characters : 8 ,
helpTip : 'The frame rate for the animation.'
} ) ,
auto : Checkbox ( {
text : 'From mouth composition' ,
helpTip : 'If checked, the animation will use the same frame rate as '
+ 'the mouth composition.'
} )
} )
} ) ,
separator : Group ( { preferredSize : [ '' , 3 ] } ) ,
buttons : Group ( {
alignment : 'right' ,
animate : Button ( {
properties : { name : 'ok' } ,
text : 'Animate'
} ) ,
cancel : Button ( {
properties : { name : 'cancel' } ,
text : 'Cancel'
} )
} )
} )
) ;
}
/ / C r e a t e w i n d o w a n d c h i l d c o n t r o l s
var window = new Window ( resourceString ) ;
var controls = {
audioFile : window . settings . audioFile . value ,
dialogText : window . settings . dialogText . value ,
mouthComp : window . settings . mouthComp . value ,
targetFolder : window . settings . targetFolder . value ,
frameRate : window . settings . frameRate . value ,
autoFrameRate : window . settings . frameRate . auto ,
animateButton : window . buttons . animate ,
cancelButton : window . buttons . cancel
} ;
extendedMouthShapeNames . forEach ( function ( shapeName ) {
controls [ 'mouthShape' + shapeName ] =
window . settings . extendedMouthShapes [ shapeName . toLowerCase ( ) ] ;
} ) ;
/ / A d d a u d i o f i l e o p t i o n s
getWaveFileProjectItems ( ) . forEach ( function ( projectItem ) {
var listItem = controls . audioFile . add ( 'item' , getItemPath ( projectItem ) ) ;
listItem . projectItem = projectItem ;
} ) ;
/ / A d d m o u t h c o m p o s i t i o n o p t i o n s
var comps = toArrayBase1 ( app . project . items ) . filter ( function ( item ) {
return item instanceof CompItem ;
} ) ;
comps . forEach ( function ( projectItem ) {
var listItem = controls . mouthComp . add ( 'item' , getItemPath ( projectItem ) ) ;
listItem . projectItem = projectItem ;
} ) ;
/ / A d d t a r g e t f o l d e r o p t i o n s
var projectFolders = toArrayBase1 ( app . project . items ) . filter ( function ( item ) {
return item instanceof FolderItem ;
} ) ;
projectFolders . unshift ( app . project . rootFolder ) ;
projectFolders . forEach ( function ( projectFolder ) {
var listItem = controls . targetFolder . add ( 'item' , getItemPath ( projectFolder ) ) ;
listItem . projectItem = projectFolder ;
} ) ;
/ / L o a d p e r s i s t e d s e t t i n g s
var settings = readSettingsFile ( ) ;
selectByTextOrFirst ( controls . audioFile , settings . audioFile ) ;
controls . dialogText . text = settings . dialogText || '' ;
selectByTextOrFirst ( controls . mouthComp , settings . mouthComp ) ;
extendedMouthShapeNames . forEach ( function ( shapeName ) {
controls [ 'mouthShape' + shapeName ] . value =
( settings . extendedMouthShapes || { } ) [ shapeName . toLowerCase ( ) ] ;
} ) ;
selectByTextOrFirst ( controls . targetFolder , settings . targetFolder ) ;
controls . frameRate . text = settings . frameRate || '' ;
controls . autoFrameRate . value = settings . autoFrameRate ;
/ / A l i g n c o n t r o l s
window . onShow = function ( ) {
/ / G i v e u n i f o r m w i d t h t o a l l l a b e l s
var groups = toArray ( window . settings . children ) ;
var labelWidths = groups . map ( function ( group ) { return group . children [ 0 ] . size . width ; } ) ;
var maxLabelWidth = Math . max . apply ( Math , labelWidths ) ;
groups . forEach ( function ( group ) {
group . children [ 0 ] . size . width = maxLabelWidth ;
} ) ;
/ / G i v e u n i f o r m w i d t h t o i n p u t s
var valueWidths = groups . map ( function ( group ) {
return last ( group . children ) . bounds . right - group . children [ 1 ] . bounds . left ;
} ) ;
var maxValueWidth = Math . max . apply ( Math , valueWidths ) ;
groups . forEach ( function ( group ) {
var multipleControls = group . children . length > 2 ;
if ( ! multipleControls ) {
group . children [ 1 ] . size . width = maxValueWidth ;
}
} ) ;
window . layout . layout ( true ) ;
} ;
var updating = false ;
function update ( ) {
if ( updating ) return ;
updating = true ;
try {
/ / H a n d l e a u t o f r a m e r a t e
var autoFrameRate = controls . autoFrameRate . value ;
controls . frameRate . enabled = ! autoFrameRate ;
if ( autoFrameRate ) {
/ / T a k e f r a m e r a t e f r o m m o u t h c o m p
var mouthComp = ( controls . mouthComp . selection || { } ) . projectItem ;
controls . frameRate . text = mouthComp ? mouthComp . frameRate : '' ;
} else {
/ / S a n i t i z e f r a m e r a t e
var sanitizedFrameRate = controls . frameRate . text . match ( /\d*\.?\d*/ ) [ 0 ] ;
if ( sanitizedFrameRate !== controls . frameRate . text ) {
controls . frameRate . text = sanitizedFrameRate ;
}
}
/ / S t o r e s e t t i n g s
var settings = {
audioFile : ( controls . audioFile . selection || { } ) . text ,
dialogText : controls . dialogText . text ,
mouthComp : ( controls . mouthComp . selection || { } ) . text ,
extendedMouthShapes : { } ,
targetFolder : ( controls . targetFolder . selection || { } ) . text ,
frameRate : Number ( controls . frameRate . text ) ,
autoFrameRate : controls . autoFrameRate . value
} ;
extendedMouthShapeNames . forEach ( function ( shapeName ) {
settings . extendedMouthShapes [ shapeName . toLowerCase ( ) ] =
controls [ 'mouthShape' + shapeName ] . value ;
} ) ;
writeSettingsFile ( settings ) ;
} finally {
updating = false ;
}
}
/ / V a l i d a t e u s e r i n p u t . P o s s i b l e r e t u r n v a l u e s :
/ / * N o n - e m p t y s t r i n g : V a l i d a t i o n f a i l e d . S h o w e r r o r m e s s a g e .
/ / * E m p t y s t r i n g : V a l i d a t i o n f a i l e d . D o n ' t s h o w e r r o r m e s s a g e .
/ / * U n d e f i n e d : V a l i d a t i o n s u c c e e d e d .
function validate ( ) {
/ / C h e c k i n p u t v a l u e s
if ( ! controls . audioFile . selection ) return 'Please select an audio file.' ;
if ( ! controls . mouthComp . selection ) return 'Please select a mouth composition.' ;
if ( ! controls . targetFolder . selection ) return 'Please select a target folder.' ;
if ( Number ( controls . frameRate . text ) < 12 ) {
return 'Please enter a frame rate of at least 12 fps.' ;
}
/ / C h e c k m o u t h s h a p e v i s i b i l i t y
var comp = controls . mouthComp . selection . projectItem ;
for ( var i = 0 ; i < mouthShapeCount ; i ++ ) {
var shapeName = mouthShapeNames [ i ] ;
var required = i < basicMouthShapeCount || controls [ 'mouthShape' + shapeName ] . value ;
if ( required && ! isFrameVisible ( comp , i ) ) {
return 'The mouth comp does not seem to contain an image for shape '
+ shapeName + ' at frame ' + i + '.' ;
}
}
if ( ! comp . preserveNestedFrameRate ) {
var fix = Window . confirm (
'The setting "Preserve frame rate when nested or in render queue" is not active '
+ 'for the mouth composition. This can result in incorrect animation.\n\n'
+ 'Activate this setting now?' ,
false ,
'Fix composition setting?' ) ;
if ( fix ) {
app . beginUndoGroup ( appName + ': Mouth composition setting' ) ;
comp . preserveNestedFrameRate = true ;
app . endUndoGroup ( ) ;
} else {
return '' ;
}
}
/ / C h e c k f o r c o r r e c t R h u b a r b v e r s i o n
var version = exec ( rhubarbPath + ' --version' ) || '' ;
var match = version . match ( /Rhubarb Lip Sync version ((\d+)\.(\d+).(\d+))/ ) ;
if ( ! match ) {
var instructions = osIsWindows
? 'Make sure your PATH environment variable contains the ' + appName + ' '
+ 'application directory.'
: 'Make sure you have created this file as a symbolic link to the ' + appName + ' '
+ 'executable (rhubarb).' ;
return 'Cannot find executable file "' + rhubarbPath + '". \n' + instructions ;
}
var versionString = match [ 1 ] ;
var major = Number ( match [ 2 ] ) ;
var minor = Number ( match [ 3 ] ) ;
if ( major != 1 || minor < 6 ) {
return 'This script requires ' + appName + ' 1.6.0 or a later 1.x version. '
+ 'Your installed version is ' + versionString + ', which is not compatible.' ;
}
}
function generateMouthCues ( audioFileFootage , dialogText , mouthComp , extendedMouthShapeNames ,
targetProjectFolder , frameRate )
{
var basePath = Folder . temp . fsName + '/' + createGuid ( ) ;
var dialogFile = new File ( basePath + '.txt' ) ;
var logFile = new File ( basePath + '.log' ) ;
var jsonFile = new File ( basePath + '.json' ) ;
try {
/ / C r e a t e t e x t f i l e c o n t a i n i n g d i a l o g
writeTextFile ( dialogFile , dialogText ) ;
/ / C r e a t e c o m m a n d l i n e
var commandLine = rhubarbPath
+ ' --dialogFile ' + cliEscape ( dialogFile . fsName )
+ ' --exportFormat json'
+ ' --extendedShapes ' + cliEscape ( extendedMouthShapeNames . join ( '' ) )
+ ' --logFile ' + cliEscape ( logFile . fsName )
+ ' --logLevel fatal'
+ ' --output ' + cliEscape ( jsonFile . fsName )
+ ' ' + cliEscape ( audioFileFootage . file . fsName ) ;
/ / R u n R h u b a r b
execInWindow ( commandLine ) ;
/ / C h e c k l o g f o r f a t a l e r r o r s
if ( logFile . exists ) {
var fatalLog = readTextFile ( logFile ) . trim ( ) ;
if ( fatalLog ) {
/ / T r y t o e x t r a c t o n l y t h e a c t u a l e r r o r m e s s a g e
var match = fatalLog . match ( /\[Fatal\] ([\s\S]*)/ ) ;
var message = match ? match [ 1 ] : fatalLog ;
throw new Error ( 'Error running ' + appName + '.\n' + message ) ;
}
}
var result ;
try {
result = JSON . parse ( readTextFile ( jsonFile ) ) ;
$ . writeln ( readTextFile ( jsonFile ) ) ;
} catch ( e ) {
throw new Error ( 'No animation result. Animation was probably canceled.' ) ;
}
return result ;
} finally {
dialogFile . remove ( ) ;
logFile . remove ( ) ;
jsonFile . remove ( ) ;
}
}
function animateMouthCues ( mouthCues , audioFileFootage , mouthComp , targetProjectFolder ,
frameRate )
{
/ / F i n d a n u n c o n f l i c t i n g c o m p n a m e
/ / . . . s t r i p e x t e n s i o n . w a v , i f p r e s e n t
var baseName = audioFileFootage . name . match ( /^(.*?)(\.wav)?$/i ) [ 1 ] ;
var compName = baseName ;
/ / . . . a d d n u m e r i c s u f f i x , i f n e e d e d
var existingItems = toArrayBase1 ( targetProjectFolder . items ) ;
var counter = 1 ;
while ( existingItems . some ( function ( item ) { return item . name === compName ; } ) ) {
counter ++ ;
compName = baseName + ' ' + counter ;
}
/ / C r e a t e n e w c o m p
var comp = targetProjectFolder . items . addComp ( compName , mouthComp . width , mouthComp . height ,
mouthComp . pixelAspect , audioFileFootage . duration , frameRate ) ;
/ / S h o w n e w c o m p
comp . openInViewer ( ) ;
/ / A d d a u d i o l a y e r
comp . layers . add ( audioFileFootage ) ;
/ / A d d m o u t h l a y e r
var mouthLayer = comp . layers . add ( mouthComp ) ;
mouthLayer . timeRemapEnabled = true ;
mouthLayer . outPoint = comp . duration ;
/ / A n i m a t e m o u t h l a y e r
var timeRemap = mouthLayer [ 'Time Remap' ] ;
/ / E n a b l i n g t i m e r e m a p p i n g a u t o m a t i c a l l y a d d s t w o k e y s . R e m o v e t h e s e c o n d .
timeRemap . removeKey ( 2 ) ;
mouthCues . mouthCues . forEach ( function ( mouthCue ) {
/ / R o u n d d o w n k e y f r a m e t i m e . I n a n i m a t i o n , e a r l i e r i s b e t t e r t h a n l a t e r .
/ / S e t k e y f r a m e t i m e t o * j u s t b e f o r e * t h e e x a c t f r a m e t o p r e v e n t r o u n d i n g e r r o r s
var frame = Math . floor ( timeToFrame ( mouthCue . start , comp ) ) ;
var time = frame !== 0 ? frameToTime ( frame - epsilon , comp ) : 0 ;
/ / S e t r e m a p p e d t i m e t o * j u s t a f t e r * t h e e x a c t f r a m e t o p r e v e n t r o u n d i n g e r r o r s
var mouthCompFrame = mouthShapeNames . indexOf ( mouthCue . value ) ;
var remappedTime = frameToTime ( mouthCompFrame + epsilon , mouthComp ) ;
timeRemap . setValueAtTime ( time , remappedTime ) ;
} ) ;
for ( var i = 1 ; i <= timeRemap . numKeys ; i ++ ) {
timeRemap . setInterpolationTypeAtKey ( i , KeyframeInterpolationType . HOLD ) ;
}
}
function animate ( audioFileFootage , dialogText , mouthComp , extendedMouthShapeNames ,
targetProjectFolder , frameRate )
{
try {
var mouthCues = generateMouthCues ( audioFileFootage , dialogText , mouthComp ,
extendedMouthShapeNames , targetProjectFolder , frameRate ) ;
app . beginUndoGroup ( appName + ': Animation' ) ;
animateMouthCues ( mouthCues , audioFileFootage , mouthComp , targetProjectFolder ,
frameRate ) ;
app . endUndoGroup ( ) ;
} catch ( e ) {
Window . alert ( e . message , appName , true ) ;
return ;
}
}
/ / H a n d l e c h a n g e s
update ( ) ;
controls . audioFile . onChange = update ;
controls . dialogText . onChanging = update ;
controls . mouthComp . onChange = update ;
extendedMouthShapeNames . forEach ( function ( shapeName ) {
controls [ 'mouthShape' + shapeName ] . onClick = update ;
} ) ;
controls . targetFolder . onChange = update ;
controls . frameRate . onChanging = update ;
controls . autoFrameRate . onClick = update ;
/ / H a n d l e a n i m a t i o n
controls . animateButton . onClick = function ( ) {
var validationError = validate ( ) ;
if ( typeof validationError === 'string' ) {
if ( validationError ) {
Window . alert ( validationError , appName , true ) ;
}
} else {
window . close ( ) ;
animate (
controls . audioFile . selection . projectItem ,
controls . dialogText . text || '' ,
controls . mouthComp . selection . projectItem ,
extendedMouthShapeNames . filter ( function ( shapeName ) {
return controls [ 'mouthShape' + shapeName ] . value ;
} ) ,
controls . targetFolder . selection . projectItem ,
Number ( controls . frameRate . text )
) ;
}
} ;
/ / H a n d l e c a n c e l a t i o n
controls . cancelButton . onClick = function ( ) {
window . close ( ) ;
} ;
return window ;
}
function checkPreconditions ( ) {
if ( ! canWriteFiles ( ) ) {
Window . alert ( 'This script requires file system access.\n\n'
+ 'Please enable Preferences > General > Allow Scripts to Write Files and Access Network.' ,
appName , true ) ;
return false ;
}
return true ;
}
if ( checkPreconditions ( ) ) {
createDialogWindow ( ) . show ( ) ;
}