easycaptionkit 0.1.1 → 0.1.3
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/README.md +14 -5
- package/package.json +1 -1
- package/skills/easycaptionkit/SKILL.md +8 -3
- package/skills/easycaptionkit/scripts/easycaptionkit.js +12 -5
- package/skills/easycaptionkit/scripts/lib/boxes.js +139 -0
- package/skills/easycaptionkit/scripts/lib/caption.js +71 -40
- package/skills/easycaptionkit/scripts/lib/chrome.js +94 -42
- package/skills/easycaptionkit/scripts/lib/ffmpeg.js +97 -91
- package/skills/easycaptionkit/scripts/lib/look.js +1 -1
- package/skills/easycaptionkit/scripts/lib/programs.js +478 -0
- package/skills/easycaptionkit/scripts/lib/render.js +26 -7
- package/skills/easycaptionkit/scripts/lib/report.js +85 -21
- package/skills/easycaptionkit/scripts/lib/subtitles.js +47 -18
- package/skills/easycaptionkit/scripts/lib/version.js +1 -1
- package/skills/easycaptionkit/scripts/lib/words.js +8 -1
|
@@ -19,6 +19,41 @@ const clock = (t) => {
|
|
|
19
19
|
};
|
|
20
20
|
const list = (items) => (items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`);
|
|
21
21
|
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
22
|
+
|
|
23
|
+
// The captions letters are in, of [{ letter, cue }], as a note says them:
|
|
24
|
+
// "caption 3", "captions 1, 2 and 5", or the first six and how many more.
|
|
25
|
+
function captionsOf(items) {
|
|
26
|
+
const cues = [...new Set(items.map((m) => m.cue))];
|
|
27
|
+
return `${cues.length === 1 ? "caption" : "captions"} ${cues.length > 6 ? `${cues.slice(0, 6).join(", ")} and ${cues.length - 6} more` : list(cues.map(String))}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The letters themselves, each once: all of them, or the first eight and
|
|
31
|
+
// how many more.
|
|
32
|
+
function lettersOf(items) {
|
|
33
|
+
const letters = [...new Set(items.map((m) => m.letter))];
|
|
34
|
+
return letters.length > 8 ? `${letters.slice(0, 8).join(" ")} and ${letters.length - 8} more` : letters.join(" ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The letters the video shows as boxes, [{ letter, cue }], as a sentence:
|
|
38
|
+
// Chrome found no font for them, or no font libass can use has them.
|
|
39
|
+
function boxesText(boxes, drawnWith) {
|
|
40
|
+
const n = boxes.length;
|
|
41
|
+
if (drawnWith === "libass") return `No font on this machine that libass can use has ${plural(n, "letter", "letters")} of ${captionsOf(boxes)} (${lettersOf(boxes)}), so the video shows ${n === 1 ? "a box for it" : "boxes for them"}.`;
|
|
42
|
+
return `Chrome found no font on this machine for ${plural(n, "letter", "letters")} of ${captionsOf(boxes)} (${lettersOf(boxes)}), so it drew a box for ${n === 1 ? "it" : "each of them"}.`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// What to do about letters no font here has: install one that has them,
|
|
46
|
+
// named where one font covers them all, as Noto Sans CJK covers Chinese,
|
|
47
|
+
// Japanese and Korean, or give a font kit whose font has them.
|
|
48
|
+
const CJK = /[\p{Script_Extensions=Han}\p{Script_Extensions=Hiragana}\p{Script_Extensions=Katakana}\p{Script_Extensions=Hangul}\p{Script_Extensions=Bopomofo} -〿-]/u;
|
|
49
|
+
const PICTURE = /[\p{Extended_Pictographic}\p{Regional_Indicator}\u20e3]/u;
|
|
50
|
+
function installText(items) {
|
|
51
|
+
const letters = items.map((m) => m.letter);
|
|
52
|
+
const them = letters.length === 1 ? "it" : "them";
|
|
53
|
+
if (letters.every((c) => PICTURE.test(c))) return "Install a color emoji font, such as Noto Color Emoji (fonts-noto-color-emoji on Debian and Ubuntu), and run again.";
|
|
54
|
+
const font = letters.every((c) => CJK.test(c)) ? "Noto Sans CJK (fonts-noto-cjk on Debian and Ubuntu)" : `a Noto font for ${letters.length === 1 ? "its" : "their"} script`;
|
|
55
|
+
return `Install a font that has ${them}, such as ${font}, or give --fonts an easyfontkit folder whose font has ${them}, and run again.`;
|
|
56
|
+
}
|
|
22
57
|
// A name on one line of Markdown or of the terminal: its control
|
|
23
58
|
// characters become a space, as a line break in it would end a heading or
|
|
24
59
|
// a list item, or start a line of its own in a message, and an escape
|
|
@@ -197,17 +232,24 @@ function fallbackLine(f) {
|
|
|
197
232
|
const n = fb.reduce((a, b) => a + b.letters, 0);
|
|
198
233
|
const where = `in ${list(fb.map((x) => x.family))}, which fontconfig gives for ${n === 1 ? "it" : "them"}, at the size that makes ${n === 1 ? "it a" : "them"} ${f.layout.fontPx} px ${n === 1 ? "letter" : "letters"} too.`;
|
|
199
234
|
if (setsNone(f)) return `The font has none of the ${plural(n, "letter", "letters")}: captions.ass sets ${n === 1 ? "it" : "them"} ${where}`;
|
|
200
|
-
|
|
235
|
+
// Where the video shows boxes too, the font lacks more letters than these.
|
|
236
|
+
return `captions.ass sets ${boxedIn(f) ? "" : "the "}${plural(n, "letter", "letters")} the font lacks ${where}`;
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
// Whether the font captions.ass names sets any letter of the captions,
|
|
204
|
-
// and whether it sets none and other fonts set them
|
|
240
|
+
// and whether it sets none and other fonts set them all, with no letter
|
|
241
|
+
// libass showed as a box.
|
|
205
242
|
const setsAny = (f) => !f.assLetters || f.assLetters.own > 0;
|
|
206
|
-
const setsNone = (f) => !setsAny(f) && (f.assFallbacks || []).length > 0;
|
|
243
|
+
const setsNone = (f) => !setsAny(f) && (f.assFallbacks || []).length > 0 && !boxedIn(f);
|
|
244
|
+
|
|
245
|
+
// Whether the video shows letters as boxes.
|
|
246
|
+
const boxedIn = (f) => (f.boxes || []).length > 0;
|
|
207
247
|
|
|
208
248
|
// The font the captions were drawn with: none named, the first of the
|
|
209
249
|
// common sans fonts this machine has, which Chrome found for Latin
|
|
210
|
-
// letters, and the fonts that drew the letters it lacks.
|
|
250
|
+
// letters, and the fonts that drew the letters it lacks. The letters the
|
|
251
|
+
// video shows as boxes, which no font drew, are said after the font, and
|
|
252
|
+
// the letters counted after them are the others.
|
|
211
253
|
function fontLines(f) {
|
|
212
254
|
const lines = [];
|
|
213
255
|
const look = f.look;
|
|
@@ -217,25 +259,36 @@ function fontLines(f) {
|
|
|
217
259
|
const m = libass ? f.libassFont || null : null;
|
|
218
260
|
const fill = (fonts) => list(fonts.map((x) => (fonts.length > 1 ? `${x.family} (${x.glyphs})` : x.family)));
|
|
219
261
|
const filled = libass && fallbackLine(f) ? [` ${fallbackLine(f)}`] : [];
|
|
262
|
+
const boxed = boxedIn(f) ? [` ${boxesText(f.boxes, f.drawnWith)}`] : [];
|
|
263
|
+
const other = boxed.length ? "other " : "";
|
|
264
|
+
const ofAll = boxed.length ? `of the other ${total}` : `of ${total}`;
|
|
265
|
+
// The letters the fonts drew as a sentence names them, where one is left
|
|
266
|
+
// beside the boxes, or is all there is: "the other letter", which "it"
|
|
267
|
+
// stands for, and never "the other 1 letters".
|
|
268
|
+
const one = total === 1;
|
|
269
|
+
const it = one ? "it" : "them";
|
|
270
|
+
const letters = `the ${other}${one ? "letter" : "letters"}`;
|
|
271
|
+
const counted = one ? letters : `the ${other}${total} letters`;
|
|
272
|
+
const lacksThem = (fonts) => `It ${one ? "does not have" : "has none of"} ${counted}, so Chrome drew ${it} with ${fill(fonts)}.`;
|
|
220
273
|
if (!look.family) {
|
|
221
274
|
if (libass) {
|
|
222
|
-
lines.push(`- Font: Arial ${look.weight}, as captions.ass asks for it by name. ${m && sameName(m.family, "Arial") ? "This machine has it." : givenFor(m)}`, ...filled);
|
|
275
|
+
lines.push(`- Font: Arial ${look.weight}, as captions.ass asks for it by name. ${m && sameName(m.family, "Arial") ? "This machine has it." : givenFor(m)}`, ...boxed, ...filled);
|
|
223
276
|
return lines;
|
|
224
277
|
}
|
|
225
278
|
// Chrome did not say which font is the first of the list: the fonts
|
|
226
279
|
// that drew, and none named as that one.
|
|
227
280
|
const first = f.fontFirst || "";
|
|
228
281
|
if (!first) {
|
|
229
|
-
lines.push(`- Font: a common sans ${look.weight}, the first of the common sans fonts easycaptionkit asks for that this machine has
|
|
230
|
-
if (drawn.length) lines.push(` Chrome drew
|
|
282
|
+
lines.push(`- Font: a common sans ${look.weight}, the first of the common sans fonts easycaptionkit asks for that this machine has.`, ...boxed);
|
|
283
|
+
if (drawn.length) lines.push(` Chrome drew ${counted} with ${fill(drawn)}.`);
|
|
231
284
|
return lines;
|
|
232
285
|
}
|
|
233
|
-
lines.push(`- Font: ${first} ${look.weight}, the first of the common sans fonts easycaptionkit asks for that this machine has
|
|
286
|
+
lines.push(`- Font: ${first} ${look.weight}, the first of the common sans fonts easycaptionkit asks for that this machine has.`, ...boxed);
|
|
234
287
|
const own = drawn.find((x) => x.family === first);
|
|
235
288
|
const others = drawn.filter((x) => x.family !== first);
|
|
236
289
|
if (others.length) {
|
|
237
290
|
const rest = total - (own ? own.glyphs : 0);
|
|
238
|
-
lines.push(own ? ` Chrome drew ${own.glyphs}
|
|
291
|
+
lines.push(own ? ` Chrome drew ${own.glyphs} ${ofAll} letters with it, and the ${rest} it lacks with ${fill(others)}.` : ` ${lacksThem(others)}`);
|
|
239
292
|
}
|
|
240
293
|
lines.push(...lighterLines(look, others));
|
|
241
294
|
return lines;
|
|
@@ -246,12 +299,20 @@ function fontLines(f) {
|
|
|
246
299
|
if (m && sameName(m.family, look.family)) lines.push(`- Font: ${look.family} ${look.weight}, as installed on this machine.`);
|
|
247
300
|
else if (m) lines.push(`- Font: ${look.family} ${look.weight} was asked for by name, and this machine does not have it: ${givenFor(m)} Give easyfontkit's folder with --fonts to burn the real font.`);
|
|
248
301
|
else lines.push(`- Font: ${look.family} ${look.weight}, asked for by name. ${givenFor(m)}`);
|
|
249
|
-
lines.push(...filled);
|
|
302
|
+
lines.push(...boxed, ...filled);
|
|
303
|
+
return lines;
|
|
304
|
+
}
|
|
305
|
+
// Not on this machine: Chrome drew the letters with other fonts, or
|
|
306
|
+
// drew every letter as a box, and its first font is another.
|
|
307
|
+
const absent = !machineHas(f) && (drawn.length ? !drawn.some((x) => drewWith(x, look)) : boxed.length > 0);
|
|
308
|
+
if (absent) {
|
|
309
|
+
if (boxed.length) lines.push(`- Font: ${look.family} ${look.weight} was asked for by name, and this machine does not have it. Give easyfontkit's folder with --fonts to burn the real font.`, ...boxed, ...(drawn.length ? [` Chrome drew ${letters} with ${fill(drawn)}.`] : []));
|
|
310
|
+
else lines.push(`- Font: ${look.family} ${look.weight} was asked for by name, and this machine does not have it, so Chrome drew ${letters} with ${fill(drawn)}. Give easyfontkit's folder with --fonts to burn the real font.`);
|
|
250
311
|
return lines;
|
|
251
312
|
}
|
|
252
313
|
if (drawn.length && !drawn.some((x) => drewWith(x, look))) {
|
|
253
|
-
if (
|
|
254
|
-
else lines.push(`- Font: ${look.family} ${look.weight}
|
|
314
|
+
if (boxed.length) lines.push(`- Font: ${look.family} ${look.weight}, as installed on this machine.`, ...boxed, ` ${lacksThem(drawn)}`, ...lighterLines(look, drawn));
|
|
315
|
+
else lines.push(`- Font: ${look.family} ${look.weight}, as installed on this machine. ${lacksThem(drawn)}`, ...lighterLines(look, drawn));
|
|
255
316
|
return lines;
|
|
256
317
|
}
|
|
257
318
|
lines.push(`- Font: ${look.family} ${look.weight}, as installed on this machine.`);
|
|
@@ -260,18 +321,19 @@ function fontLines(f) {
|
|
|
260
321
|
lines.push(`- Font: ${look.family}, from ${files}. That file holds the weights ${face.weight[0]} to ${face.weight[1]}, and libass draws such a file at its default weight, not at ${look.weight}, so the letters are thinner than Chrome draws them.`);
|
|
261
322
|
} else lines.push(`- Font: ${look.family} ${look.weight}, from ${files}.`);
|
|
262
323
|
if (f.assFamily && !sameName(f.assFamily, look.family)) lines.push(` captions.ass names it ${f.assFamily}, the family name in its file, which libass and the players that read ASS find a font by.`);
|
|
263
|
-
if (libass) lines.push(" libass drew the letters and does not say which font drew each.", ...filled);
|
|
324
|
+
if (libass) lines.push(" libass drew the letters and does not say which font drew each.", ...boxed, ...filled);
|
|
325
|
+
else lines.push(...boxed);
|
|
264
326
|
if (!drawn.length) return lines;
|
|
265
327
|
// A font file of fonts.css is the only web font on the page, and Chrome
|
|
266
328
|
// names it by the family in the file, which fonts.css can call
|
|
267
329
|
// otherwise.
|
|
268
330
|
const own = drawn.filter((x) => drewWith(x, look));
|
|
269
331
|
const others = drawn.filter((x) => !drewWith(x, look));
|
|
270
|
-
if (!own.length) lines.push(` Chrome drew no letter with the file of ${look.family}: ${drawn[0].family} drew
|
|
271
|
-
else if (!others.length) lines.push(" Chrome drew every letter with it.");
|
|
332
|
+
if (!own.length) lines.push(` Chrome ${!boxed.length ? "drew no letter" : one ? "did not draw the other letter" : "drew none of the other letters"} with the file of ${look.family}: ${drawn[0].family} drew ${it}.`);
|
|
333
|
+
else if (!others.length) lines.push(boxed.length ? ` Chrome drew ${one ? "the other letter" : "all the other letters"} with it.` : " Chrome drew every letter with it.");
|
|
272
334
|
else {
|
|
273
335
|
const mine = own.reduce((a, b) => a + b.glyphs, 0);
|
|
274
|
-
lines.push(` Chrome drew ${mine}
|
|
336
|
+
lines.push(` Chrome drew ${mine} ${ofAll} letters with it, and the ${total - mine} it lacks with ${fill(others)}.`);
|
|
275
337
|
}
|
|
276
338
|
lines.push(...lighterLines(look, others));
|
|
277
339
|
return lines;
|
|
@@ -298,10 +360,11 @@ function lighterLines(look, drawn) {
|
|
|
298
360
|
// The fonts that drew every letter of the captions where the font asked
|
|
299
361
|
// for, by name or from fonts.css, drew none of them, as a Latin font draws
|
|
300
362
|
// no Japanese: the fonts Chrome drew them with, or with libass the fonts
|
|
301
|
-
// captions.ass sets them in.
|
|
302
|
-
//
|
|
363
|
+
// captions.ass sets them in. Letters that show as boxes, which no font
|
|
364
|
+
// drew, do not count. Their names, or [] when the font drew any letter, no
|
|
365
|
+
// font was asked for, or nothing says.
|
|
303
366
|
function drewNone(f) {
|
|
304
|
-
if (f.drawnWith === "libass") return f.look.family &&
|
|
367
|
+
if (f.drawnWith === "libass") return f.look.family && !setsAny(f) && (f.assFallbacks || []).length ? f.assFallbacks.map((x) => x.family) : [];
|
|
305
368
|
const drawn = (f.fontsDrawn || []).filter((x) => x.glyphs > 0);
|
|
306
369
|
if (!f.look.family || !drawn.length || drawn.some((x) => drewWith(x, f.look))) return [];
|
|
307
370
|
return drawn.map((x) => x.family);
|
|
@@ -341,7 +404,8 @@ function licenseSection(f) {
|
|
|
341
404
|
// ones that drew them are, whatever its license.
|
|
342
405
|
const instead = drewNone(f);
|
|
343
406
|
if (instead.length) {
|
|
344
|
-
const
|
|
407
|
+
const which = boxedIn(f) ? "all but the boxes" : "them all";
|
|
408
|
+
const drew = libass ? `captions.ass sets ${which} in` : `Chrome drew ${which} with`;
|
|
345
409
|
// A font asked for by name that this machine has, or one from fonts.css, lacks the letters.
|
|
346
410
|
const lacks = look.from === "fonts.css" || (libass ? f.libassFont && sameName(f.libassFont.family, look.family) : machineHas(f));
|
|
347
411
|
const lead = read ? `${read}, but no letter of it is in the video` : `No letter of ${look.family} is in the video`;
|
|
@@ -601,4 +665,4 @@ function captionsJson(f) {
|
|
|
601
665
|
};
|
|
602
666
|
}
|
|
603
667
|
|
|
604
|
-
module.exports = { captionsMarkdown, subtitlesMarkdown, captionsJson, marginsText, drewWith, setsAny, setsNone, machineHas, arg, quote, code, clock, subtitlesCommand, programOf, shown, soundText };
|
|
668
|
+
module.exports = { captionsMarkdown, subtitlesMarkdown, captionsJson, marginsText, drewWith, setsAny, setsNone, boxedIn, machineHas, captionsOf, lettersOf, boxesText, installText, arg, quote, code, clock, subtitlesCommand, programOf, shown, soundText };
|
|
@@ -121,12 +121,10 @@ const styleName = (name) => Boolean(name) && !/[\u0000-\u001f\u007f,]/.test(name
|
|
|
121
121
|
// with. libass sizes each font by its own heights, so captions.ass sets
|
|
122
122
|
// those letters in that font at a size of its own: the font then has
|
|
123
123
|
// `fallback(c)`, the font a letter is set in, { family, scale, advance },
|
|
124
|
-
// or null.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
const nearest = faces.length ? Math.min(...faces.map((f) => distance(f, look.weight))) : 0;
|
|
129
|
-
const files = faces.filter((f) => distance(f, look.weight) === nearest);
|
|
124
|
+
// or null. `lacks(codePoint)` says no font on this machine has a letter,
|
|
125
|
+
// which then needs no asking (boxes.js).
|
|
126
|
+
function assFont(look, machine = null, { fallback = null, budget = BUDGET, lacks = null } = {}) {
|
|
127
|
+
const files = facesAt(look);
|
|
130
128
|
const kit = files.length ? familyOf(files, budget) : null;
|
|
131
129
|
const first = kit ? kit.first : machine ? readFont(machine.file, machine.index) : null;
|
|
132
130
|
const scale = first ? assScale(first) : 0;
|
|
@@ -135,10 +133,31 @@ function assFont(look, machine = null, { fallback = null, budget = BUDGET } = {}
|
|
|
135
133
|
const has = kit ? kit.has : (c) => Number.isFinite(first.advance(c));
|
|
136
134
|
const own = kit ? kit.family : "";
|
|
137
135
|
const font = { scale, advance, has, from: files.length ? files[0].name || files[0].file : machine.file, family: styleName(own) ? own : "" };
|
|
138
|
-
if (fallback) font.fallback = fallbacksOf(fallback);
|
|
136
|
+
if (fallback) font.fallback = fallbacksOf(fallback, lacks);
|
|
139
137
|
return font;
|
|
140
138
|
}
|
|
141
139
|
|
|
140
|
+
// The faces of a fonts.css family the captions are drawn with, by Chrome
|
|
141
|
+
// and by libass: the upright ones nearest the weight.
|
|
142
|
+
function facesAt(look) {
|
|
143
|
+
const upright = (look.faces || []).filter((f) => !f.italic);
|
|
144
|
+
const faces = upright.length ? upright : look.faces || [];
|
|
145
|
+
const nearest = faces.length ? Math.min(...faces.map((f) => distance(f, look.weight))) : 0;
|
|
146
|
+
return faces.filter((f) => distance(f, look.weight) === nearest);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Whether the files of a fonts.css family the captions are drawn with
|
|
150
|
+
// have a letter, one code point: has(c), or null for a look with no
|
|
151
|
+
// files. A letter is taken to be there when a file that could hold it
|
|
152
|
+
// was not read, as one past what is read of a family: only a letter
|
|
153
|
+
// they are known to lack is not.
|
|
154
|
+
function kitLetters(look, budget = BUDGET) {
|
|
155
|
+
const files = facesAt(look);
|
|
156
|
+
if (!files.length) return null;
|
|
157
|
+
const kit = familyOf(files, budget);
|
|
158
|
+
return (c) => kit.has(c) || kit.unread(c);
|
|
159
|
+
}
|
|
160
|
+
|
|
142
161
|
// What is read of a fonts.css family's files in all, at most, counting
|
|
143
162
|
// what WOFF and WOFF2 files unpack to: a site splits a CJK family into a
|
|
144
163
|
// hundred files and more, each small, and a file of a few hundred bytes
|
|
@@ -159,10 +178,11 @@ function rangesOf(text) {
|
|
|
159
178
|
// The files of a fonts.css family at one weight, each read once a letter
|
|
160
179
|
// in its unicode-range is asked for, as Chrome loads them, and no more than
|
|
161
180
|
// `budget` bytes of them in all, files and what they unpack to. { first,
|
|
162
|
-
// family, advance, has }: the first file that reads, which gives
|
|
163
|
-
// and the family name in it or in the next file that has one.
|
|
164
|
-
// the range of a file that was not read, or is no font this
|
|
165
|
-
// the family's still: libass finds that file by its name.
|
|
181
|
+
// family, advance, has, unread }: the first file that reads, which gives
|
|
182
|
+
// the size, and the family name in it or in the next file that has one.
|
|
183
|
+
// A letter in the range of a file that was not read, or is no font this
|
|
184
|
+
// kit reads, is the family's still: libass finds that file by its name.
|
|
185
|
+
// unread(c) says a file that could hold the letter was not read.
|
|
166
186
|
function familyOf(faces, budget) {
|
|
167
187
|
const list = faces.map((face) => ({ face, ranges: rangesOf(face.unicodeRange), font: undefined }));
|
|
168
188
|
let left = budget;
|
|
@@ -179,16 +199,17 @@ function familyOf(faces, budget) {
|
|
|
179
199
|
const lookUp = (c) => {
|
|
180
200
|
const code = c.codePointAt(0);
|
|
181
201
|
if (known.has(code)) return known.get(code);
|
|
182
|
-
let found = { advance: NaN, has: false };
|
|
202
|
+
let found = { advance: NaN, has: false, unread: false };
|
|
183
203
|
for (const item of list) {
|
|
184
204
|
if (!inRange(item, code)) continue;
|
|
185
205
|
const font = fontOf(item);
|
|
186
206
|
const w = font ? font.advance(c) : NaN;
|
|
187
207
|
if (Number.isFinite(w)) {
|
|
188
|
-
found = { advance: w, has: true };
|
|
208
|
+
found = { advance: w, has: true, unread: false };
|
|
189
209
|
break;
|
|
190
210
|
}
|
|
191
211
|
if (!font && item.ranges) found.has = true;
|
|
212
|
+
if (!font) found.unread = true;
|
|
192
213
|
}
|
|
193
214
|
known.set(code, found);
|
|
194
215
|
return found;
|
|
@@ -201,7 +222,7 @@ function familyOf(faces, budget) {
|
|
|
201
222
|
if (font && font.family) family = font.family;
|
|
202
223
|
if (first && family) break;
|
|
203
224
|
}
|
|
204
|
-
return { first, family, advance: (c) => lookUp(c).advance, has: (c) => lookUp(c).has };
|
|
225
|
+
return { first, family, advance: (c) => lookUp(c).advance, has: (c) => lookUp(c).has, unread: (c) => lookUp(c).unread };
|
|
205
226
|
}
|
|
206
227
|
|
|
207
228
|
// A name an override tag can carry: it ends at a backslash or a brace.
|
|
@@ -222,13 +243,17 @@ const NONE = Object.freeze({ none: true });
|
|
|
222
243
|
// that has them all. libass draws no font of pictures alone, as a color
|
|
223
244
|
// emoji font is, and fontconfig gives a font that lacks a letter only
|
|
224
245
|
// when no font has it: `none(c)` says fontconfig gives no font libass can
|
|
225
|
-
// draw the letter with, so libass draws a box for it.
|
|
226
|
-
|
|
246
|
+
// draw the letter with, so libass draws a box for it. A letter `lacks`
|
|
247
|
+
// says no font on this machine has is one too, without asking, and asks
|
|
248
|
+
// none of the 64: so every such letter is counted, however many there
|
|
249
|
+
// are, as in Japanese on a machine with no Japanese font.
|
|
250
|
+
function fallbacksOf(give, lacks = null) {
|
|
227
251
|
const found = [];
|
|
228
252
|
const asked = new Map();
|
|
229
253
|
const fontFor = (first, arabic) => {
|
|
230
254
|
const code = first.codePointAt(0);
|
|
231
255
|
if (asked.has(code)) return asked.get(code);
|
|
256
|
+
if (lacks && lacks(code)) return NONE;
|
|
232
257
|
if (asked.size >= 64) return null;
|
|
233
258
|
const given = give(code);
|
|
234
259
|
let got = null;
|
|
@@ -450,6 +475,10 @@ function ass(cues, words, { states, style, look, place, title = "", level = 1, s
|
|
|
450
475
|
// default, one asked for so, or a flag. A © or a ™ is a letter.
|
|
451
476
|
const SHOWN_AS_EMOJI = /\p{Emoji_Presentation}|\ufe0f|\p{Regional_Indicator}/u;
|
|
452
477
|
|
|
478
|
+
// A no-break hyphen no font here has is no box where the font has the
|
|
479
|
+
// plain hyphen: HarfBuzz, which libass shapes with, draws that for it.
|
|
480
|
+
const hyphenFor = (c, font) => c === "\u2011" && font.has("\u2010");
|
|
481
|
+
|
|
453
482
|
// How captions.ass sets the letters of the captions, the spaces left out:
|
|
454
483
|
// { total, own, fallbacks, missing, unsure }. own is how many are in the
|
|
455
484
|
// font it names, or where nothing says otherwise; fallbacks the fonts it
|
|
@@ -471,7 +500,7 @@ function assLetters(cues, font) {
|
|
|
471
500
|
const lacks = !font.has(Array.from(c)[0]);
|
|
472
501
|
const other = setIn(c, font, null);
|
|
473
502
|
if (other) counts.set(other.family, (counts.get(other.family) || 0) + 1);
|
|
474
|
-
else if (font.fallback && lacks && font.fallback.none(c)) missing.push({ letter: c, cue: cue.n });
|
|
503
|
+
else if (font.fallback && lacks && font.fallback.none(c) && !hyphenFor(c, font)) missing.push({ letter: c, cue: cue.n });
|
|
475
504
|
else {
|
|
476
505
|
own++;
|
|
477
506
|
if (lacks && SHOWN_AS_EMOJI.test(c)) unsure.push({ letter: c, cue: cue.n });
|
|
@@ -486,4 +515,4 @@ function assLetters(cues, font) {
|
|
|
486
515
|
// names, by that font: [{ family, letters }].
|
|
487
516
|
const fallbackLetters = (cues, font) => assLetters(cues, font).fallbacks;
|
|
488
517
|
|
|
489
|
-
module.exports = { srt, vtt, ass, assFont, estimatedScales, shrunkOf, assLetters, fallbackLetters, clock, assClock, assColor, assTag, dimmed, escapeSrt, SRT_SPECIAL, ARIAL };
|
|
518
|
+
module.exports = { srt, vtt, ass, assFont, kitLetters, estimatedScales, shrunkOf, assLetters, fallbackLetters, clock, assClock, assColor, assTag, dimmed, escapeSrt, SRT_SPECIAL, ARIAL };
|
|
@@ -645,7 +645,12 @@ function detectJson(data) {
|
|
|
645
645
|
// `from`: a demo's steps stay steps when their words.json is read
|
|
646
646
|
// again, in its own folder or in another.
|
|
647
647
|
const first = Object.keys(FORMATS).find((key) => key !== "easycaptionkit" && FORMATS[key] === data.from);
|
|
648
|
-
|
|
648
|
+
// The length steps.json gave the demo's video, which words.json
|
|
649
|
+
// keeps, so a subtitles run with no video ends the captions there
|
|
650
|
+
// again, as the run from steps.json did.
|
|
651
|
+
const length = entry(data.demo) ? numberOf(data.demo.duration) : NaN;
|
|
652
|
+
const demo = Number.isFinite(length) && length > 0 ? { duration: length, file: "" } : null;
|
|
653
|
+
return shape({ format: "easycaptionkit", words, hard: Boolean(data.hard), fixed: handFixed(data), ...(first ? { first } : {}), ...(demo ? { video: demo } : {}) }, data.words);
|
|
649
654
|
}
|
|
650
655
|
if (data.kind === "demo" && Array.isArray(data.steps)) return shape({ format: "steps", ...readSteps(data, listed(data.steps, read)) }, data.steps);
|
|
651
656
|
if (Array.isArray(data.transcription)) return shape({ format: "whispercpp", words: readWhisperCpp(listed(data.transcription, read), read) }, data.transcription);
|
|
@@ -1083,6 +1088,8 @@ function readWords(file, given = null) {
|
|
|
1083
1088
|
// words.json a run writes from them keeps saying.
|
|
1084
1089
|
fixed: Boolean(found.fixed),
|
|
1085
1090
|
notes,
|
|
1091
|
+
// The demo's video as steps.json gives it, { duration, file }, also
|
|
1092
|
+
// from a words.json made of steps, which keeps its length.
|
|
1086
1093
|
video: found.video || null,
|
|
1087
1094
|
};
|
|
1088
1095
|
}
|