@met4citizen/talkinghead
Version:
Talking Head (3D): A JavaScript class for real-time lip-sync using Ready Player Me full-body 3D avatars.
209 lines (177 loc) • 5.5 kB
JavaScript
/**
* MIT License
*
* Copyright (c) 2024 Mika Suominen
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
class SSML {
constructor( opt = null ) {
this.opt = Object.assign({
engine: "google"
}, opt || {});
this.reset();
}
reset() {
this.root = { type: 'root', value: '', parent: null, children: [] };
this.ssml = '';
}
traverse(node) {
if (node.children) {
if (node.type !== 'root') {
this.ssml += `<${node.type}${node.value}>`;
}
node.children.forEach(x => {
this.traverse(x);
});
this.ssml += `</${node.type}>`;
} else {
if (node.type === 'TEXT') {
this.ssml += node.value;
} else {
this.ssml += `<${node.type}${node.value}/>`;
}
}
}
convert(s) {
// Reset
this.reset();
// Sanitize
s = s
.replace(/\r?\n|\r/g, '') // remove new lines
.replace(/>\s+</g, '><') // remove spaces between <tags>'s
.replace(/<p>\s+<\/p>/g, '') // Remove empty <p> </p>
.replace(/<p><\/p>/g, '') // Remove empty <p></p>
.replace(/<s>\s+<\/s>/g, '') // Remove empty <s> </s>
.replace(/<s><\/s>/g, '') // Remove empty <s></s>
.replace(/<s\/>/g, '') // Remove self-closing <s/>
.replace(/<p\/>/g, '') // Remove self-closing <p/>
.trim();
let text = '';
let isText = false;
let node = this.root;
for (let i = 0, l = s.length; i < l; i++) {
// SSML tag
if (s[i] === '<') {
// check if the text was already started - finish it and add to the parentNode
if (isText) {
isText = false;
const newNode = {
type: 'TEXT',
value: text,
parent: node,
children: []
};
node.children.push(newNode);
}
// type and value/attributes of parsed SSML tag
let type = '';
let value = ''; // can be blank, like <tag /> or <tag></tag>
let isEndTag = false; // flag for end tag (</tag>)
let isEmptyTag = false; // flag for empty tag (<tag />)
// start from next char
let j = i + 1;
// check if it is an end tag (no value)
if (s[j] === '/') {
isEndTag = true;
// start from the next char
j++;
// parse only type
while (s[j] !== '>') {
type += s[j];
j++;
}
} else {
/*
* 1. Parse type unless:
* ' ' - value is coming
* '>' - is start tag marker
* '/' - is empty tag marker
*/
while (s[j] !== ' ' && s[j] !== '>' && s[j] !== '/') {
type += s[j];
j++;
}
// 2. Parse value
while (true) {
if (s[j] !== '>') {
// A. value continues -> accumulate value
value += s[j];
} else if (s[j - 1] === '/') {
// B. empty tag <tag />
isEmptyTag = true;
// remove last `/` char from value
if (value.length !== 0) { value = value.slice(0, value.length - 1); }
break;
} else {
// C. end tag </tag>
break;
}
j++;
}
}
/*
* Process parsed results
*/
if (!isEndTag) {
const newNode = {
parentNode: node,
type: type,
value: value,
children: []
};
node.children.push(newNode);
if (!isEmptyTag) {
// Not an empty tag => can have other children, then keep it active
node = newNode;
}
} else {
// End tag
node = node.parent;
if ( node.type !== type ) {
console.error("Incorrent SSML.")
}
}
// skip processed chars for the next iteration
i = j;
} else {
// Text
if (!isText) {
isText = true;
text = '';
}
// accumulate characters
text += s[i];
if (i === l - 1 && isText) {
// ssml ends with plain text => create node
const newNode = {
type: 'TEXT',
value: text,
parent: node,
children: []
};
node.parent.push(newNode);
}
}
}
this.traverse(this.root);
return this.ssml;
}
}
export { SSML };