@makefully/dutifully 1.10.1 → 1.12.0
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 +13 -5
- package/LICENSE +21 -21
- package/README.md +216 -214
- package/cheer.js +36 -36
- package/package.json +63 -63
- package/say.js +15 -15
- package/src/classes/Cheer.js +254 -254
- package/src/classes/ElevenLabs.js +16 -16
- package/src/classes/FFMPEG.js +12 -12
- package/src/classes/LipSync.js +140 -132
- package/src/classes/Polly.js +9 -9
- package/src/classes/Rasterize.js +12 -12
- package/src/classes/TextToSpeech.js +215 -204
- package/src/classes/Transcript.js +72 -68
- package/src/classes/Transcription.js +169 -162
- package/src/formats/File.js +28 -26
- package/src/formats/JSONFile.js +41 -39
- package/src/formats/LRC.js +37 -37
- package/src/formats/MP3.js +61 -59
- package/src/formats/SAMI.js +58 -58
- package/src/formats/SRT.js +39 -39
- package/src/formats/VTT.js +37 -37
- package/src/helpers/dirs.js +30 -0
- package/src/helpers/getCaptions.js +43 -43
- package/src/helpers/getFileData.js +32 -32
- package/src/helpers/getJSON.js +32 -32
- package/src/index.js +58 -58
- package/src/say.js +141 -141
|
@@ -1,163 +1,170 @@
|
|
|
1
|
-
const
|
|
2
|
-
Cheer = require('./Cheer'),
|
|
3
|
-
Transcript = require('./Transcript'),
|
|
4
|
-
fs = require('fs').promises,
|
|
5
|
-
getCaptions = require('../helpers/getCaptions'),
|
|
6
|
-
getFileData = require('../helpers/getFileData'),
|
|
7
|
-
getJSON = require('../helpers/getJSON'),
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
1
|
+
const
|
|
2
|
+
Cheer = require('./Cheer'),
|
|
3
|
+
Transcript = require('./Transcript'),
|
|
4
|
+
fs = require('fs').promises,
|
|
5
|
+
getCaptions = require('../helpers/getCaptions'),
|
|
6
|
+
getFileData = require('../helpers/getFileData'),
|
|
7
|
+
getJSON = require('../helpers/getJSON'),
|
|
8
|
+
{ensureDir, readdirOrEmpty} = require('../helpers/dirs'),
|
|
9
|
+
SEPARATE_CHARACTER = '|',
|
|
10
|
+
JOIN_CHARACTER = '^',
|
|
11
|
+
filter = (fileType, file) => (file.indexOf('.') !== 0) && (file.slice(-(fileType.length + 1)) === `.${fileType}`),
|
|
12
|
+
mapReduction = (map, list) => list.reduce((obj, file) => {
|
|
13
|
+
const
|
|
14
|
+
id = file.substring(0, file.length - 4);
|
|
15
|
+
|
|
16
|
+
obj[id] = map[id];
|
|
17
|
+
return obj;
|
|
18
|
+
}, {}),
|
|
19
|
+
Transcription = class Transcription extends Cheer {
|
|
20
|
+
async prepare (data) {
|
|
21
|
+
this.differenceOnly = false;
|
|
22
|
+
this.updateList = null;
|
|
23
|
+
await this.replaceConfigPathWithJSON('script', 'files');
|
|
24
|
+
|
|
25
|
+
// create standardized transcripts and an accompanying hash for each.
|
|
26
|
+
const
|
|
27
|
+
{files, voice} = this.config,
|
|
28
|
+
standardizedFiles = {},
|
|
29
|
+
transcripts = {};
|
|
30
|
+
|
|
31
|
+
Object.keys(files).forEach((key) => {
|
|
32
|
+
const
|
|
33
|
+
transcript = new Transcript(files[key], {voice});
|
|
34
|
+
|
|
35
|
+
standardizedFiles[key] = transcript.toJSON();
|
|
36
|
+
transcripts[key] = transcript;
|
|
37
|
+
});
|
|
38
|
+
this.config.files = standardizedFiles;
|
|
39
|
+
this.transcripts = transcripts;
|
|
40
|
+
|
|
41
|
+
return super.prepare(data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async checkDifference () {
|
|
45
|
+
const
|
|
46
|
+
{config} = this,
|
|
47
|
+
{files, format = 'json', output, src} = config,
|
|
48
|
+
fileType = format.toLowerCase();
|
|
49
|
+
|
|
50
|
+
if (output) {
|
|
51
|
+
await ensureDir(output);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const
|
|
55
|
+
{captions: alreadyCaptioned, file: captionsFile, files: fileMap} = await getCaptions(output, fileType),
|
|
56
|
+
check = (id, caption) => {
|
|
57
|
+
const
|
|
58
|
+
file = fileMap?.[id],
|
|
59
|
+
original = alreadyCaptioned[id],
|
|
60
|
+
hash = file?.getHash();
|
|
61
|
+
|
|
62
|
+
delete alreadyCaptioned[id];
|
|
63
|
+
|
|
64
|
+
if (hash) {
|
|
65
|
+
const
|
|
66
|
+
same = hash === this.transcripts[id].getHash();
|
|
67
|
+
|
|
68
|
+
if (!same) {
|
|
69
|
+
console.log(`Updating "${id}".`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return same;
|
|
73
|
+
} else {
|
|
74
|
+
const
|
|
75
|
+
cap = (Array.isArray(caption) ? caption : [caption]).map((cap) => cap?.caption ?? cap ?? null).filter((cap) => cap !== null).join(' ').replaceAll(JOIN_CHARACTER, ' ').replaceAll(SEPARATE_CHARACTER, ' ');
|
|
76
|
+
|
|
77
|
+
if (cap && cap !== original) {
|
|
78
|
+
console.log(`Updating "${id}".`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// if a caption is not supplied, we'll assume the existence of an original caption means we don't need to redo.
|
|
82
|
+
return cap === '' || cap === original;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
mergeCaptions = async (newCaptions = false) => { // Must run _after_ all checks and will remove what's left.
|
|
86
|
+
const
|
|
87
|
+
olds = Object.keys(alreadyCaptioned);
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < olds.length; i++) {
|
|
90
|
+
const
|
|
91
|
+
caption = olds[i];
|
|
92
|
+
|
|
93
|
+
if (captionsFile) {
|
|
94
|
+
captionsFile.removeKey(caption);
|
|
95
|
+
} else {
|
|
96
|
+
await fs.rm(`${output}${caption}.${fileType}`);
|
|
97
|
+
}
|
|
98
|
+
console.log(`Removed "${caption}"`);
|
|
99
|
+
}
|
|
100
|
+
if (captionsFile) {
|
|
101
|
+
if (newCaptions) {
|
|
102
|
+
captionsFile.mergeData(await getJSON(`${output}captions.json`));
|
|
103
|
+
}
|
|
104
|
+
await captionsFile.save();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return olds.length;
|
|
108
|
+
},
|
|
109
|
+
list = (await readdirOrEmpty(src)).filter(filter.bind(null, 'mp3')).filter((file) => {
|
|
110
|
+
const
|
|
111
|
+
id = file.substring(0, file.length - 4),
|
|
112
|
+
caption = files?.[id];
|
|
113
|
+
|
|
114
|
+
return !check(id, caption);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.differenceOnly = true;
|
|
118
|
+
|
|
119
|
+
if (list.length === 0) {
|
|
120
|
+
// We'll still run the merge in case any have been removed.
|
|
121
|
+
if (await mergeCaptions()) {
|
|
122
|
+
throw Error('Old captions removed, but no new captions required generation.');
|
|
123
|
+
} else {
|
|
124
|
+
throw Error('Captions already up to date.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.updateList = list;
|
|
129
|
+
config.files = mapReduction(files, list);
|
|
130
|
+
this.mergeCaptions = mergeCaptions;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
beforeSend (archive) {
|
|
134
|
+
const
|
|
135
|
+
{src} = this.config;
|
|
136
|
+
|
|
137
|
+
if (this.differenceOnly) {
|
|
138
|
+
this.updateList.forEach((file) => archive.file(`${src}${file}`, {
|
|
139
|
+
name: file
|
|
140
|
+
}));
|
|
141
|
+
} else {
|
|
142
|
+
archive.directory(src, false);
|
|
143
|
+
}
|
|
144
|
+
super.beforeSend(archive);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async afterExport (...args) {
|
|
148
|
+
const
|
|
149
|
+
{config, transcripts, updateList} = this,
|
|
150
|
+
{format = 'json', output} = config;
|
|
151
|
+
|
|
152
|
+
if (this.mergeCaptions) {
|
|
153
|
+
await this.mergeCaptions(true);
|
|
154
|
+
}
|
|
155
|
+
if (format !== 'json') {
|
|
156
|
+
for (let i = 0; i < updateList.length; i++) {
|
|
157
|
+
const
|
|
158
|
+
filename = updateList[i],
|
|
159
|
+
id = filename.substring(0, filename.length - 4),
|
|
160
|
+
file = await getFileData(`${output}${id}.${format}`, format);
|
|
161
|
+
|
|
162
|
+
file.addHash(transcripts[id].getHash());
|
|
163
|
+
await file.save();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
super.afterExport(...args)
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
163
170
|
module.exports = Transcription;
|
package/src/formats/File.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
const
|
|
2
|
-
fs = require('fs').promises,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
this.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
const
|
|
2
|
+
fs = require('fs').promises,
|
|
3
|
+
{ensureParentDir} = require('../helpers/dirs'),
|
|
4
|
+
File = class {
|
|
5
|
+
constructor ({fileType}) {
|
|
6
|
+
this.data = null;
|
|
7
|
+
this.fileType = fileType;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async load (path) {
|
|
11
|
+
this.path = path;
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getHash () {
|
|
16
|
+
return null; // does not support hashes.
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async save () { // return a promise
|
|
20
|
+
await ensureParentDir(this.path);
|
|
21
|
+
return fs.writeFile(`${this.path}`, this.data ?? '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toString () {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
27
29
|
module.exports = File;
|
package/src/formats/JSONFile.js
CHANGED
|
@@ -1,40 +1,42 @@
|
|
|
1
|
-
const
|
|
2
|
-
crypto = require('crypto'),
|
|
3
|
-
fs = require('fs').promises,
|
|
4
|
-
File = require("./File"),
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
...data
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
const
|
|
2
|
+
crypto = require('crypto'),
|
|
3
|
+
fs = require('fs').promises,
|
|
4
|
+
File = require("./File"),
|
|
5
|
+
{ensureParentDir} = require('../helpers/dirs'),
|
|
6
|
+
getJSON = require('../helpers/getJSON'),
|
|
7
|
+
sortKeys = (obj) => Object.keys(obj).sort().reduce((s, key) => {
|
|
8
|
+
s[key] = obj[key];
|
|
9
|
+
return s;
|
|
10
|
+
}, {});
|
|
11
|
+
|
|
12
|
+
module.exports = class JSONFile extends File {
|
|
13
|
+
async load (path) {
|
|
14
|
+
await super.load(path);
|
|
15
|
+
this.data = await getJSON(path) ?? {};
|
|
16
|
+
return this.data;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getHash () {
|
|
20
|
+
return this.data ? crypto.createHash('sha256').update(JSON.stringify(this.data)).digest('hex') : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
toString () {
|
|
24
|
+
return Object.keys(this.data).map((key) => this.data[key].map(({content}) => content).join(' ')).join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
removeKey (key) {
|
|
28
|
+
delete this.data[key];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
mergeData (data) {
|
|
32
|
+
this.data = sortKeys({
|
|
33
|
+
...this.data,
|
|
34
|
+
...data
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async save () { // return a promise
|
|
39
|
+
await ensureParentDir(this.path);
|
|
40
|
+
return fs.writeFile(`${this.path}`, JSON.stringify(this.data, null, 4));
|
|
41
|
+
}
|
|
40
42
|
}
|
package/src/formats/LRC.js
CHANGED
|
@@ -1,38 +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
|
-
}
|
|
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
38
|
}
|