@makefully/dutifully 1.12.1 → 1.13.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 +8 -0
- package/README.md +47 -2
- package/package.json +6 -2
- package/src/classes/Packfully.js +15 -0
- package/src/classes/TextToSpeech.js +40 -6
- package/src/index.js +1 -0
- package/src/say.js +29 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.1
|
|
4
|
+
|
|
5
|
+
- Harden `say` / ElevenLabs export when the result zip has no MP3 (clearer error, no unhandled ENOENT from ID3 metadata).
|
|
6
|
+
|
|
7
|
+
## 1.13.0
|
|
8
|
+
|
|
9
|
+
- Add `packfully` service client for packing images into texture atlases.
|
|
10
|
+
|
|
3
11
|
## 1.12.1
|
|
4
12
|
|
|
5
13
|
- Preserve optional script-line `events` through Transcript for timed marker definitions.
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ cheerfully({
|
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
### Say (spoken notifications)
|
|
44
44
|
|
|
45
45
|
`say` generates short spoken phrases through Cheerfully's ElevenLabs service, caches the audio under `.cache/tts/`, and plays it locally via `node-web-audio-api`. Use it from npm scripts instead of wiring ElevenLabs into the consuming project:
|
|
46
46
|
|
|
@@ -109,6 +109,51 @@ This audio file process accepts ffmpeg commandline arguments to perform operatio
|
|
|
109
109
|
}
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
### Packfully
|
|
113
|
+
|
|
114
|
+
This image-to-atlas service packs PNG/JPG/WebP sprites into texture atlases and downloads atlas PNG(s) plus descriptor file(s).
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
{
|
|
118
|
+
"packfully": [{
|
|
119
|
+
"format": "texturepacker",
|
|
120
|
+
"src": "./example/sprites/",
|
|
121
|
+
"output": "./example/atlases/",
|
|
122
|
+
"options": {
|
|
123
|
+
"maxWidth": 2048,
|
|
124
|
+
"maxHeight": 2048,
|
|
125
|
+
"pot": true,
|
|
126
|
+
"smart": true,
|
|
127
|
+
"square": false,
|
|
128
|
+
"padding": 1,
|
|
129
|
+
"extrude": 1,
|
|
130
|
+
"border": 1,
|
|
131
|
+
"allowRotation": false,
|
|
132
|
+
"trim": true,
|
|
133
|
+
"trimThreshold": 0,
|
|
134
|
+
"prefix": "packed",
|
|
135
|
+
"scale": 1
|
|
136
|
+
},
|
|
137
|
+
"files": {
|
|
138
|
+
"hero.png": { "trim": false }
|
|
139
|
+
}
|
|
140
|
+
}]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Use `exports: ["texturepacker", "createjs"]` (or `format`) to choose one or more atlas descriptors. Supported formats:
|
|
145
|
+
|
|
146
|
+
* `texturepacker` / `json-hash` / `pixi` / `phaser-hash` — TexturePacker JSON Hash
|
|
147
|
+
* `json-array` / `phaser-array` — TexturePacker JSON Array
|
|
148
|
+
* `createjs` / `easeljs` — CreateJS SpriteSheet JSON
|
|
149
|
+
* `spine` — Spine `.atlas` text
|
|
150
|
+
* `css` — CSS sprite classes
|
|
151
|
+
* `starling` / `sparrow` — Starling/Sparrow XML
|
|
152
|
+
* `unity` — Unity-oriented JSON
|
|
153
|
+
* `godot` — Godot atlas JSON
|
|
154
|
+
|
|
155
|
+
Spacing: `padding` is empty gap beyond `extrude`; `extrude` is edge-pixel bleed into that gap; `border` is margin from the atlas edge.
|
|
156
|
+
|
|
112
157
|
### Polly
|
|
113
158
|
|
|
114
159
|
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:
|
|
@@ -215,7 +260,7 @@ Cheerfully resolves these against caption timings and exports them in parallel u
|
|
|
215
260
|
|
|
216
261
|
## Example
|
|
217
262
|
|
|
218
|
-
Find an example script in the `./examples/` folder. You can run either `npm run test-elevenlabs` or `npm run test-polly` to create VO tracks in the examples folder that you can then use to try out `npm run test-rhubarb` and `npm run test-transcription`.
|
|
263
|
+
Find an example script in the `./examples/` folder. You can run either `npm run test-elevenlabs` or `npm run test-polly` to create VO tracks in the examples folder that you can then use to try out `npm run test-rhubarb` and `npm run test-transcription`. Sample sprites for atlas packing are under `./example/sprites/`; run `npm run test-packfully` to pack them into `./example/atlases/`.
|
|
219
264
|
|
|
220
265
|
These examples are specified using a JSON definition in `./cheerfully.json` which you can copy or modify to set different generation parameters as supported by Cheerfully.
|
|
221
266
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makefully/dutifully",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"description": "Client tools for Cheerfully service.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"test-allosaurus": "node ./cheer.js -service allosaurus",
|
|
20
20
|
"test-elevenlabs": "node ./cheer.js -service elevenlabs",
|
|
21
21
|
"test-ffmpeg": "node ./cheer.js -service ffmpeg",
|
|
22
|
+
"test-packfully": "node ./cheer.js -service packfully",
|
|
22
23
|
"test-polly": "node ./cheer.js -service polly",
|
|
23
24
|
"test-rasterize": "node ./cheer.js -service rasterize",
|
|
24
25
|
"test-rhubarb": "node ./cheer.js -service rhubarb",
|
|
@@ -41,7 +42,10 @@
|
|
|
41
42
|
"polly",
|
|
42
43
|
"lipsync",
|
|
43
44
|
"transcription",
|
|
44
|
-
"captions"
|
|
45
|
+
"captions",
|
|
46
|
+
"packfully",
|
|
47
|
+
"atlas",
|
|
48
|
+
"spritesheet"
|
|
45
49
|
],
|
|
46
50
|
"author": "Derek Detweiler <derek@makefullystudios.com>",
|
|
47
51
|
"contributors": [
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const
|
|
2
|
+
Cheer = require('./Cheer'),
|
|
3
|
+
Packfully = class Packfully 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 = Packfully;
|
|
@@ -82,8 +82,12 @@ const
|
|
|
82
82
|
await ensureDir(output);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
const
|
|
86
|
+
ids = Object.keys(captions);
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < ids.length; i++) {
|
|
86
89
|
const
|
|
90
|
+
id = ids[i],
|
|
87
91
|
text = captionText(captions[id]),
|
|
88
92
|
tags = {
|
|
89
93
|
album,
|
|
@@ -95,13 +99,13 @@ const
|
|
|
95
99
|
};
|
|
96
100
|
|
|
97
101
|
await id3.update(tags, `${output}${id}.mp3`);
|
|
98
|
-
|
|
102
|
+
|
|
99
103
|
if (generated) {
|
|
100
104
|
console.log(`Voice generated for "${id}".`);
|
|
101
105
|
} else {
|
|
102
106
|
console.log(`Voice meta data updated for "${id}".`);
|
|
103
107
|
}
|
|
104
|
-
}
|
|
108
|
+
}
|
|
105
109
|
},
|
|
106
110
|
mapReduction = (map, list) => list.reduce((obj, file) => {
|
|
107
111
|
const
|
|
@@ -167,7 +171,7 @@ const
|
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
if (updateAllMetaData && present.length) {
|
|
170
|
-
appendMP3Meta({
|
|
174
|
+
await appendMP3Meta({
|
|
171
175
|
album,
|
|
172
176
|
captions: mapReduction(files, present),
|
|
173
177
|
language,
|
|
@@ -186,9 +190,39 @@ const
|
|
|
186
190
|
const
|
|
187
191
|
{album, encodedBy, config, transcripts, updateList} = this,
|
|
188
192
|
{files, output, language, voice} = config,
|
|
189
|
-
list = updateList ?? Object.keys(files).map((id) => `${id}.mp3`)
|
|
193
|
+
list = updateList ?? Object.keys(files).map((id) => `${id}.mp3`),
|
|
194
|
+
missing = [];
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < list.length; i++) {
|
|
197
|
+
const
|
|
198
|
+
filename = list[i],
|
|
199
|
+
fullPath = `${output}${filename}`;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await fs.access(fullPath);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
missing.push(filename);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (missing.length) {
|
|
209
|
+
let detail = '';
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const
|
|
213
|
+
log = JSON.parse(await fs.readFile(`${output}log.json`, 'utf8'));
|
|
214
|
+
|
|
215
|
+
if (log?.errors?.length) {
|
|
216
|
+
detail = ` Server errors: ${log.errors.join('; ')}`;
|
|
217
|
+
}
|
|
218
|
+
} catch (e) { /* no log.json */ }
|
|
219
|
+
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Voice export missing ${missing.length === 1 ? `"${missing[0]}"` : `${missing.length} files (${missing.join(', ')})`}.${detail}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
190
224
|
|
|
191
|
-
appendMP3Meta({
|
|
225
|
+
await appendMP3Meta({
|
|
192
226
|
album,
|
|
193
227
|
captions: files,
|
|
194
228
|
generated: true,
|
package/src/index.js
CHANGED
package/src/say.js
CHANGED
|
@@ -59,17 +59,20 @@ const
|
|
|
59
59
|
},
|
|
60
60
|
path: '',
|
|
61
61
|
voice,
|
|
62
|
-
output
|
|
62
|
+
output,
|
|
63
|
+
extract: true
|
|
63
64
|
},
|
|
64
65
|
handler = new ElevenLabs({
|
|
65
66
|
config,
|
|
66
67
|
contents: {
|
|
67
68
|
...contents,
|
|
68
|
-
service: 'elevenlabs'
|
|
69
|
+
service: 'elevenlabs',
|
|
70
|
+
extract: true
|
|
69
71
|
}
|
|
70
72
|
});
|
|
71
73
|
|
|
72
74
|
delete config.script;
|
|
75
|
+
delete config.src;
|
|
73
76
|
|
|
74
77
|
await handler.prepare({
|
|
75
78
|
difference: false,
|
|
@@ -79,7 +82,30 @@ const
|
|
|
79
82
|
instanceId: `${contents.id}-say`
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
const
|
|
86
|
+
mp3File = path.join(CACHE_DIR, `${key}.mp3`);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await fsp.access(mp3File);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
let detail = '';
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const
|
|
95
|
+
log = JSON.parse(await fsp.readFile(path.join(CACHE_DIR, 'log.json'), 'utf8'));
|
|
96
|
+
|
|
97
|
+
if (log?.errors?.length) {
|
|
98
|
+
detail = ` ${log.errors.join('; ')}`;
|
|
99
|
+
}
|
|
100
|
+
} catch (err) { /* ignore */ }
|
|
101
|
+
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Speech generation finished but "${mp3File}" was not created.${detail} ` +
|
|
104
|
+
'Check that the Cheerfully worker is running and picking up elevenlabs chores.'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return mp3File;
|
|
83
109
|
};
|
|
84
110
|
|
|
85
111
|
module.exports = async (textOrArgs = {}) => {
|