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
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// The programs a run starts, ffmpeg, ffprobe and Chrome: where each one
|
|
4
|
+
// is, and whether it starts and answers as itself, asked before any work.
|
|
5
|
+
// Of one that does not, a run says only what is certain: what is at its
|
|
6
|
+
// path, as a folder or a file this user may not run, else what the system
|
|
7
|
+
// said when it could not start it, the first line the program printed and
|
|
8
|
+
// how it ended, or that it gave no answer in time. That line is the
|
|
9
|
+
// program's own, and says in its words what is at fault: nothing here
|
|
10
|
+
// guesses which file of a chain it is.
|
|
11
|
+
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
const { spawn } = require("node:child_process");
|
|
15
|
+
const { getSystemErrorMap } = require("node:util");
|
|
16
|
+
const { track } = require("./running");
|
|
17
|
+
|
|
18
|
+
const WINDOWS = process.platform === "win32";
|
|
19
|
+
|
|
20
|
+
const exe = (name) => (WINDOWS ? `${name}.exe` : name);
|
|
21
|
+
|
|
22
|
+
// A name with no folder in it, which is looked up on PATH.
|
|
23
|
+
const bare = (value) => !(value.includes("/") || (WINDOWS && value.includes("\\")));
|
|
24
|
+
|
|
25
|
+
// The errors a start can fail with that libuv has no name for, and Node
|
|
26
|
+
// calls "Unknown system error -86": each one's name and the system's
|
|
27
|
+
// words, by system and number. Linux's numbers are those of every
|
|
28
|
+
// processor Node.js runs on but MIPS.
|
|
29
|
+
const UNNAMED = {
|
|
30
|
+
linux: { 80: ["ELIBBAD", "accessing a corrupted shared library"], 116: ["ESTALE", "stale file handle"] },
|
|
31
|
+
darwin: { 70: ["ESTALE", "stale NFS file handle"], 85: ["EBADEXEC", "bad executable (or shared library)"], 86: ["EBADARCH", "bad CPU type in executable"], 87: ["ESHLIBVERS", "shared library version mismatch"], 88: ["EBADMACHO", "malformed Mach-o file"] },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// An error's name and the system's words for it, as ["EACCES",
|
|
35
|
+
// "permission denied"], or [] when neither is known.
|
|
36
|
+
const known = (err) => getSystemErrorMap().get(err.errno) || (!/^mips/.test(process.arch) && (UNNAMED[process.platform] || {})[-err.errno]) || [];
|
|
37
|
+
|
|
38
|
+
// The system's name for an error, as EACCES, else Node's.
|
|
39
|
+
const codeOf = (err) => known(err)[0] || err.code;
|
|
40
|
+
|
|
41
|
+
// The system's words for an error, as "permission denied", else its code.
|
|
42
|
+
const worded = (err) => known(err)[1] || err.code || String(err.message || err);
|
|
43
|
+
|
|
44
|
+
// How long a program may take to answer, in milliseconds: on a machine
|
|
45
|
+
// short of memory, with its swap full, ffmpeg 4.4 took more than 30
|
|
46
|
+
// seconds to say its version.
|
|
47
|
+
const LIMIT = 120_000;
|
|
48
|
+
|
|
49
|
+
// ─── where a program is ───────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
// A file this user may run, as a shell takes one: a file, not a folder,
|
|
52
|
+
// with the permission to run it. On Windows that permission is not looked
|
|
53
|
+
// at, and any file passes.
|
|
54
|
+
function runnable(file) {
|
|
55
|
+
try {
|
|
56
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
57
|
+
return fs.statSync(file).isFile();
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Whether a folder is there and this user may enter it, as a program run
|
|
64
|
+
// in it needs.
|
|
65
|
+
function enterable(folder) {
|
|
66
|
+
try {
|
|
67
|
+
fs.accessSync(folder, fs.constants.X_OK);
|
|
68
|
+
return fs.statSync(folder).isDirectory();
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The folders of PATH in order, each as a whole path: one given from the
|
|
75
|
+
// current folder is taken from the folder the run started in, so a
|
|
76
|
+
// program found there is found when it runs in another folder. An empty
|
|
77
|
+
// entry is passed over.
|
|
78
|
+
const folders = () =>
|
|
79
|
+
String(process.env.PATH || "")
|
|
80
|
+
.split(path.delimiter)
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.map((dir) => path.resolve(dir));
|
|
83
|
+
|
|
84
|
+
// The first program by a name on PATH that this user may run, in a folder
|
|
85
|
+
// `skip` does not pass over, or "".
|
|
86
|
+
function onPath(name, { skip = () => false } = {}) {
|
|
87
|
+
for (const dir of String(process.env.PATH || "").split(path.delimiter)) {
|
|
88
|
+
if (!dir || skip(dir)) continue;
|
|
89
|
+
const file = path.resolve(dir, name);
|
|
90
|
+
if (runnable(file)) return file;
|
|
91
|
+
}
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Whether a file, not a folder, is at a path, as Windows looks for one.
|
|
96
|
+
function there(file) {
|
|
97
|
+
try {
|
|
98
|
+
return !fs.statSync(file).isDirectory();
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Whether Windows runs a file by its own name: one whose name has an
|
|
105
|
+
// extension. It runs a name with none only as that name with .com or .exe
|
|
106
|
+
// added, which libuv looks for next to it.
|
|
107
|
+
function runsAsNamed(file) {
|
|
108
|
+
const name = path.basename(file);
|
|
109
|
+
const dot = name.indexOf(".");
|
|
110
|
+
return dot >= 0 && dot < name.length - 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The file a path goes through as if it were a folder, as notes.txt in
|
|
114
|
+
// notes.txt/ffmpeg, or "".
|
|
115
|
+
function fileOnTheWay(file) {
|
|
116
|
+
const parts = file.split(path.sep);
|
|
117
|
+
for (let i = 2; i < parts.length; i++) {
|
|
118
|
+
const part = parts.slice(0, i).join(path.sep);
|
|
119
|
+
try {
|
|
120
|
+
if (!fs.statSync(part).isDirectory()) return part;
|
|
121
|
+
} catch {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// What keeps the system from running the file at a path, as far as can be
|
|
129
|
+
// told for certain before it runs: null when nothing does, else the words
|
|
130
|
+
// for it, and in `on` what the system does with such a file as it looks
|
|
131
|
+
// for a program on PATH. It "passes" over a folder, a named pipe, a
|
|
132
|
+
// socket and a device, none of which it runs, a link to nothing and a
|
|
133
|
+
// file this user may not run, to look in the next folder, and these are
|
|
134
|
+
// named when no program comes after them; it passes over nothing there
|
|
135
|
+
// and a folder this user may not enter "quietly", as these may hold no
|
|
136
|
+
// such program at all; and it "stops" at a path it cannot follow, as a
|
|
137
|
+
// loop of links. On Windows, where no permission to run a file is looked
|
|
138
|
+
// at, a file whose name has no extension passes, as Windows does not run
|
|
139
|
+
// it, unless a .com or an .exe by that name is next to it, which it runs.
|
|
140
|
+
function barred(file) {
|
|
141
|
+
let stat;
|
|
142
|
+
try {
|
|
143
|
+
stat = fs.statSync(file);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") {
|
|
146
|
+
let link = false;
|
|
147
|
+
try {
|
|
148
|
+
link = fs.lstatSync(file).isSymbolicLink();
|
|
149
|
+
} catch {
|
|
150
|
+
// Nothing there at all.
|
|
151
|
+
}
|
|
152
|
+
if (link) return { on: "passes", words: "is a link to a file that is not there" };
|
|
153
|
+
const through = err.code === "ENOTDIR" ? fileOnTheWay(file) : "";
|
|
154
|
+
return { on: "quietly", words: through ? `where there is none, as ${through} is a file, not a folder` : "where there is none" };
|
|
155
|
+
}
|
|
156
|
+
if (err.code === "EACCES" || err.code === "EPERM") return { on: "quietly", words: "may not be run: permission denied" };
|
|
157
|
+
if (err.code === "ELOOP") return { on: "stops", words: "goes through a loop of links" };
|
|
158
|
+
return { on: "stops", words: `cannot be looked at: ${worded(err)}` };
|
|
159
|
+
}
|
|
160
|
+
if (stat.isDirectory()) return { on: "passes", words: "is a folder" };
|
|
161
|
+
if (WINDOWS) return runsAsNamed(file) || there(`${file}.com`) || there(`${file}.exe`) ? null : { on: "passes", words: "has no .exe in its name, so Windows does not run it" };
|
|
162
|
+
if (!stat.isFile()) return { on: "passes", words: stat.isFIFO() ? "is a named pipe" : stat.isSocket() ? "is a socket" : "is a device" };
|
|
163
|
+
try {
|
|
164
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
165
|
+
} catch {
|
|
166
|
+
return { on: "passes", words: (stat.mode & 0o111) === 0 ? "is not executable" : "may not be run: permission denied" };
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── what a program said ──────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
// What `file` did when run with `args`, given `limit` milliseconds: { code,
|
|
174
|
+
// signal, stdout, stderr }, { late: true } when it gave no answer in that
|
|
175
|
+
// time and was stopped (see late()), or { err } when the system did not
|
|
176
|
+
// start it. What it printed is kept up to `cap` bytes.
|
|
177
|
+
function ask(file, args, { limit = LIMIT, cap = 1 << 20 } = {}) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
let child;
|
|
180
|
+
try {
|
|
181
|
+
child = track(spawn(file, args, { stdio: ["ignore", "pipe", "pipe"] }));
|
|
182
|
+
} catch (err) {
|
|
183
|
+
resolve({ err });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let timer = null;
|
|
187
|
+
let done = false;
|
|
188
|
+
const end = (result) => {
|
|
189
|
+
if (done) return;
|
|
190
|
+
done = true;
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
resolve(result);
|
|
193
|
+
};
|
|
194
|
+
// A start that fails gives its error before the close.
|
|
195
|
+
child.on("error", (err) => end({ err }));
|
|
196
|
+
// With no file descriptor left Node makes no pipes, and says why a
|
|
197
|
+
// moment later.
|
|
198
|
+
if (!child.stdout || !child.stderr) return;
|
|
199
|
+
const said = { stdout: [], stderr: [] };
|
|
200
|
+
let size = 0;
|
|
201
|
+
for (const name of ["stdout", "stderr"]) {
|
|
202
|
+
child[name].on("data", (chunk) => {
|
|
203
|
+
if (size >= cap) return;
|
|
204
|
+
said[name].push(chunk);
|
|
205
|
+
size += chunk.length;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// A program that hands its output on to one that goes on running may
|
|
209
|
+
// never close it, so the answer is not waited for past the limit.
|
|
210
|
+
timer = setTimeout(() => {
|
|
211
|
+
try {
|
|
212
|
+
child.kill("SIGKILL");
|
|
213
|
+
} catch {
|
|
214
|
+
// Gone already.
|
|
215
|
+
}
|
|
216
|
+
child.stdout.destroy();
|
|
217
|
+
child.stderr.destroy();
|
|
218
|
+
end({ late: true });
|
|
219
|
+
}, limit);
|
|
220
|
+
child.on("close", (code, signal) => end({ code, signal, stdout: Buffer.concat(said.stdout).toString("utf8"), stderr: Buffer.concat(said.stderr).toString("utf8") }));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// A line a program printed, in quotes, as it printed it, with its control
|
|
225
|
+
// characters written out, as \r or \x1b. A line longer than 200 characters
|
|
226
|
+
// keeps its first 120 and its last 80, as its end most often says why.
|
|
227
|
+
function quoted(line) {
|
|
228
|
+
const chars = Array.from(String(line)).map((c) => {
|
|
229
|
+
if (c === "\t") return "\\t";
|
|
230
|
+
if (c === "\r") return "\\r";
|
|
231
|
+
if (!/[\p{Cc}\p{Cf}]/u.test(c)) return c;
|
|
232
|
+
const n = c.codePointAt(0);
|
|
233
|
+
return n < 0x100 ? `\\x${n.toString(16).padStart(2, "0")}` : `\\u${n.toString(16).padStart(4, "0")}`;
|
|
234
|
+
});
|
|
235
|
+
const shown = chars.length > 200 ? [...chars.slice(0, 120), "...", ...chars.slice(-80)] : chars;
|
|
236
|
+
return `"${shown.join("")}"`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// The first line with anything in it of what a program printed, or null.
|
|
240
|
+
const firstLine = (text) =>
|
|
241
|
+
String(text || "")
|
|
242
|
+
.split(/\r?\n/)
|
|
243
|
+
.find((line) => line.trim()) || null;
|
|
244
|
+
|
|
245
|
+
// What a program that ran did, where it did not answer: the first line it
|
|
246
|
+
// printed on its error output, where a program says what is wrong, else on
|
|
247
|
+
// its output, and how it ended. ffmpeg, ffprobe and Chrome give their
|
|
248
|
+
// versions on their output, so a program that ended well and printed on
|
|
249
|
+
// its error output alone is said to.
|
|
250
|
+
function happened(r) {
|
|
251
|
+
const line = firstLine(r.stderr);
|
|
252
|
+
if (line && r.code === 0 && !firstLine(r.stdout)) return `it printed ${quoted(line)} on its error output, nothing on its output, and exited with 0`;
|
|
253
|
+
const ended = r.signal ? `was stopped by ${r.signal}` : `exited with ${r.code}`;
|
|
254
|
+
const said = line || firstLine(r.stdout);
|
|
255
|
+
if (said) return `it printed ${quoted(said)} and ${ended}`;
|
|
256
|
+
return r.signal ? `it was stopped by ${r.signal}` : `it printed nothing and exited with ${r.code}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The first line of a file that starts with #!, which names the program
|
|
260
|
+
// the system starts the file with, or null. It is opened without waiting,
|
|
261
|
+
// as a named pipe put in the file's place since it was looked at would
|
|
262
|
+
// wait for a writer for good, and read only when it is a file.
|
|
263
|
+
function bangLine(file) {
|
|
264
|
+
let fd;
|
|
265
|
+
try {
|
|
266
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0));
|
|
267
|
+
if (!fs.fstatSync(fd).isFile()) return null;
|
|
268
|
+
const bytes = Buffer.alloc(256);
|
|
269
|
+
const got = bytes.subarray(0, fs.readSync(fd, bytes, 0, bytes.length, 0));
|
|
270
|
+
if (got.indexOf("#!") !== 0) return null;
|
|
271
|
+
const end = got.indexOf(10);
|
|
272
|
+
return got.subarray(0, end < 0 ? got.length : end).toString("utf8");
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
} finally {
|
|
276
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// What the system said when it did not start a file the checks found
|
|
281
|
+
// nothing wrong with. Off Windows it says ENOENT of a file that is there
|
|
282
|
+
// when an interpreter or a loader the file needs is not, so a script's
|
|
283
|
+
// first line, which names its interpreter, is shown with it. Windows runs
|
|
284
|
+
// no interpreter a first line names.
|
|
285
|
+
function unstartable(file, err) {
|
|
286
|
+
if (["ENOEXEC", "EFTYPE", "EBADARCH", "EBADMACHO"].includes(codeOf(err))) return "it is not a program this system can run";
|
|
287
|
+
const line = WINDOWS ? null : bangLine(file);
|
|
288
|
+
const first = line === null ? "" : `; its first line is ${quoted(line)}`;
|
|
289
|
+
if (codeOf(err) === "ENOENT" && !WINDOWS) return `the system could not start it, as an interpreter or a loader it needs is not there${first}`;
|
|
290
|
+
return `the system could not start it: ${worded(err)}${first}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// The errors that say the system is short of something to start a program
|
|
294
|
+
// with, not that the program is wrong: what it is short of, and what to do.
|
|
295
|
+
const SHORT = {
|
|
296
|
+
EMFILE: "too many open files. Raise the limit on open files (ulimit -n) and run it again",
|
|
297
|
+
ENFILE: "the system has too many files open. Run it again once fewer are",
|
|
298
|
+
EAGAIN: "the system starts no more processes for now. Run it again once fewer are running",
|
|
299
|
+
ENOMEM: "not enough memory. Free some and run it again",
|
|
300
|
+
E2BIG: "its environment is too large. Run it with fewer or shorter environment variables",
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// The errors a start fails with that the system passes over, as glibc's
|
|
304
|
+
// execvp does, to try the next folder of PATH: a file that is not there or
|
|
305
|
+
// may not be run, or one whose interpreter or loader is not there.
|
|
306
|
+
const PASSED = new Set(["ENOENT", "EACCES", "ENOTDIR", "ESTALE", "ENODEV", "ETIMEDOUT"]);
|
|
307
|
+
|
|
308
|
+
// The error for a program the system could not start for want of something
|
|
309
|
+
// it is short of, as open files: no fault of the program's.
|
|
310
|
+
const short = (name, err) => Object.assign(new Error(`easycaptionkit could not start ${name}: ${SHORT[err.code]}.`), { unusable: true, why: `the system could not start ${name}: ${SHORT[err.code].replace(/\. .*$/, "")}` });
|
|
311
|
+
|
|
312
|
+
// The error for a program that gave no answer in `limit` ms when asked
|
|
313
|
+
// `what`, as "its version": ffmpeg on a machine short of memory can be
|
|
314
|
+
// that slow as well as a program that is not ffmpeg, so only that is said,
|
|
315
|
+
// and to run again, with no advice to install another.
|
|
316
|
+
function late(spec, place, what, limit) {
|
|
317
|
+
const words = `gave no answer in ${limit / 1000} s when asked ${what}`;
|
|
318
|
+
return Object.assign(new Error(`easycaptionkit could not check ${spec.name}, ${place.file}: it ${words}, as on a machine short of memory. ${spec.advice.late}`), { unusable: true, why: placed(place, words) });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The error for a program a run checked before any work that the system
|
|
322
|
+
// did not start during the work, `name` being what messages call it: what
|
|
323
|
+
// the system is short of, else the system's words, never its code alone,
|
|
324
|
+
// and what to check, as a program that answered when the run began can
|
|
325
|
+
// have been taken away or changed since.
|
|
326
|
+
function startFailure(file, err, name = path.basename(file)) {
|
|
327
|
+
if (Object.hasOwn(SHORT, err.code)) return short(name, err);
|
|
328
|
+
return new Error(`easycaptionkit could not start ${name}, ${file}, again: ${worded(err)}. Check that it is still there and may be run, and run easycaptionkit again.`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── finding a program and asking it ──────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
// A program a run looks for, as ffmpeg.js and chrome.js give it: { name,
|
|
334
|
+
// variable, args, answers, names, first, known, ask, none, advice }.
|
|
335
|
+
// `name` is what messages call it, `variable` the one that names it,
|
|
336
|
+
// `args` what asks its version and `answers(stdout)` whether what it
|
|
337
|
+
// printed then answers as it does, after it exited with 0. `names` are its
|
|
338
|
+
// names on PATH. Where the variable is not set, `first()` gives places
|
|
339
|
+
// looked at before PATH, each { file, name, where, next }, of which the
|
|
340
|
+
// first that is there is taken, and `known` folders looked in where PATH
|
|
341
|
+
// has nothing by its names, as package managers put it there for an agent
|
|
342
|
+
// or an app whose PATH is short. With `ask` false, a program is taken once
|
|
343
|
+
// nothing is in the way of running it, as Chrome on Windows, which prints
|
|
344
|
+
// no version. `none` is the message when there is none, and `advice` the
|
|
345
|
+
// sentence that ends one about a program the run cannot use: `variable`
|
|
346
|
+
// for one the variable names, `next` for ffprobe next to ffmpeg, `found`
|
|
347
|
+
// for the others, and `late` for one that gave no answer in time.
|
|
348
|
+
|
|
349
|
+
// Where a run found a program, in words that name its place and say what
|
|
350
|
+
// happened there.
|
|
351
|
+
function placed(place, words) {
|
|
352
|
+
if (place.variable && !bare(place.value)) return `${place.variable} points to ${place.file}, ${words.startsWith("where ") ? words : `which ${words}`}`;
|
|
353
|
+
if (place.variable) return `${place.variable} names ${place.value}, found on PATH at ${place.file}, which ${words}`;
|
|
354
|
+
return `the ${place.name} ${place.where}, ${place.file}, ${words}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// The error for a program a run cannot use: the message, and in `why` the
|
|
358
|
+
// words that name its place and what happened, as a note gives them.
|
|
359
|
+
function unusable(spec, place, words) {
|
|
360
|
+
const why = placed(place, words);
|
|
361
|
+
const advice = place.variable ? spec.advice.variable : place.next ? spec.advice.next : spec.advice.found;
|
|
362
|
+
return Object.assign(new Error(`easycaptionkit needs ${spec.name}, and ${why}. ${advice}`), { unusable: true, why });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Whether what a program did when asked its version answers as `spec`.
|
|
366
|
+
const answered = (spec, r) => !r.err && !r.late && r.code === 0 && !r.signal && spec.answers(r.stdout);
|
|
367
|
+
|
|
368
|
+
// The places in turn: what keeps each from running, then, unless `spec`
|
|
369
|
+
// says not to, what it did when asked its version. The first that answers
|
|
370
|
+
// is { found: place, stdout }. A place `alone`, the one a variable or a
|
|
371
|
+
// place before PATH gives, is the only one, and what is wrong with it is
|
|
372
|
+
// said. Of places on PATH, the system's search passes over some (see
|
|
373
|
+
// barred() and PASSED), and the first of those is given as `passed`; at
|
|
374
|
+
// any other the search stops, with what is wrong there.
|
|
375
|
+
async function walk(spec, places, limit, { alone = false } = {}) {
|
|
376
|
+
let passed = null;
|
|
377
|
+
for (const place of places) {
|
|
378
|
+
let seen = barred(place.file);
|
|
379
|
+
if (!seen) {
|
|
380
|
+
if (spec.ask === false) return { found: place, stdout: "" };
|
|
381
|
+
const r = await ask(place.file, spec.args, { limit, cap: 1 << 16 });
|
|
382
|
+
if (r.err && Object.hasOwn(SHORT, r.err.code)) throw short(spec.name, r.err);
|
|
383
|
+
if (r.late) throw late(spec, place, "its version", limit);
|
|
384
|
+
if (answered(spec, r)) return { found: place, stdout: r.stdout };
|
|
385
|
+
// A file taken away or changed since it was looked at is said to be
|
|
386
|
+
// what it is now.
|
|
387
|
+
if (r.err) seen = barred(place.file) || { on: PASSED.has(codeOf(r.err)) ? "passes" : "stops", words: `did not run as ${spec.name}: ${unstartable(place.file, r.err)}` };
|
|
388
|
+
else seen = { on: "stops", words: `did not run as ${spec.name}: ${happened(r)}` };
|
|
389
|
+
}
|
|
390
|
+
if (alone || seen.on === "stops") throw unusable(spec, place, seen.words);
|
|
391
|
+
if (seen.on === "passes" && !passed) passed = { place, words: seen.words };
|
|
392
|
+
}
|
|
393
|
+
return { passed };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The places a program is looked for on PATH, by each of its names, and
|
|
397
|
+
// in the folders `known` where PATH has nothing by them.
|
|
398
|
+
function onPathPlaces(spec, value) {
|
|
399
|
+
const names = value ? [value, ...(WINDOWS ? [exe(value)] : [])] : spec.names;
|
|
400
|
+
const named = value ? { variable: spec.variable, value } : {};
|
|
401
|
+
return names.flatMap((name) => folders().map((dir) => ({ file: path.join(dir, name), name, where: "on PATH", ...named })));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const knownPlaces = (spec) => (WINDOWS ? [] : (spec.known || []).flatMap((dir) => spec.names.map((name) => ({ file: path.join(dir, name), name, where: `in ${dir}` }))));
|
|
405
|
+
|
|
406
|
+
// The program a run uses, found and asked its version before any work, as
|
|
407
|
+
// { file, stdout, place }: the path its variable gives, as it is, or the
|
|
408
|
+
// first program by its name on PATH the system would start; with the
|
|
409
|
+
// variable unset, the first of the places before PATH that is there, else
|
|
410
|
+
// the first on PATH, else one in the known folders. It is taken only when
|
|
411
|
+
// it exits with 0 and its answer is one `spec` takes; otherwise the run
|
|
412
|
+
// stops before any work, with what is wrong with it, what the system said
|
|
413
|
+
// or what it printed (see unusable()), and when there is none, with
|
|
414
|
+
// `spec.none`. Every run checks the programs it uses with this first.
|
|
415
|
+
async function find(spec, limit = LIMIT) {
|
|
416
|
+
const value = process.env[spec.variable] || "";
|
|
417
|
+
const through = async (places, options) => {
|
|
418
|
+
const { found, stdout, passed } = await walk(spec, places, limit, options);
|
|
419
|
+
return found ? { file: found.file, stdout, place: found } : { passed };
|
|
420
|
+
};
|
|
421
|
+
if (value && !bare(value)) return through([{ file: path.resolve(value), variable: spec.variable, value }], { alone: true });
|
|
422
|
+
if (!value) {
|
|
423
|
+
const first = (spec.first ? spec.first() : []).find((place) => fs.existsSync(place.file));
|
|
424
|
+
if (first) return through([first], { alone: true });
|
|
425
|
+
}
|
|
426
|
+
let result = await through(onPathPlaces(spec, value));
|
|
427
|
+
if (!result.file && !result.passed && !value) result = await through(knownPlaces(spec));
|
|
428
|
+
if (result.file) return result;
|
|
429
|
+
if (result.passed) throw unusable(spec, result.passed.place, result.passed.words);
|
|
430
|
+
if (value) throw Object.assign(new Error(`easycaptionkit needs ${spec.name}, and ${spec.variable} names ${value}, which is not on PATH. ${spec.advice.variable}`), { unusable: true, why: `${spec.variable} names ${value}, which is not on PATH` });
|
|
431
|
+
throw Object.assign(new Error(spec.none), { unusable: true, why: `no ${spec.name} was found` });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// The file a run would take for a program by what is at each place alone,
|
|
435
|
+
// not run, or "": the path its variable gives, as it is, else as find()
|
|
436
|
+
// looks, where nothing keeps a file from running. ffprobe is looked for
|
|
437
|
+
// next to it, where a run uses ffprobe alone.
|
|
438
|
+
function locate(spec) {
|
|
439
|
+
const value = process.env[spec.variable] || "";
|
|
440
|
+
if (value && !bare(value)) return path.resolve(value);
|
|
441
|
+
if (!value) {
|
|
442
|
+
const first = (spec.first ? spec.first() : []).find((place) => fs.existsSync(place.file));
|
|
443
|
+
if (first) return first.file;
|
|
444
|
+
}
|
|
445
|
+
// The first place nothing keeps from running, as { file }, else whether
|
|
446
|
+
// the search stopped at one or passed over one.
|
|
447
|
+
const look = (places) => {
|
|
448
|
+
let passed = false;
|
|
449
|
+
for (const place of places) {
|
|
450
|
+
const seen = barred(place.file);
|
|
451
|
+
if (!seen) return { file: place.file };
|
|
452
|
+
if (seen.on === "stops") return { stopped: true };
|
|
453
|
+
if (seen.on === "passes") passed = true;
|
|
454
|
+
}
|
|
455
|
+
return { passed };
|
|
456
|
+
};
|
|
457
|
+
const onPath = look(onPathPlaces(spec, value));
|
|
458
|
+
if (onPath.file) return onPath.file;
|
|
459
|
+
if (onPath.stopped || onPath.passed || value) return "";
|
|
460
|
+
return look(knownPlaces(spec)).file || "";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// What the program found at `place` printed when run with `args` after it
|
|
464
|
+
// answered its version, as asked about `what` ("its encoders"): its
|
|
465
|
+
// output, read only once it ended well. One that fails or is stopped
|
|
466
|
+
// stops the run with how it ended, so a program that cannot answer is not
|
|
467
|
+
// taken for one without the encoder or the filter asked for, and one that
|
|
468
|
+
// gives no answer in time stops it as late() says.
|
|
469
|
+
async function probe(spec, place, args, what, limit = LIMIT) {
|
|
470
|
+
const r = await ask(place.file, args, { limit });
|
|
471
|
+
const asked = args.filter((arg) => arg !== "-hide_banner").join(" ");
|
|
472
|
+
if (r.err && Object.hasOwn(SHORT, r.err.code)) throw short(spec.name, r.err);
|
|
473
|
+
if (r.late) throw late(spec, place, `${what} (${asked})`, limit);
|
|
474
|
+
if (!r.err && r.code === 0 && !r.signal) return r.stdout;
|
|
475
|
+
throw unusable(spec, place, `failed when asked ${what} (${asked}): ${r.err ? unstartable(place.file, r.err) : happened(r)}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = { exe, enterable, onPath, quoted, happened, unstartable, find, locate, probe, startFailure, LIMIT };
|
|
@@ -13,6 +13,7 @@ const { launch, openPage, evaluate } = require("./chrome");
|
|
|
13
13
|
const { cueLines } = require("./cues");
|
|
14
14
|
const { inkOf } = require("./places");
|
|
15
15
|
const { writeApng } = require("./apng");
|
|
16
|
+
const { clusters } = require("./text");
|
|
16
17
|
|
|
17
18
|
// The cue's lines as words, each with whether a space goes before it.
|
|
18
19
|
const lineWords = cueLines;
|
|
@@ -198,11 +199,14 @@ function pageHtml(look, place, style, data) {
|
|
|
198
199
|
|
|
199
200
|
// Draws every state of the timeline to PNGs in `dir` and puts them in one
|
|
200
201
|
// animated PNG. Returns { file, y, height, reach, states, fonts, first,
|
|
201
|
-
// failedFonts, shrunk, scales }: the band the PNGs cover, the rows
|
|
202
|
-
// captions reach in it, how many states there are, which fonts drew
|
|
203
|
-
// letters and which of them is the first of the list this machine
|
|
204
|
-
//
|
|
205
|
-
|
|
202
|
+
// failedFonts, shrunk, scales, boxes }: the band the PNGs cover, the rows
|
|
203
|
+
// the captions reach in it, how many states there are, which fonts drew
|
|
204
|
+
// the letters and which of them is the first of the list this machine
|
|
205
|
+
// has, how many captions had to be drawn smaller to fit, and the letters
|
|
206
|
+
// Chrome drew as boxes. `boxed(letters, fonts)` gives those, of the
|
|
207
|
+
// letters of the captions and the fonts Chrome names for them
|
|
208
|
+
// (chromeBoxes); without it, none are.
|
|
209
|
+
async function renderStates({ cues, words, states, style, look, place, duration, dir, log = () => {}, boxed = null }) {
|
|
206
210
|
const W = place.frame.width;
|
|
207
211
|
const H = place.frame.height;
|
|
208
212
|
const ink = place.ink || inkOf(place.fontPx, { style, box: look.box });
|
|
@@ -248,7 +252,13 @@ async function renderStates({ cues, words, states, style, look, place, duration,
|
|
|
248
252
|
})()`,
|
|
249
253
|
60_000,
|
|
250
254
|
);
|
|
251
|
-
const
|
|
255
|
+
const named = await fontsOf(page, probeText);
|
|
256
|
+
// A letter no font Chrome found has is a box, which Chrome says the
|
|
257
|
+
// first font of the list drew. Those letters are left out when it is
|
|
258
|
+
// asked again, so the fonts it names drew the letters that show.
|
|
259
|
+
const boxes = boxed ? boxed([...new Set(probeText.split("\n").flatMap((word) => clusters(word)))], named) : [];
|
|
260
|
+
const rest = boxes.length ? withoutLetters(probeText, new Set(boxes)) : probeText;
|
|
261
|
+
const fonts = !boxes.length ? named : rest ? await fontsOf(page, rest) : [];
|
|
252
262
|
// The first font of the list this machine has: the one that draws
|
|
253
263
|
// plain Latin letters. The others draw only what it lacks.
|
|
254
264
|
const latin = await fontsOf(page, "Hamburgefonstiv");
|
|
@@ -305,12 +315,21 @@ async function renderStates({ cues, words, states, style, look, place, duration,
|
|
|
305
315
|
const shrunk = all.filter((s) => s < 1).length;
|
|
306
316
|
// The rows every state stays in, its outline and shadow included.
|
|
307
317
|
const reach = Number.isFinite(fit.top) ? { top: Math.max(0, Math.floor(fit.top)), bottom: Math.min(H, Math.ceil(fit.bottom)) } : null;
|
|
308
|
-
return { file: animation, y, height, reach, states: made.size + 1, fonts, first: latin.length ? latin[0].family : "", failedFonts: loaded || [], shrunk, smallest: all.reduce((a, b) => Math.min(a, b), 1), scales: fit.scales };
|
|
318
|
+
return { file: animation, y, height, reach, states: made.size + 1, fonts, first: latin.length ? latin[0].family : "", failedFonts: loaded || [], shrunk, smallest: all.reduce((a, b) => Math.min(a, b), 1), scales: fit.scales, boxes };
|
|
309
319
|
} finally {
|
|
310
320
|
await browser.close();
|
|
311
321
|
}
|
|
312
322
|
}
|
|
313
323
|
|
|
324
|
+
// The words of the probe, one a line, without the letters of `left`: a
|
|
325
|
+
// word of those letters alone goes.
|
|
326
|
+
const withoutLetters = (text, left) =>
|
|
327
|
+
text
|
|
328
|
+
.split("\n")
|
|
329
|
+
.map((word) => clusters(word).filter((c) => !left.has(c)).join(""))
|
|
330
|
+
.filter(Boolean)
|
|
331
|
+
.join("\n");
|
|
332
|
+
|
|
314
333
|
// Which fonts Chrome draws a text with, and how many letters each. Chrome
|
|
315
334
|
// says so only of text it has laid out, and a text just set is laid out at
|
|
316
335
|
// the next frame: the probe is laid out in the same step its text is set
|