@makefully/dutifully 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/cheer.js +37 -0
- package/package.json +63 -0
- package/say.js +15 -0
- package/src/classes/Cheer.js +255 -0
- package/src/classes/ElevenLabs.js +17 -0
- package/src/classes/FFMPEG.js +13 -0
- package/src/classes/LipSync.js +133 -0
- package/src/classes/Polly.js +10 -0
- package/src/classes/Rasterize.js +13 -0
- package/src/classes/TextToSpeech.js +205 -0
- package/src/classes/Transcript.js +69 -0
- package/src/classes/Transcription.js +163 -0
- package/src/formats/File.js +27 -0
- package/src/formats/JSONFile.js +40 -0
- package/src/formats/LRC.js +38 -0
- package/src/formats/MP3.js +60 -0
- package/src/formats/SAMI.js +59 -0
- package/src/formats/SRT.js +40 -0
- package/src/formats/VTT.js +38 -0
- package/src/helpers/getCaptions.js +44 -0
- package/src/helpers/getFileData.js +33 -0
- package/src/helpers/getJSON.js +33 -0
- package/src/index.js +58 -0
- package/src/say.js +141 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const
|
|
2
|
+
fs = require('fs').promises,
|
|
3
|
+
os = require('os'),
|
|
4
|
+
File = require("./File"),
|
|
5
|
+
getSeparater = (str) => str.indexOf(os.EOL) >= 0 ? os.EOL : '\n';
|
|
6
|
+
|
|
7
|
+
module.exports = class LRC extends File {
|
|
8
|
+
async load (path) {
|
|
9
|
+
await super.load(path);
|
|
10
|
+
this.data = await fs.readFile(path, 'utf8');
|
|
11
|
+
return this.data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
addHash (hash) {
|
|
15
|
+
this.data = this.data.replace(/^\[hash:[a-f0-9]+\]\n?/im, '');
|
|
16
|
+
|
|
17
|
+
const
|
|
18
|
+
sep = getSeparater(this.data),
|
|
19
|
+
insertAfter = this.data.match(/^\[generator:[^\]]*\]/im);
|
|
20
|
+
|
|
21
|
+
if (insertAfter) {
|
|
22
|
+
this.data = this.data.replace(insertAfter[0], `${insertAfter[0]}${sep}[hash:${hash}]`);
|
|
23
|
+
} else {
|
|
24
|
+
this.data = `[hash:${hash}]${sep}${this.data}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getHash () {
|
|
29
|
+
const
|
|
30
|
+
match = this.data.match(/^\[hash:([a-f0-9]+)\]/im);
|
|
31
|
+
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toString () {
|
|
36
|
+
return this.data.split(getSeparater(this.data)).filter((line) => line.length > 0 && line.indexOf('[') === 0 && line.indexOf(']') === 9).map((text) => text.substring(10)).join(' ');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const
|
|
2
|
+
id3 = require('node-id3').Promise,
|
|
3
|
+
File = require("./File");
|
|
4
|
+
|
|
5
|
+
module.exports = class MP3 extends File {
|
|
6
|
+
async load (path) {
|
|
7
|
+
await super.load(path);
|
|
8
|
+
this.data = await id3.read(path);
|
|
9
|
+
return this.data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
addHash (hash, {hashKey = 'CaptionHash'} = {}) {
|
|
13
|
+
const
|
|
14
|
+
existing = (this.data.userDefinedText ?? []).filter(({description}) => description !== hashKey);
|
|
15
|
+
|
|
16
|
+
this.data.userDefinedText = [...existing, {description: hashKey, value: hash}];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getHash ({hashKey = 'CaptionHash'} = {}) {
|
|
20
|
+
const
|
|
21
|
+
{userDefinedText} = this.data;
|
|
22
|
+
|
|
23
|
+
if (userDefinedText) {
|
|
24
|
+
for (let i = 0; i < userDefinedText.length; i++) {
|
|
25
|
+
if (userDefinedText[i].description === hashKey) {
|
|
26
|
+
return userDefinedText[i].value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
save () { // return a promise
|
|
35
|
+
return id3.write(this.data, this.path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toString () {
|
|
39
|
+
const
|
|
40
|
+
{synchronisedLyrics, unsynchronisedLyrics} = this.data;
|
|
41
|
+
|
|
42
|
+
if (unsynchronisedLyrics?.text) {
|
|
43
|
+
return unsynchronisedLyrics.text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (synchronisedLyrics) {
|
|
47
|
+
const
|
|
48
|
+
index = synchronisedLyrics.map(({shortText}) => shortText).indexOf('captions');
|
|
49
|
+
|
|
50
|
+
if (index >= 0) {
|
|
51
|
+
const
|
|
52
|
+
{synchronisedText} = synchronisedLyrics[index];
|
|
53
|
+
|
|
54
|
+
return synchronisedText.map(({text}) => text).join(' ');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const
|
|
2
|
+
fs = require('fs').promises,
|
|
3
|
+
File = require("./File"),
|
|
4
|
+
removeComments = (html) => html.replace(/<!--[\s\S]*?-->/g, ''),
|
|
5
|
+
cleanHTML = (html) => html
|
|
6
|
+
.replace(/<[^>]+>/g, '') // remove HTML tags
|
|
7
|
+
.replace(/ /gi, ' ') // decode common HTML entities
|
|
8
|
+
.replace(/&/gi, '&')
|
|
9
|
+
.replace(/</gi, '<')
|
|
10
|
+
.replace(/>/gi, '>')
|
|
11
|
+
.replace(/"/gi, '"')
|
|
12
|
+
.trim();
|
|
13
|
+
|
|
14
|
+
module.exports = class SAMI extends File {
|
|
15
|
+
async load (path) {
|
|
16
|
+
await super.load(path);
|
|
17
|
+
this.data = await fs.readFile(path, 'utf8');
|
|
18
|
+
return this.data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
addHash (hash) {
|
|
22
|
+
this.data = this.data.replace(/<META[\s]+Hash="[a-f0-9]+"[\s]*\/>\n?/i, '');
|
|
23
|
+
|
|
24
|
+
const
|
|
25
|
+
insertAfter = this.data.match(/<META[\s]+Generator="[^"]*"[\s]*\/>/i);
|
|
26
|
+
|
|
27
|
+
if (insertAfter) {
|
|
28
|
+
this.data = this.data.replace(insertAfter[0], `${insertAfter[0]}\n <META Hash="${hash}"/>`);
|
|
29
|
+
} else {
|
|
30
|
+
this.data = this.data.replace(/<HEAD>/i, `<HEAD>\n <META Hash="${hash}"/>`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getHash () {
|
|
35
|
+
const
|
|
36
|
+
match = this.data.match(/<META[\s]+Hash="([a-f0-9]+)"/i);
|
|
37
|
+
|
|
38
|
+
return match ? match[1] : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toString () {
|
|
42
|
+
const
|
|
43
|
+
sami = removeComments(this.data),
|
|
44
|
+
regex = /<P[^>]*>([\s\S]*?)<\/P>/gi,
|
|
45
|
+
matches = [];
|
|
46
|
+
let match;
|
|
47
|
+
|
|
48
|
+
while ((match = regex.exec(sami)) !== null) {
|
|
49
|
+
const
|
|
50
|
+
innerText = cleanHTML(match[1]);
|
|
51
|
+
|
|
52
|
+
if (innerText) {
|
|
53
|
+
matches.push(innerText);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return matches.join(' ');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const
|
|
2
|
+
fs = require('fs').promises,
|
|
3
|
+
os = require('os'),
|
|
4
|
+
File = require("./File"),
|
|
5
|
+
getSeparater = (str) => str.indexOf(os.EOL) >= 0 ? os.EOL : '\n';
|
|
6
|
+
|
|
7
|
+
module.exports = class SRT extends File {
|
|
8
|
+
async load (path) {
|
|
9
|
+
await super.load(path);
|
|
10
|
+
this.data = await fs.readFile(path, 'utf8');
|
|
11
|
+
return this.data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
addHash (hash) {
|
|
15
|
+
this.data = this.data.replace(/<u[\s]+hash="[a-f0-9]+"[\s]*(?:\/>|><\/u>)\n?/im, '');
|
|
16
|
+
|
|
17
|
+
const
|
|
18
|
+
insertAfter = this.data.match(/<u[\s]+generator="[^"]*"[\s]*(?:\/>|><\/u>)/im);
|
|
19
|
+
|
|
20
|
+
if (insertAfter) {
|
|
21
|
+
this.data = this.data.replace(insertAfter[0], `${insertAfter[0]}${getSeparater(this.data)}<u hash="${hash}"></u>`);
|
|
22
|
+
} else {
|
|
23
|
+
const
|
|
24
|
+
sep = getSeparater(this.data);
|
|
25
|
+
|
|
26
|
+
this.data = `1${sep}00:00:00,000 --> 00:00:00,001${sep}<u hash="${hash}"></u>${sep}${sep}${this.data}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getHash () {
|
|
31
|
+
const
|
|
32
|
+
match = this.data.match(/<u[\s]+hash="([a-f0-9]+)"[\s]*(?:\/>|><\/u>)/im);
|
|
33
|
+
|
|
34
|
+
return match ? match[1] : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toString () {
|
|
38
|
+
return this.data.split(getSeparater(this.data)).filter((line) => line.length > 0 && line.indexOf(' --> ') === -1 && parseInt(line) != line).join(' ');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const
|
|
2
|
+
fs = require('fs').promises,
|
|
3
|
+
os = require('os'),
|
|
4
|
+
File = require("./File"),
|
|
5
|
+
getSeparater = (str) => str.indexOf(os.EOL) >= 0 ? os.EOL : '\n';
|
|
6
|
+
|
|
7
|
+
module.exports = class VTT extends File {
|
|
8
|
+
async load (path) {
|
|
9
|
+
await super.load(path);
|
|
10
|
+
this.data = await fs.readFile(path, 'utf8');
|
|
11
|
+
return this.data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
addHash (hash) {
|
|
15
|
+
this.data = this.data.replace(/^NOTE[\s]+Hash:[\s]+[a-f0-9]+\n?/im, '');
|
|
16
|
+
|
|
17
|
+
const
|
|
18
|
+
sep = getSeparater(this.data),
|
|
19
|
+
insertAfter = this.data.match(/^NOTE[\s]+Generator:[^\n]*/im);
|
|
20
|
+
|
|
21
|
+
if (insertAfter) {
|
|
22
|
+
this.data = this.data.replace(insertAfter[0], `${insertAfter[0]}${sep}NOTE Hash: ${hash}`);
|
|
23
|
+
} else {
|
|
24
|
+
this.data = this.data.replace('WEBVTT', `WEBVTT${sep}${sep}NOTE Hash: ${hash}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getHash () {
|
|
29
|
+
const
|
|
30
|
+
match = this.data.match(/^NOTE[\s]+Hash:[\s]+([a-f0-9]+)/im);
|
|
31
|
+
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toString () {
|
|
36
|
+
return this.data.split(getSeparater(this.data)).filter((line) => line.length > 0 && line.indexOf(' --> ') === -1 && parseInt(line) != line).join(' ');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const
|
|
2
|
+
fs = require('fs').promises,
|
|
3
|
+
getFileData = require("./getFileData"),
|
|
4
|
+
filter = (fileType, file) => (file.indexOf('.') !== 0) && (file.slice(-(fileType.length + 1)) === `.${fileType}`);
|
|
5
|
+
|
|
6
|
+
module.exports = async (path, fileType) => {
|
|
7
|
+
|
|
8
|
+
// "captions.json" is a proprietary format that includes all captions.
|
|
9
|
+
if (fileType.toLowerCase() === 'json') {
|
|
10
|
+
const
|
|
11
|
+
file = await getFileData(`${path}captions.json`, 'json'),
|
|
12
|
+
{data: json} = file;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
captions: Object.keys(json).reduce((obj, key) => {
|
|
16
|
+
obj[key] = json[key].map(({content}) => content).join(' ');
|
|
17
|
+
return obj;
|
|
18
|
+
}, {}),
|
|
19
|
+
file,
|
|
20
|
+
files: null
|
|
21
|
+
};
|
|
22
|
+
} else { // Otherwise we pull all the files and construct a key/value object.
|
|
23
|
+
const
|
|
24
|
+
files = (await fs.readdir(path)).filter(filter.bind(null, fileType)),
|
|
25
|
+
captions = {},
|
|
26
|
+
fileMap = {};
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < files.length; i++) {
|
|
29
|
+
const
|
|
30
|
+
filename = files[i],
|
|
31
|
+
file = await getFileData(`${path}${filename}`, fileType),
|
|
32
|
+
key = filename.substring(0, filename.length - fileType.length - 1);
|
|
33
|
+
|
|
34
|
+
captions[key] = file.toString();
|
|
35
|
+
fileMap[key] = file;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
captions,
|
|
40
|
+
file: null,
|
|
41
|
+
files: fileMap
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const
|
|
2
|
+
JSONFile = require("../formats/JSONFile"),
|
|
3
|
+
LRC = require("../formats/LRC"),
|
|
4
|
+
MP3 = require("../formats/MP3"),
|
|
5
|
+
SAMI = require("../formats/SAMI"),
|
|
6
|
+
SRT = require("../formats/SRT"),
|
|
7
|
+
VTT = require("../formats/VTT"),
|
|
8
|
+
formats = {
|
|
9
|
+
json: JSONFile,
|
|
10
|
+
lrc: LRC,
|
|
11
|
+
mp3: MP3,
|
|
12
|
+
sami: SAMI,
|
|
13
|
+
smi: SAMI,
|
|
14
|
+
srt: SRT,
|
|
15
|
+
vtt: VTT
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = async (path, fileType) => {
|
|
19
|
+
const
|
|
20
|
+
lcFormat = fileType.toLowerCase(),
|
|
21
|
+
File = formats[lcFormat] ?? formats.JSONFile;
|
|
22
|
+
|
|
23
|
+
// Find file type and throw if that doesn't work.
|
|
24
|
+
if (File) {
|
|
25
|
+
const
|
|
26
|
+
file = new File({fileType: lcFormat});
|
|
27
|
+
|
|
28
|
+
await file.load(path);
|
|
29
|
+
return file;
|
|
30
|
+
} else {
|
|
31
|
+
throw `"${fileType}" not supported.`;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const
|
|
2
|
+
fs = require('fs'),
|
|
3
|
+
getJSON = (path) => new Promise ((resolve, reject) => {
|
|
4
|
+
fs.readFile(path, (err, data) => {
|
|
5
|
+
if (err) {
|
|
6
|
+
reject(err);
|
|
7
|
+
} else {
|
|
8
|
+
let json = null;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
json = JSON.parse(data);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
reject(e);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (json) {
|
|
17
|
+
resolve(json);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
module.exports = async (path) => {
|
|
24
|
+
let json = null;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
json = await getJSON(path);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.warn(e);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return json;
|
|
33
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* eslint-disable no-sync */
|
|
2
|
+
/* global console, process, require */
|
|
3
|
+
const
|
|
4
|
+
LipSync = require('./classes/LipSync'),
|
|
5
|
+
parsers = {
|
|
6
|
+
allosaurus: LipSync,
|
|
7
|
+
elevenlabs: require('./classes/ElevenLabs'),
|
|
8
|
+
ffmpeg: require('./classes/FFMPEG'),
|
|
9
|
+
polly: require('./classes/Polly'),
|
|
10
|
+
rasterize: require('./classes/Rasterize'),
|
|
11
|
+
rhubarb: LipSync,
|
|
12
|
+
transcription: require('./classes/Transcription')
|
|
13
|
+
},
|
|
14
|
+
getJSON = require('./helpers/getJSON'),
|
|
15
|
+
send = function (contents) {
|
|
16
|
+
const
|
|
17
|
+
{id, service} = contents,
|
|
18
|
+
configs = Array.isArray(contents[service]) ? contents[service] : [contents[service]];
|
|
19
|
+
|
|
20
|
+
configs.forEach(async (config, index, {length}) => {
|
|
21
|
+
if (!config) {
|
|
22
|
+
console.warn(`Empty configuration for "${service}" service.`);
|
|
23
|
+
} else {
|
|
24
|
+
const
|
|
25
|
+
serviceHandler = new parsers[service]({config, contents});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await serviceHandler.prepare({...contents, ...config});
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn(e.message);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await serviceHandler.send({
|
|
35
|
+
instanceId: `${id}-${service}${length > 1 ? `-${index}` : ''}`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const cheer = async (cmdArgs) => {
|
|
42
|
+
const
|
|
43
|
+
package = await getJSON('./package.json') ?? {},
|
|
44
|
+
config = await getJSON('./cheerfully.json') ?? {},
|
|
45
|
+
env = await getJSON('./env-cheerfully.json') ?? {};
|
|
46
|
+
|
|
47
|
+
send({
|
|
48
|
+
id: `${package.name}-${package.version}`,
|
|
49
|
+
package,
|
|
50
|
+
...config,
|
|
51
|
+
...env,
|
|
52
|
+
...cmdArgs
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
cheer.say = require('./say');
|
|
57
|
+
|
|
58
|
+
module.exports = cheer;
|
package/src/say.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* eslint-disable no-sync */
|
|
2
|
+
/* global console, process, require */
|
|
3
|
+
const
|
|
4
|
+
fs = require('fs'),
|
|
5
|
+
fsp = require('fs').promises,
|
|
6
|
+
path = require('path'),
|
|
7
|
+
{createHash} = require('crypto'),
|
|
8
|
+
{AudioContext} = require('node-web-audio-api'),
|
|
9
|
+
ElevenLabs = require('./classes/ElevenLabs'),
|
|
10
|
+
getJSON = require('./helpers/getJSON'),
|
|
11
|
+
CACHE_DIR = '.cache/tts',
|
|
12
|
+
resolveVoiceId = (voice) => {
|
|
13
|
+
if (!voice) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof voice === 'string') {
|
|
18
|
+
return voice;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return voice.default ?? Object.values(voice)[0] ?? null;
|
|
22
|
+
},
|
|
23
|
+
cacheKey = ({voice, text}) => createHash('sha256')
|
|
24
|
+
.update(JSON.stringify({
|
|
25
|
+
version: 1,
|
|
26
|
+
voice,
|
|
27
|
+
text
|
|
28
|
+
}))
|
|
29
|
+
.digest('hex'),
|
|
30
|
+
playMp3 = async (file) => {
|
|
31
|
+
const
|
|
32
|
+
audioContext = new AudioContext(),
|
|
33
|
+
data = await fsp.readFile(file),
|
|
34
|
+
arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
|
|
35
|
+
buffer = await audioContext.decodeAudioData(arrayBuffer),
|
|
36
|
+
source = audioContext.createBufferSource();
|
|
37
|
+
|
|
38
|
+
source.buffer = buffer;
|
|
39
|
+
source.connect(audioContext.destination);
|
|
40
|
+
|
|
41
|
+
await new Promise((resolve, reject) => {
|
|
42
|
+
source.onended = resolve;
|
|
43
|
+
try {
|
|
44
|
+
source.start();
|
|
45
|
+
} catch (e) {
|
|
46
|
+
reject(e);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await audioContext.close();
|
|
51
|
+
},
|
|
52
|
+
generate = async ({text, voice, key, contents, elevenlabsConfig}) => {
|
|
53
|
+
const
|
|
54
|
+
output = `${CACHE_DIR}/`,
|
|
55
|
+
config = {
|
|
56
|
+
...elevenlabsConfig,
|
|
57
|
+
files: {
|
|
58
|
+
[key]: text
|
|
59
|
+
},
|
|
60
|
+
path: '',
|
|
61
|
+
voice,
|
|
62
|
+
output
|
|
63
|
+
},
|
|
64
|
+
handler = new ElevenLabs({
|
|
65
|
+
config,
|
|
66
|
+
contents: {
|
|
67
|
+
...contents,
|
|
68
|
+
service: 'elevenlabs'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
delete config.script;
|
|
73
|
+
|
|
74
|
+
await handler.prepare({
|
|
75
|
+
difference: false,
|
|
76
|
+
extract: true
|
|
77
|
+
});
|
|
78
|
+
await handler.send({
|
|
79
|
+
instanceId: `${contents.id}-say`
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return path.join(CACHE_DIR, `${key}.mp3`);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = async (textOrArgs = {}) => {
|
|
86
|
+
const
|
|
87
|
+
isString = typeof textOrArgs === 'string',
|
|
88
|
+
cmdArgs = isString ? {} : (textOrArgs ?? {}),
|
|
89
|
+
text = (isString ? textOrArgs : (cmdArgs.text ?? '')).trim();
|
|
90
|
+
|
|
91
|
+
if (!text) {
|
|
92
|
+
throw new Error('Usage: say "Hello world"');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const
|
|
96
|
+
packageJson = await getJSON('./package.json') ?? {},
|
|
97
|
+
cheerfully = await getJSON('./cheerfully.json') ?? {},
|
|
98
|
+
env = await getJSON('./env-cheerfully.json') ?? {},
|
|
99
|
+
contents = {
|
|
100
|
+
id: `${packageJson.name ?? 'cheerfully'}-${packageJson.version ?? '0'}`,
|
|
101
|
+
package: packageJson,
|
|
102
|
+
...cheerfully,
|
|
103
|
+
...env,
|
|
104
|
+
...cmdArgs
|
|
105
|
+
},
|
|
106
|
+
elevenlabsConfig = Array.isArray(contents.elevenlabs)
|
|
107
|
+
? (contents.elevenlabs[0] ?? {})
|
|
108
|
+
: (contents.elevenlabs ?? {}),
|
|
109
|
+
voice = elevenlabsConfig.voice,
|
|
110
|
+
voiceId = resolveVoiceId(voice);
|
|
111
|
+
|
|
112
|
+
if (!voiceId) {
|
|
113
|
+
throw new Error('No ElevenLabs voice configured. Set elevenlabs[0].voice in cheerfully.json.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!contents.server || !contents.accessToken) {
|
|
117
|
+
throw new Error('Missing server/accessToken. Set them in env-cheerfully.json.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await fsp.mkdir(CACHE_DIR, {recursive: true});
|
|
121
|
+
|
|
122
|
+
const
|
|
123
|
+
key = cacheKey({
|
|
124
|
+
voice: voiceId,
|
|
125
|
+
text
|
|
126
|
+
}),
|
|
127
|
+
mp3File = path.join(CACHE_DIR, `${key}.mp3`);
|
|
128
|
+
|
|
129
|
+
if (!fs.existsSync(mp3File)) {
|
|
130
|
+
console.log('Generating speech...');
|
|
131
|
+
await generate({
|
|
132
|
+
text,
|
|
133
|
+
voice,
|
|
134
|
+
key,
|
|
135
|
+
contents,
|
|
136
|
+
elevenlabsConfig
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await playMp3(mp3File);
|
|
141
|
+
};
|