@makefully/dutifully 1.13.1 → 1.14.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 +9 -0
- package/README.md +66 -5
- package/package.json +2 -2
- package/src/classes/Sharp.js +15 -0
- package/src/classes/Stackfully.js +15 -0
- package/src/classes/Transcription.js +51 -2
- package/src/helpers/getCaptions.js +21 -2
- package/src/index.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.1
|
|
4
|
+
|
|
5
|
+
- Upgrade `archiver` so `directory()` works when a modern `glob` is hoisted (fixes `TypeError: glob is not a function`).
|
|
6
|
+
|
|
7
|
+
## 1.14.0
|
|
8
|
+
|
|
9
|
+
- Add `sharp` service client for image convert/compress/resize, thumbnails, and favicon packs.
|
|
10
|
+
- Add `stackfully` service client for multi-layer image compositing.
|
|
11
|
+
|
|
3
12
|
## 1.13.1
|
|
4
13
|
|
|
5
14
|
- Harden `say` / ElevenLabs export when the result zip has no MP3 (clearer error, no unhandled ENOENT from ID3 metadata).
|
package/README.md
CHANGED
|
@@ -76,14 +76,14 @@ This text-to-voice service requires a script as input and will download audio VO
|
|
|
76
76
|
"output": "./example/vo/en/",
|
|
77
77
|
"model": "eleven_multilingual_v1",
|
|
78
78
|
"updateAllMetaData": true,
|
|
79
|
-
"voice": "
|
|
79
|
+
"voice": "21m00Tcm4TlvDq8ikWAM"
|
|
80
80
|
}]
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
You may set a single `voice` value
|
|
84
|
+
You may set a single `voice` value to an ElevenLabs voice id, or an object of speaker name keys mapped to voice ids. The speaker names must match speakers listed in the script. If "default" is provided as a key, this voice id will be used for any scripts not specifying a speaker.
|
|
85
85
|
|
|
86
|
-
Specifying the `model` is optional: if unspecified, Cheerfully
|
|
86
|
+
Specifying the `model` is optional: if unspecified, Cheerfully tries to look up the voice id and pick a good model; if lookup fails it falls back to `eleven_multilingual_v2`.
|
|
87
87
|
|
|
88
88
|
Specifying `updateAllMetaData` is `false` by default. If it's set to `true`, album, title, and unsynchronized lyrics are appended to any MP3's in the source directory that were not generated.
|
|
89
89
|
|
|
@@ -154,6 +154,67 @@ Use `exports: ["texturepacker", "createjs"]` (or `format`) to choose one or more
|
|
|
154
154
|
|
|
155
155
|
Spacing: `padding` is empty gap beyond `extrude`; `extrude` is edge-pixel bleed into that gap; `border` is margin from the atlas edge.
|
|
156
156
|
|
|
157
|
+
### Sharp
|
|
158
|
+
|
|
159
|
+
This still-image service converts, compresses, resizes, and can emit multi-size thumbnails or favicon packs (including `.ico`). Every image under `src` is processed; `files` overrides are optional.
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
{
|
|
163
|
+
"sharp": [{
|
|
164
|
+
"src": "./example/images/",
|
|
165
|
+
"output": "./example/out/",
|
|
166
|
+
"options": {
|
|
167
|
+
"format": "webp",
|
|
168
|
+
"quality": 80,
|
|
169
|
+
"resize": { "width": 1920, "fit": "inside", "withoutEnlargement": true }
|
|
170
|
+
},
|
|
171
|
+
"files": {
|
|
172
|
+
"logo.png": {
|
|
173
|
+
"format": ["ico", "png"],
|
|
174
|
+
"sizes": [16, 32, 48, 64, 128, 256],
|
|
175
|
+
"prefix": "favicon"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Stackfully
|
|
183
|
+
|
|
184
|
+
This multi-layer compositing service builds sized outputs from ordered image layers (positioning, blend, alpha). Every image under `src` is available as a layer source; define composites under `outputs`.
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
{
|
|
188
|
+
"stackfully": [{
|
|
189
|
+
"src": "./example/images/",
|
|
190
|
+
"output": "./example/composites/",
|
|
191
|
+
"options": {
|
|
192
|
+
"format": "png"
|
|
193
|
+
},
|
|
194
|
+
"files": {
|
|
195
|
+
"badge.png": { "composite": true }
|
|
196
|
+
},
|
|
197
|
+
"outputs": {
|
|
198
|
+
"hero.png": {
|
|
199
|
+
"width": 1024,
|
|
200
|
+
"height": 1024,
|
|
201
|
+
"format": "png",
|
|
202
|
+
"layers": [
|
|
203
|
+
"background.png",
|
|
204
|
+
{
|
|
205
|
+
"input": "foreground.png",
|
|
206
|
+
"alpha": 0.85,
|
|
207
|
+
"blend": "over",
|
|
208
|
+
"bottom": 0,
|
|
209
|
+
"right": 0
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}]
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
157
218
|
### Polly
|
|
158
219
|
|
|
159
220
|
This text-to-voice service requires a script as input and will download audio VO generated by Amazon Polly. You can set Polly settings in `cheerfully.json` like so:
|
|
@@ -253,8 +314,8 @@ Script lines may include an optional `class` (for example `"title"` or `"stage"`
|
|
|
253
314
|
Script lines may also include an optional `events` array for one-shot timed markers. Entries may be:
|
|
254
315
|
|
|
255
316
|
* a **number** — add to the running absolute time offset (ms)
|
|
256
|
-
* a **string** `"<phrase"` / `">phrase"` — set the offset to just before/after that phrase in the caption; any other string fires that event at the current offset
|
|
257
|
-
* an **object** `{ "event", "time"?, "before"?, "after"? }` — fires `event`; optional fields set the absolute offset (`time` wins over `before` over `after
|
|
317
|
+
* a **string** `"<phrase"` / `">phrase"` — set the offset to just before/after that phrase in the caption (first word may be an incomplete suffix, last word an incomplete prefix); unmatched phrases leave the offset unchanged and are warned/logged for the client; any other string fires that event at the current offset
|
|
318
|
+
* an **object** `{ "event", "time"?, "before"?, "after"? }` — fires `event`; optional fields set the absolute offset (`time` wins over `before` over `after`; same phrase edge rules)
|
|
258
319
|
|
|
259
320
|
Cheerfully resolves these against caption timings and exports them in parallel using the same caption `format` (`events.json`, `{id}.events.vtt` / `.srt` / `.sami` / `.smi` / `.lrc`, or a separate MP3 SYLT frame with content type `EVENTS` labeled `"events"`).
|
|
260
321
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makefully/dutifully",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"description": "Client tools for Cheerfully service.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"node": ">=18"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"archiver": "^
|
|
62
|
+
"archiver": "^7.0.1",
|
|
63
63
|
"node-id3": "^0.2.6",
|
|
64
64
|
"node-web-audio-api": "^2.0.0",
|
|
65
65
|
"unzip-stream": "^0.3.0"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const
|
|
2
|
+
Cheer = require('./Cheer'),
|
|
3
|
+
Sharp = class Sharp extends Cheer {
|
|
4
|
+
beforeSend (archive) {
|
|
5
|
+
const
|
|
6
|
+
{src} = this.config;
|
|
7
|
+
|
|
8
|
+
if (src) {
|
|
9
|
+
archive.directory(src, false);
|
|
10
|
+
}
|
|
11
|
+
super.beforeSend(archive);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
module.exports = Sharp;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const
|
|
2
|
+
Cheer = require('./Cheer'),
|
|
3
|
+
Stackfully = class Stackfully extends Cheer {
|
|
4
|
+
beforeSend (archive) {
|
|
5
|
+
const
|
|
6
|
+
{src} = this.config;
|
|
7
|
+
|
|
8
|
+
if (src) {
|
|
9
|
+
archive.directory(src, false);
|
|
10
|
+
}
|
|
11
|
+
super.beforeSend(archive);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
module.exports = Stackfully;
|
|
@@ -53,6 +53,8 @@ const
|
|
|
53
53
|
|
|
54
54
|
const
|
|
55
55
|
{captions: alreadyCaptioned, file: captionsFile, files: fileMap} = await getCaptions(output, fileType),
|
|
56
|
+
eventsJsonExisting = fileType === 'json' ? await getJSON(`${output}events.json`) : null,
|
|
57
|
+
eventsFile = eventsJsonExisting ? await getFileData(`${output}events.json`, 'json') : null,
|
|
56
58
|
check = (id, caption) => {
|
|
57
59
|
const
|
|
58
60
|
file = fileMap?.[id],
|
|
@@ -82,6 +84,17 @@ const
|
|
|
82
84
|
return cap === '' || cap === original;
|
|
83
85
|
}
|
|
84
86
|
},
|
|
87
|
+
saveEventsFile = async () => {
|
|
88
|
+
if (!eventsFile) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (Object.keys(eventsFile.data ?? {}).length === 0) {
|
|
93
|
+
await fs.rm(eventsFile.path, {force: true});
|
|
94
|
+
} else {
|
|
95
|
+
await eventsFile.save();
|
|
96
|
+
}
|
|
97
|
+
},
|
|
85
98
|
mergeCaptions = async (newCaptions = false) => { // Must run _after_ all checks and will remove what's left.
|
|
86
99
|
const
|
|
87
100
|
olds = Object.keys(alreadyCaptioned);
|
|
@@ -92,14 +105,40 @@ const
|
|
|
92
105
|
|
|
93
106
|
if (captionsFile) {
|
|
94
107
|
captionsFile.removeKey(caption);
|
|
108
|
+
eventsFile?.removeKey(caption);
|
|
95
109
|
} else {
|
|
96
|
-
await fs.rm(`${output}${caption}.${fileType}
|
|
110
|
+
await fs.rm(`${output}${caption}.${fileType}`, {force: true});
|
|
111
|
+
// Remove parallel event export for the same caption id, if present.
|
|
112
|
+
await fs.rm(`${output}${caption}.events.${fileType}`, {force: true});
|
|
97
113
|
}
|
|
98
114
|
console.log(`Removed "${caption}"`);
|
|
99
115
|
}
|
|
100
116
|
if (captionsFile) {
|
|
101
117
|
if (newCaptions) {
|
|
102
118
|
captionsFile.mergeData(await getJSON(`${output}captions.json`));
|
|
119
|
+
|
|
120
|
+
// Drop events for every regenerated caption, then merge whatever the server returned.
|
|
121
|
+
// Captions that no longer export events stay removed (no orphan keys).
|
|
122
|
+
const
|
|
123
|
+
updated = this.updateList ?? [],
|
|
124
|
+
newEvents = await getJSON(`${output}events.json`) ?? {};
|
|
125
|
+
|
|
126
|
+
updated.forEach((filename) => {
|
|
127
|
+
const
|
|
128
|
+
id = filename.substring(0, filename.length - 4);
|
|
129
|
+
|
|
130
|
+
eventsFile?.removeKey(id);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (eventsFile) {
|
|
134
|
+
eventsFile.mergeData(newEvents);
|
|
135
|
+
await saveEventsFile();
|
|
136
|
+
} else if (Object.keys(newEvents).length === 0) {
|
|
137
|
+
await fs.rm(`${output}events.json`, {force: true});
|
|
138
|
+
}
|
|
139
|
+
// else: first events.json for this output — unzipped partial is already correct.
|
|
140
|
+
} else if (olds.length) {
|
|
141
|
+
await saveEventsFile();
|
|
103
142
|
}
|
|
104
143
|
await captionsFile.save();
|
|
105
144
|
}
|
|
@@ -128,6 +167,16 @@ const
|
|
|
128
167
|
this.updateList = list;
|
|
129
168
|
config.files = mapReduction(files, list);
|
|
130
169
|
this.mergeCaptions = mergeCaptions;
|
|
170
|
+
|
|
171
|
+
// Sidecar event files are not overwritten when the server omits them — clear before regenerate.
|
|
172
|
+
if (fileType !== 'json' && fileType !== 'mp3') {
|
|
173
|
+
for (let i = 0; i < list.length; i++) {
|
|
174
|
+
const
|
|
175
|
+
id = list[i].substring(0, list[i].length - 4);
|
|
176
|
+
|
|
177
|
+
await fs.rm(`${output}${id}.events.${fileType}`, {force: true});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
131
180
|
}
|
|
132
181
|
|
|
133
182
|
beforeSend (archive) {
|
|
@@ -167,4 +216,4 @@ const
|
|
|
167
216
|
}
|
|
168
217
|
};
|
|
169
218
|
|
|
170
|
-
module.exports = Transcription;
|
|
219
|
+
module.exports = Transcription;
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
const
|
|
2
2
|
getFileData = require("./getFileData"),
|
|
3
3
|
{readdirOrEmpty} = require("./dirs"),
|
|
4
|
-
filter = (fileType, file) =>
|
|
4
|
+
filter = (fileType, file) => {
|
|
5
|
+
if (file.indexOf('.') === 0) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (file.slice(-(fileType.length + 1)) !== `.${fileType}`) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Parallel event exports are `{id}.events.{format}` — not caption tracks.
|
|
14
|
+
const
|
|
15
|
+
base = file.slice(0, -(fileType.length + 1));
|
|
16
|
+
|
|
17
|
+
return !base.endsWith('.events');
|
|
18
|
+
};
|
|
5
19
|
|
|
6
20
|
module.exports = async (path, fileType) => {
|
|
7
21
|
|
|
@@ -13,6 +27,11 @@ module.exports = async (path, fileType) => {
|
|
|
13
27
|
|
|
14
28
|
return {
|
|
15
29
|
captions: Object.keys(json ?? {}).reduce((obj, key) => {
|
|
30
|
+
// Guard against any legacy/mistaken event keys in captions.json.
|
|
31
|
+
if (key.endsWith('.events')) {
|
|
32
|
+
return obj;
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
obj[key] = json[key].map(({content}) => content).join(' ');
|
|
17
36
|
return obj;
|
|
18
37
|
}, {}),
|
|
@@ -41,4 +60,4 @@ module.exports = async (path, fileType) => {
|
|
|
41
60
|
files: fileMap
|
|
42
61
|
};
|
|
43
62
|
}
|
|
44
|
-
};
|
|
63
|
+
};
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,8 @@ const
|
|
|
10
10
|
polly: require('./classes/Polly'),
|
|
11
11
|
rasterize: require('./classes/Rasterize'),
|
|
12
12
|
rhubarb: LipSync,
|
|
13
|
+
sharp: require('./classes/Sharp'),
|
|
14
|
+
stackfully: require('./classes/Stackfully'),
|
|
13
15
|
transcription: require('./classes/Transcription')
|
|
14
16
|
},
|
|
15
17
|
getJSON = require('./helpers/getJSON'),
|