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.
@@ -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
- return `captions.ass sets the ${plural(n, "letter", "letters")} the font lacks ${where}`;
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 the ${total} letters with ${fill(drawn)}.`);
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} of ${total} letters with it, and the ${rest} it lacks with ${fill(others)}.` : ` It has none of the ${total} letters, so Chrome drew them with ${fill(others)}.`);
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 (machineHas(f)) lines.push(`- Font: ${look.family} ${look.weight}, as installed on this machine. It has none of the ${total} letters, so Chrome drew them with ${fill(drawn)}.`, ...lighterLines(look, drawn));
254
- else lines.push(`- Font: ${look.family} ${look.weight} was asked for by name, and this machine does not have it, so Chrome drew the letters with ${fill(drawn)}. Give easyfontkit's folder with --fonts to burn the real font.`);
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 them.`);
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} of ${total} letters with it, and the ${total - mine} it lacks with ${fill(others)}.`);
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. Their names, or [] when the font drew any
302
- // letter, no font was asked for, or nothing says.
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 && setsNone(f) ? f.assFallbacks.map((x) => x.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 drew = libass ? "captions.ass sets them all in" : "Chrome drew them all with";
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
- function assFont(look, machine = null, { fallback = null, budget = BUDGET } = {}) {
126
- const upright = (look.faces || []).filter((f) => !f.italic);
127
- const faces = upright.length ? upright : look.faces || [];
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 the size,
163
- // and the family name in it or in the next file that has one. A letter in
164
- // the range of a file that was not read, or is no font this kit reads, is
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
- function fallbacksOf(give) {
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 };
@@ -2,4 +2,4 @@
2
2
 
3
3
  // Kept here, not read from package.json: a skill install copies this folder
4
4
  // alone.
5
- module.exports = "0.1.1";
5
+ module.exports = "0.1.3";
@@ -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
- return shape({ format: "easycaptionkit", words, hard: Boolean(data.hard), fixed: handFixed(data), ...(first ? { first } : {}) }, data.words);
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
  }