@makefully/dutifully 1.10.1 → 1.11.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 +9 -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 +132 -132
- package/src/classes/Polly.js +9 -9
- package/src/classes/Rasterize.js +12 -12
- package/src/classes/TextToSpeech.js +204 -204
- package/src/classes/Transcript.js +72 -68
- package/src/classes/Transcription.js +162 -162
- package/src/formats/File.js +26 -26
- package/src/formats/JSONFile.js +39 -39
- package/src/formats/LRC.js +37 -37
- package/src/formats/MP3.js +59 -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/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,205 +1,205 @@
|
|
|
1
|
-
const
|
|
2
|
-
Cheer = require('./Cheer'),
|
|
3
|
-
Transcript = require('./Transcript'),
|
|
4
|
-
fs = require('fs').promises,
|
|
5
|
-
getFileData = require('../helpers/getFileData'),
|
|
6
|
-
id3 = require('node-id3').Promise,
|
|
7
|
-
isoLanguageMap = {
|
|
8
|
-
'en-US': 'eng',
|
|
9
|
-
'es-ES': 'spa'
|
|
10
|
-
},
|
|
11
|
-
matches = (file, types) => types.reduce((prev, current) => prev || (file.slice(-(current.length + 1)) === `.${current}`), false),
|
|
12
|
-
getMP3s = async ({encodedBy = '', output = './', fileTypes = [], transcripts}) => {
|
|
13
|
-
const
|
|
14
|
-
all = (await fs.readdir(output))
|
|
15
|
-
.filter((file) => (file.indexOf('.') !== 0) && (fileTypes.length === 0 || matches(file, fileTypes)));
|
|
16
|
-
|
|
17
|
-
if (transcripts) {
|
|
18
|
-
const
|
|
19
|
-
keys = Object.keys(transcripts),
|
|
20
|
-
list = keys.map((id) => `${id}.mp3`),
|
|
21
|
-
missing = [],
|
|
22
|
-
present = [];
|
|
23
|
-
|
|
24
|
-
// need to check for presence _and_ whether lyrics match.
|
|
25
|
-
for (let i = 0; i < list.length; i++) {
|
|
26
|
-
const
|
|
27
|
-
file = list[i];
|
|
28
|
-
|
|
29
|
-
if (all.indexOf(file) === -1) {
|
|
30
|
-
missing.push(file)
|
|
31
|
-
} else { // not actually missing, so let's check the artist and lyrics.
|
|
32
|
-
const
|
|
33
|
-
fileHash = (await getFileData(`${output}${file}`, 'mp3')).getHash({
|
|
34
|
-
hashKey: 'GenerationHash'
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// We'll assume the original file should be kept if it doesn't include a generation hash.
|
|
38
|
-
if (fileHash && fileHash !== transcripts[keys[i]].getHash({
|
|
39
|
-
seed: encodedBy
|
|
40
|
-
})) {
|
|
41
|
-
missing.push(file);
|
|
42
|
-
} else {
|
|
43
|
-
present.push(file);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
missing,
|
|
50
|
-
present,
|
|
51
|
-
unlisted: all.filter((id) => list.indexOf(id) === -1),
|
|
52
|
-
all
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
all
|
|
58
|
-
};
|
|
59
|
-
},
|
|
60
|
-
captionText = (caption) => (Array.isArray(caption) ? caption : [caption])
|
|
61
|
-
.map((cap) => cap?.caption ?? cap ?? null)
|
|
62
|
-
.filter((cap) => cap !== null)
|
|
63
|
-
.join(' '),
|
|
64
|
-
clamp = (text, max) => {
|
|
65
|
-
if (text.length <= max) {
|
|
66
|
-
return text;
|
|
67
|
-
} else {
|
|
68
|
-
const
|
|
69
|
-
arr = text.substring(0, max).split(' ');
|
|
70
|
-
|
|
71
|
-
if (arr.length > 1) {
|
|
72
|
-
arr.length -= 1; // remove last word which may be partial.
|
|
73
|
-
return `${arr.join(' ')}...`;
|
|
74
|
-
} else {
|
|
75
|
-
return arr[0];
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
appendMP3Meta = ({album, captions, generated = false, language, output, voice}) => {
|
|
80
|
-
Object.keys(captions).forEach(async (id) => {
|
|
81
|
-
const
|
|
82
|
-
text = captionText(captions[id]),
|
|
83
|
-
tags = {
|
|
84
|
-
album,
|
|
85
|
-
title: clamp(text, 40),
|
|
86
|
-
unsynchronisedLyrics: {
|
|
87
|
-
language: isoLanguageMap[language] ?? 'eng',
|
|
88
|
-
text
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
await id3.update(tags, `${output}${id}.mp3`);
|
|
93
|
-
|
|
94
|
-
if (generated) {
|
|
95
|
-
console.log(`Voice generated for "${id}".`);
|
|
96
|
-
} else {
|
|
97
|
-
console.log(`Voice meta data updated for "${id}".`);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
},
|
|
101
|
-
mapReduction = (map, list) => list.reduce((obj, file) => {
|
|
102
|
-
const
|
|
103
|
-
id = file.substring(0, file.length - 4);
|
|
104
|
-
|
|
105
|
-
obj[id] = map[id];
|
|
106
|
-
return obj;
|
|
107
|
-
}, {}),
|
|
108
|
-
TextToSpeech = class extends Cheer {
|
|
109
|
-
constructor (data) {
|
|
110
|
-
const
|
|
111
|
-
{album, encodedBy} = data;
|
|
112
|
-
|
|
113
|
-
super(data);
|
|
114
|
-
this.album = album;
|
|
115
|
-
this.encodedBy = encodedBy;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async prepare (data) {
|
|
119
|
-
await this.replaceConfigPathWithJSON('script', 'files');
|
|
120
|
-
|
|
121
|
-
// create standardized transcripts and an accompanying hash for each.
|
|
122
|
-
const
|
|
123
|
-
{files, voice} = this.config,
|
|
124
|
-
standardizedFiles = {},
|
|
125
|
-
transcripts = {};
|
|
126
|
-
|
|
127
|
-
Object.keys(files).forEach((key) => {
|
|
128
|
-
const
|
|
129
|
-
transcript = new Transcript(files[key], {voice});
|
|
130
|
-
|
|
131
|
-
standardizedFiles[key] = transcript.toJSON();
|
|
132
|
-
transcripts[key] = transcript;
|
|
133
|
-
});
|
|
134
|
-
this.config.files = standardizedFiles;
|
|
135
|
-
this.transcripts = transcripts;
|
|
136
|
-
|
|
137
|
-
return super.prepare(data);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async checkDifference () {
|
|
141
|
-
const
|
|
142
|
-
{album, config, encodedBy = '', transcripts} = this,
|
|
143
|
-
{output, files = {}, language, updateAllMetaData = false} = config,
|
|
144
|
-
{missing, present, unlisted} = await getMP3s({
|
|
145
|
-
encodedBy,
|
|
146
|
-
fileTypes: ['mp3'],
|
|
147
|
-
output,
|
|
148
|
-
transcripts
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
config.files = mapReduction(files, missing);
|
|
152
|
-
|
|
153
|
-
for (let i = 0; i < unlisted.length; i++) {
|
|
154
|
-
await fs.rm(`${output}${unlisted[i]}`);
|
|
155
|
-
console.log(`Removed "${unlisted[i]}"`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (updateAllMetaData && present.length) {
|
|
159
|
-
appendMP3Meta({
|
|
160
|
-
album,
|
|
161
|
-
captions: mapReduction(files, present),
|
|
162
|
-
language,
|
|
163
|
-
output
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (missing.length === 0) {
|
|
168
|
-
throw Error('All voice-over files already exist.');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
this.updateList = missing;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async afterExport (...args) {
|
|
175
|
-
const
|
|
176
|
-
{album, encodedBy, config, transcripts, updateList} = this,
|
|
177
|
-
{files, output, language, voice} = config,
|
|
178
|
-
list = updateList ?? Object.keys(files).map((id) => `${id}.mp3`);
|
|
179
|
-
|
|
180
|
-
appendMP3Meta({
|
|
181
|
-
album,
|
|
182
|
-
captions: files,
|
|
183
|
-
generated: true,
|
|
184
|
-
language,
|
|
185
|
-
output,
|
|
186
|
-
voice
|
|
187
|
-
});
|
|
188
|
-
for (let i = 0; i < list.length; i++) {
|
|
189
|
-
const
|
|
190
|
-
filename = list[i],
|
|
191
|
-
id = filename.substring(0, filename.length - 4),
|
|
192
|
-
file = await getFileData(`${output}${filename}`, 'mp3');
|
|
193
|
-
|
|
194
|
-
file.addHash(transcripts[id].getHash({
|
|
195
|
-
seed: encodedBy ?? ''
|
|
196
|
-
}), {
|
|
197
|
-
hashKey: 'GenerationHash'
|
|
198
|
-
});
|
|
199
|
-
await file.save();
|
|
200
|
-
}
|
|
201
|
-
super.afterExport(...args);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
1
|
+
const
|
|
2
|
+
Cheer = require('./Cheer'),
|
|
3
|
+
Transcript = require('./Transcript'),
|
|
4
|
+
fs = require('fs').promises,
|
|
5
|
+
getFileData = require('../helpers/getFileData'),
|
|
6
|
+
id3 = require('node-id3').Promise,
|
|
7
|
+
isoLanguageMap = {
|
|
8
|
+
'en-US': 'eng',
|
|
9
|
+
'es-ES': 'spa'
|
|
10
|
+
},
|
|
11
|
+
matches = (file, types) => types.reduce((prev, current) => prev || (file.slice(-(current.length + 1)) === `.${current}`), false),
|
|
12
|
+
getMP3s = async ({encodedBy = '', output = './', fileTypes = [], transcripts}) => {
|
|
13
|
+
const
|
|
14
|
+
all = (await fs.readdir(output))
|
|
15
|
+
.filter((file) => (file.indexOf('.') !== 0) && (fileTypes.length === 0 || matches(file, fileTypes)));
|
|
16
|
+
|
|
17
|
+
if (transcripts) {
|
|
18
|
+
const
|
|
19
|
+
keys = Object.keys(transcripts),
|
|
20
|
+
list = keys.map((id) => `${id}.mp3`),
|
|
21
|
+
missing = [],
|
|
22
|
+
present = [];
|
|
23
|
+
|
|
24
|
+
// need to check for presence _and_ whether lyrics match.
|
|
25
|
+
for (let i = 0; i < list.length; i++) {
|
|
26
|
+
const
|
|
27
|
+
file = list[i];
|
|
28
|
+
|
|
29
|
+
if (all.indexOf(file) === -1) {
|
|
30
|
+
missing.push(file)
|
|
31
|
+
} else { // not actually missing, so let's check the artist and lyrics.
|
|
32
|
+
const
|
|
33
|
+
fileHash = (await getFileData(`${output}${file}`, 'mp3')).getHash({
|
|
34
|
+
hashKey: 'GenerationHash'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// We'll assume the original file should be kept if it doesn't include a generation hash.
|
|
38
|
+
if (fileHash && fileHash !== transcripts[keys[i]].getHash({
|
|
39
|
+
seed: encodedBy
|
|
40
|
+
})) {
|
|
41
|
+
missing.push(file);
|
|
42
|
+
} else {
|
|
43
|
+
present.push(file);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
missing,
|
|
50
|
+
present,
|
|
51
|
+
unlisted: all.filter((id) => list.indexOf(id) === -1),
|
|
52
|
+
all
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
all
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
captionText = (caption) => (Array.isArray(caption) ? caption : [caption])
|
|
61
|
+
.map((cap) => cap?.caption ?? cap ?? null)
|
|
62
|
+
.filter((cap) => cap !== null)
|
|
63
|
+
.join(' '),
|
|
64
|
+
clamp = (text, max) => {
|
|
65
|
+
if (text.length <= max) {
|
|
66
|
+
return text;
|
|
67
|
+
} else {
|
|
68
|
+
const
|
|
69
|
+
arr = text.substring(0, max).split(' ');
|
|
70
|
+
|
|
71
|
+
if (arr.length > 1) {
|
|
72
|
+
arr.length -= 1; // remove last word which may be partial.
|
|
73
|
+
return `${arr.join(' ')}...`;
|
|
74
|
+
} else {
|
|
75
|
+
return arr[0];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
appendMP3Meta = ({album, captions, generated = false, language, output, voice}) => {
|
|
80
|
+
Object.keys(captions).forEach(async (id) => {
|
|
81
|
+
const
|
|
82
|
+
text = captionText(captions[id]),
|
|
83
|
+
tags = {
|
|
84
|
+
album,
|
|
85
|
+
title: clamp(text, 40),
|
|
86
|
+
unsynchronisedLyrics: {
|
|
87
|
+
language: isoLanguageMap[language] ?? 'eng',
|
|
88
|
+
text
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
await id3.update(tags, `${output}${id}.mp3`);
|
|
93
|
+
|
|
94
|
+
if (generated) {
|
|
95
|
+
console.log(`Voice generated for "${id}".`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`Voice meta data updated for "${id}".`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
mapReduction = (map, list) => list.reduce((obj, file) => {
|
|
102
|
+
const
|
|
103
|
+
id = file.substring(0, file.length - 4);
|
|
104
|
+
|
|
105
|
+
obj[id] = map[id];
|
|
106
|
+
return obj;
|
|
107
|
+
}, {}),
|
|
108
|
+
TextToSpeech = class extends Cheer {
|
|
109
|
+
constructor (data) {
|
|
110
|
+
const
|
|
111
|
+
{album, encodedBy} = data;
|
|
112
|
+
|
|
113
|
+
super(data);
|
|
114
|
+
this.album = album;
|
|
115
|
+
this.encodedBy = encodedBy;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async prepare (data) {
|
|
119
|
+
await this.replaceConfigPathWithJSON('script', 'files');
|
|
120
|
+
|
|
121
|
+
// create standardized transcripts and an accompanying hash for each.
|
|
122
|
+
const
|
|
123
|
+
{files, voice} = this.config,
|
|
124
|
+
standardizedFiles = {},
|
|
125
|
+
transcripts = {};
|
|
126
|
+
|
|
127
|
+
Object.keys(files).forEach((key) => {
|
|
128
|
+
const
|
|
129
|
+
transcript = new Transcript(files[key], {voice});
|
|
130
|
+
|
|
131
|
+
standardizedFiles[key] = transcript.toJSON();
|
|
132
|
+
transcripts[key] = transcript;
|
|
133
|
+
});
|
|
134
|
+
this.config.files = standardizedFiles;
|
|
135
|
+
this.transcripts = transcripts;
|
|
136
|
+
|
|
137
|
+
return super.prepare(data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async checkDifference () {
|
|
141
|
+
const
|
|
142
|
+
{album, config, encodedBy = '', transcripts} = this,
|
|
143
|
+
{output, files = {}, language, updateAllMetaData = false} = config,
|
|
144
|
+
{missing, present, unlisted} = await getMP3s({
|
|
145
|
+
encodedBy,
|
|
146
|
+
fileTypes: ['mp3'],
|
|
147
|
+
output,
|
|
148
|
+
transcripts
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
config.files = mapReduction(files, missing);
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < unlisted.length; i++) {
|
|
154
|
+
await fs.rm(`${output}${unlisted[i]}`);
|
|
155
|
+
console.log(`Removed "${unlisted[i]}"`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (updateAllMetaData && present.length) {
|
|
159
|
+
appendMP3Meta({
|
|
160
|
+
album,
|
|
161
|
+
captions: mapReduction(files, present),
|
|
162
|
+
language,
|
|
163
|
+
output
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (missing.length === 0) {
|
|
168
|
+
throw Error('All voice-over files already exist.');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.updateList = missing;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async afterExport (...args) {
|
|
175
|
+
const
|
|
176
|
+
{album, encodedBy, config, transcripts, updateList} = this,
|
|
177
|
+
{files, output, language, voice} = config,
|
|
178
|
+
list = updateList ?? Object.keys(files).map((id) => `${id}.mp3`);
|
|
179
|
+
|
|
180
|
+
appendMP3Meta({
|
|
181
|
+
album,
|
|
182
|
+
captions: files,
|
|
183
|
+
generated: true,
|
|
184
|
+
language,
|
|
185
|
+
output,
|
|
186
|
+
voice
|
|
187
|
+
});
|
|
188
|
+
for (let i = 0; i < list.length; i++) {
|
|
189
|
+
const
|
|
190
|
+
filename = list[i],
|
|
191
|
+
id = filename.substring(0, filename.length - 4),
|
|
192
|
+
file = await getFileData(`${output}${filename}`, 'mp3');
|
|
193
|
+
|
|
194
|
+
file.addHash(transcripts[id].getHash({
|
|
195
|
+
seed: encodedBy ?? ''
|
|
196
|
+
}), {
|
|
197
|
+
hashKey: 'GenerationHash'
|
|
198
|
+
});
|
|
199
|
+
await file.save();
|
|
200
|
+
}
|
|
201
|
+
super.afterExport(...args);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
205
|
module.exports = TextToSpeech;
|
|
@@ -1,69 +1,73 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Processes and manages transcript data with voice resolution and key normalization.
|
|
3
|
-
*
|
|
4
|
-
* This class transforms raw transcript input into a standardized format. It ensures
|
|
5
|
-
* every line has an assigned voice, sorts object keys for consistent hashing,
|
|
6
|
-
* and provides utility for generating integrity hashes of the content.
|
|
7
|
-
*
|
|
8
|
-
* @class Transcript
|
|
9
|
-
* @param {string|string[]|Object|Object[]} transcript - The raw dialogue or data lines.
|
|
10
|
-
* @param {Object} options - Configuration object.
|
|
11
|
-
* @param {string|Object} options.voice - A single voice ID or a mapping of speaker names to IDs.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
crypto = require('crypto'),
|
|
16
|
-
sortKeys = (obj) => Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b))),
|
|
17
|
-
resolveVoice = (voice, speaker) => {
|
|
18
|
-
if (typeof voice === 'string') {
|
|
19
|
-
return voice;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (speaker && voice[speaker]) {
|
|
23
|
-
return voice[speaker];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return voice.default ?? Object.values(voice)[0];
|
|
27
|
-
},
|
|
28
|
-
Transcript = class {
|
|
29
|
-
constructor (transcript, {voice: voiceOrVoiceMap}) {
|
|
30
|
-
|
|
31
|
-
this.data = (Array.isArray(transcript) ? transcript : [transcript]).map((line) => {
|
|
32
|
-
if (typeof line === 'string') {
|
|
33
|
-
const
|
|
34
|
-
result = {caption: line};
|
|
35
|
-
|
|
36
|
-
if (voiceOrVoiceMap) {
|
|
37
|
-
result.voice = resolveVoice(voiceOrVoiceMap);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
{caption, speaker, voice} = line,
|
|
45
|
-
resolvedVoice = voice ?? (voiceOrVoiceMap ? resolveVoice(voiceOrVoiceMap, speaker) : null),
|
|
46
|
-
result = {caption};
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
result.
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
result.
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return this.data;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Processes and manages transcript data with voice resolution and key normalization.
|
|
3
|
+
*
|
|
4
|
+
* This class transforms raw transcript input into a standardized format. It ensures
|
|
5
|
+
* every line has an assigned voice, sorts object keys for consistent hashing,
|
|
6
|
+
* and provides utility for generating integrity hashes of the content.
|
|
7
|
+
*
|
|
8
|
+
* @class Transcript
|
|
9
|
+
* @param {string|string[]|Object|Object[]} transcript - The raw dialogue or data lines.
|
|
10
|
+
* @param {Object} options - Configuration object.
|
|
11
|
+
* @param {string|Object} options.voice - A single voice ID or a mapping of speaker names to IDs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const
|
|
15
|
+
crypto = require('crypto'),
|
|
16
|
+
sortKeys = (obj) => Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b))),
|
|
17
|
+
resolveVoice = (voice, speaker) => {
|
|
18
|
+
if (typeof voice === 'string') {
|
|
19
|
+
return voice;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (speaker && voice[speaker]) {
|
|
23
|
+
return voice[speaker];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return voice.default ?? Object.values(voice)[0];
|
|
27
|
+
},
|
|
28
|
+
Transcript = class {
|
|
29
|
+
constructor (transcript, {voice: voiceOrVoiceMap}) {
|
|
30
|
+
|
|
31
|
+
this.data = (Array.isArray(transcript) ? transcript : [transcript]).map((line) => {
|
|
32
|
+
if (typeof line === 'string') {
|
|
33
|
+
const
|
|
34
|
+
result = {caption: line};
|
|
35
|
+
|
|
36
|
+
if (voiceOrVoiceMap) {
|
|
37
|
+
result.voice = resolveVoice(voiceOrVoiceMap);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const
|
|
44
|
+
{caption, class: lineClass, speaker, voice} = line,
|
|
45
|
+
resolvedVoice = voice ?? (voiceOrVoiceMap ? resolveVoice(voiceOrVoiceMap, speaker) : null),
|
|
46
|
+
result = {caption};
|
|
47
|
+
|
|
48
|
+
if (lineClass) {
|
|
49
|
+
result.class = lineClass;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (speaker) {
|
|
53
|
+
result.speaker = speaker;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (resolvedVoice) {
|
|
57
|
+
result.voice = resolvedVoice;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sortKeys(result);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getHash ({seed = ''} = {}) {
|
|
65
|
+
return crypto.createHash('sha256').update(seed + JSON.stringify(this.data)).digest('hex');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
toJSON () {
|
|
69
|
+
return this.data;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
69
73
|
module.exports = Transcript;
|