@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.
@@ -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 (speaker) {
49
- result.speaker = speaker;
50
- }
51
-
52
- if (resolvedVoice) {
53
- result.voice = resolvedVoice;
54
- }
55
-
56
- return sortKeys(result);
57
- });
58
- }
59
-
60
- getHash ({seed = ''} = {}) {
61
- return crypto.createHash('sha256').update(seed + JSON.stringify(this.data)).digest('hex');
62
- }
63
-
64
- toJSON () {
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;