@hajimism/dek 0.4.0 → 0.5.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 +92 -0
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/package.json +8 -7
- package/src/cli/address.ts +59 -15
- package/src/cli/commands.ts +47 -14
- package/src/cli/contract.ts +49 -3
- package/src/cli/files.ts +1 -0
- package/src/cli/format.ts +5 -4
- package/src/cli/goto.ts +41 -10
- package/src/cli/keep-alive.ts +12 -6
- package/src/cli/lint.ts +32 -13
- package/src/cli/ls.ts +5 -1
- package/src/cli/main.ts +48 -13
- package/src/cli/new.ts +14 -1
- package/src/cli/ref.ts +1 -0
- package/src/cli/result.ts +42 -4
- package/src/cli/scope.ts +40 -1
- package/src/cli/shot.ts +77 -6
- package/src/cli/show.ts +15 -2
- package/src/cli/text.ts +61 -9
- package/src/cli/theme.ts +50 -16
- package/src/cli/tty.ts +16 -2
- package/src/cli/voice.ts +11 -6
- package/src/core/assets.ts +18 -10
- package/src/core/attribute-value.ts +188 -0
- package/src/core/build.ts +3 -3
- package/src/core/css-scan.ts +173 -39
- package/src/core/css-transform.ts +57 -11
- package/src/core/css.ts +19 -13
- package/src/core/foreign-text.ts +138 -0
- package/src/core/github.ts +1 -0
- package/src/core/html-entities.ts +318 -0
- package/src/core/html-scan.ts +49 -27
- package/src/core/html.ts +21 -15
- package/src/core/lint/asset-refs.ts +44 -6
- package/src/core/lint/context.ts +2 -0
- package/src/core/lint/shared-styles.ts +26 -1
- package/src/core/lint/slide-html.ts +21 -18
- package/src/core/lint/theme-css.ts +64 -3
- package/src/core/lint.ts +15 -2
- package/src/core/mv.ts +28 -18
- package/src/core/order.ts +14 -0
- package/src/core/path.ts +8 -2
- package/src/core/playwright-module.d.ts +12 -1
- package/src/core/playwright-motion.ts +1 -0
- package/src/core/playwright-sheet.ts +1 -0
- package/src/core/playwright-visual.ts +27 -10
- package/src/core/playwright.ts +77 -19
- package/src/core/pptx.ts +1 -0
- package/src/core/ref-name.ts +8 -3
- package/src/core/ref.ts +145 -47
- package/src/core/resolve.ts +3 -2
- package/src/core/rumdl.ts +51 -25
- package/src/core/safe-fs.ts +19 -2
- package/src/core/sandbox.ts +217 -0
- package/src/core/shot-cache.ts +1 -0
- package/src/core/slide-eval-worker.ts +1 -0
- package/src/core/slide-measure.ts +1 -0
- package/src/core/slide-script-eval.ts +2 -0
- package/src/core/spawn.ts +136 -35
- package/src/core/sync.ts +3 -12
- package/src/core/tar.ts +315 -0
- package/src/core/text-contrast.ts +2 -0
- package/src/core/toml-keys.ts +116 -51
- package/src/core/url-attributes.ts +27 -9
- package/src/core/visual.ts +28 -9
- package/src/core/voice.ts +2 -1
- package/src/server/auth.ts +21 -0
- package/src/server/dev.ts +13 -5
- package/src/server/lock.ts +43 -13
- package/src/server/serial.ts +8 -2
- package/src/video/mux.ts +27 -3
- package/src/video/recorder.ts +3 -2
- package/src/video/worker.ts +1 -0
- package/src/voice/engine.ts +81 -18
- package/tsconfig.json +1 -1
package/src/core/voice.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import type { VoiceDict } from "./cue.ts";
|
|
6
6
|
import { deckPaths } from "./deck-paths.ts";
|
|
7
7
|
import { DekError } from "./error.ts";
|
|
8
|
+
import { compareCodeUnits } from "./order.ts";
|
|
8
9
|
import { cacheDir, deckProjectRoot } from "./path.ts";
|
|
9
10
|
import { readDeckFile } from "./resolve.ts";
|
|
10
11
|
import { writeInside } from "./safe-fs.ts";
|
|
@@ -184,7 +185,7 @@ export function loadVoiceDict(deckDir: string): VoiceDict {
|
|
|
184
185
|
export function writeVoiceDict(deckDir: string, dict: VoiceDict): string {
|
|
185
186
|
const path = deckPaths(deckDir).voiceDict;
|
|
186
187
|
const body = Object.entries(dict)
|
|
187
|
-
.sort(([a], [b]) => a
|
|
188
|
+
.sort(([a], [b]) => compareCodeUnits(a, b))
|
|
188
189
|
.map(([key, entry]) => {
|
|
189
190
|
const accent = entry.accent !== undefined ? `\naccent = ${entry.accent}` : "";
|
|
190
191
|
return `[${escapeTomlKey(key)}]\nkana = ${JSON.stringify(entry.kana)}${accent}\n`;
|
package/src/server/auth.ts
CHANGED
|
@@ -78,6 +78,27 @@ function forbidden(reason: string): Response {
|
|
|
78
78
|
return new Response(`Forbidden: ${reason}\n`, { status: 403 });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* What every answer tells the browser: take its type as given, hand it to no other origin's
|
|
83
|
+
* page, and show it in no other origin's frame. Another site can still make the browser ask, but
|
|
84
|
+
* gets nothing to play, apply as a stylesheet, tell apart by whether it loaded, or frame. The
|
|
85
|
+
* frame rule goes on every answer, not only pages: the voice or a listing opened in a frame is
|
|
86
|
+
* a page too.
|
|
87
|
+
*/
|
|
88
|
+
const SAME_ORIGIN_ONLY = {
|
|
89
|
+
"x-content-type-options": "nosniff",
|
|
90
|
+
"cross-origin-resource-policy": "same-origin",
|
|
91
|
+
"content-security-policy": "frame-ancestors 'self'",
|
|
92
|
+
} as const;
|
|
93
|
+
|
|
94
|
+
/** `response` with SAME_ORIGIN_ONLY set; the server passes every answer through it. */
|
|
95
|
+
export function keepToOwnOrigin(response: Response): Response {
|
|
96
|
+
for (const [name, value] of Object.entries(SAME_ORIGIN_ONLY)) {
|
|
97
|
+
response.headers.set(name, value);
|
|
98
|
+
}
|
|
99
|
+
return response;
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
/**
|
|
82
103
|
* Who may read a response. The audience sees the slides as they are shown; the presenter also
|
|
83
104
|
* sees what they are made from, and may move the deck.
|
package/src/server/dev.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { locateDeck, type Project } from "../core/resolve.ts";
|
|
|
6
6
|
import { annotateScript, livePlayerScript, liveReloadScript } from "../runtime/player.ts";
|
|
7
7
|
import {
|
|
8
8
|
clearance,
|
|
9
|
+
keepToOwnOrigin,
|
|
9
10
|
mayRead,
|
|
10
11
|
pairingResponse,
|
|
11
12
|
ROUTE_EXPOSURE,
|
|
@@ -75,9 +76,9 @@ export async function startDevServer(options: DevServerOptions): Promise<DevServ
|
|
|
75
76
|
if (options.deck !== undefined && isRefName(options.deck)) {
|
|
76
77
|
throw refReadOnlyError(options.deck);
|
|
77
78
|
}
|
|
78
|
-
// Before the port and the watchers; writing the lock checks again, for a server that raced us.
|
|
79
|
-
assertNoDevServer(project.root);
|
|
80
79
|
const { scopedDeckName, deckDir } = serverScope(project, options);
|
|
80
|
+
// Before the port and the watchers; writing the lock checks again, for a server that raced us.
|
|
81
|
+
assertNoDevServer(project.root, scopedDeckName);
|
|
81
82
|
const access = accessFor(options);
|
|
82
83
|
const embed = {
|
|
83
84
|
playerScript: await livePlayerScript(),
|
|
@@ -199,10 +200,17 @@ function listen(port: number | undefined, access: Access, routes: ServedRoutes)
|
|
|
199
200
|
port: port ?? 0,
|
|
200
201
|
// Never Bun's development error page: it shows the source and absolute paths to anyone.
|
|
201
202
|
development: false,
|
|
202
|
-
|
|
203
|
+
// Every answer dek writes leaves through one of these two, so none goes without
|
|
204
|
+
// keepToOwnOrigin. Bun answers a few requests itself before either runs: one its parser
|
|
205
|
+
// refuses (a bad request line, no Host, a body or headers too large) and an upgrade it
|
|
206
|
+
// cannot take; none of those has a body a page could use.
|
|
207
|
+
fetch: async (req, server) => {
|
|
208
|
+
const response = await answer(req, server, access, routes);
|
|
209
|
+
return response && keepToOwnOrigin(response);
|
|
210
|
+
},
|
|
203
211
|
error(error) {
|
|
204
212
|
console.error(error);
|
|
205
|
-
return new Response("Internal Server Error\n", { status: 500 });
|
|
213
|
+
return keepToOwnOrigin(new Response("Internal Server Error\n", { status: 500 }));
|
|
206
214
|
},
|
|
207
215
|
websocket: roomSockets(routes.rooms),
|
|
208
216
|
});
|
|
@@ -233,7 +241,7 @@ function answer(req: Request, server: Bun.Server<WsData>, access: Access, routes
|
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
/** Each socket joins its deck's room; only a presenter's socket may move it or point in it. */
|
|
236
|
-
function roomSockets(rooms: RouteContext["rooms"]): Bun.WebSocketHandler<WsData> {
|
|
244
|
+
export function roomSockets(rooms: RouteContext["rooms"]): Bun.WebSocketHandler<WsData> {
|
|
237
245
|
return {
|
|
238
246
|
// A position or a point is a few dozen bytes; anything near this is neither.
|
|
239
247
|
maxPayloadLength: 4 * 1024,
|
package/src/server/lock.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { DekError } from "../core/error.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { isDeckName } from "../core/path.ts";
|
|
5
|
+
import { readSourceIfExists, removeInside, writeInside } from "../core/safe-fs.ts";
|
|
5
6
|
|
|
6
7
|
export type DevServerLock = {
|
|
7
8
|
url: string;
|
|
@@ -88,30 +89,59 @@ export function readDevServerLock(root: string): DevServerLock | undefined {
|
|
|
88
89
|
return undefined;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
/** The
|
|
92
|
-
const claimedRoots = new Map<string,
|
|
92
|
+
/** The server each root a server in this process holds, which two racing servers both see. */
|
|
93
|
+
const claimedRoots = new Map<string, Omit<DevServerLock, "password">>();
|
|
93
94
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Throw when a live server, in this process or another, already holds `root`. `deck` is the one
|
|
97
|
+
* deck the server about to start would serve, none for the whole project.
|
|
98
|
+
*/
|
|
99
|
+
export function assertNoDevServer(root: string, deck?: string): void {
|
|
96
100
|
const existing = readDevServerLock(root);
|
|
97
101
|
const running =
|
|
98
|
-
claimedRoots.get(root) ?? (existing && isPidAlive(existing.pid) ? existing
|
|
102
|
+
claimedRoots.get(root) ?? (existing && isPidAlive(existing.pid) ? existing : undefined);
|
|
99
103
|
if (running !== undefined) {
|
|
100
|
-
throw new DekError(`dev server already running at ${running}`, {
|
|
104
|
+
throw new DekError(`dev server already running at ${running.url}`, {
|
|
101
105
|
path: serverLockPath(root),
|
|
102
|
-
hint:
|
|
106
|
+
hint: runningServerHint(root, running, deck),
|
|
103
107
|
});
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Where the running server shows what was asked for, or, when it serves one other deck only, how
|
|
113
|
+
* to get one that serves both: `dekc` started inside a deck's folder serves that deck alone.
|
|
114
|
+
*/
|
|
115
|
+
function runningServerHint(
|
|
116
|
+
root: string,
|
|
117
|
+
running: Omit<DevServerLock, "password">,
|
|
118
|
+
deck: string | undefined,
|
|
119
|
+
): string {
|
|
120
|
+
if (running.deck === deck) {
|
|
121
|
+
return `open ${running.url}`;
|
|
122
|
+
}
|
|
123
|
+
if (running.deck === undefined && deck !== undefined) {
|
|
124
|
+
return `open ${running.url}decks/${encodeURIComponent(deck)}/`;
|
|
125
|
+
}
|
|
126
|
+
// The lock is a file a repository could bring along: name only a deck this project has.
|
|
127
|
+
const named =
|
|
128
|
+
running.deck !== undefined &&
|
|
129
|
+
isDeckName(running.deck) &&
|
|
130
|
+
existsSync(join(root, "decks", running.deck, "script.md"))
|
|
131
|
+
? `decks/${running.deck}`
|
|
132
|
+
: "one deck";
|
|
133
|
+
const wanted = deck === undefined ? "the whole project" : `decks/${deck}`;
|
|
134
|
+
return `it serves only ${named}; to serve ${wanted}, stop it (pid ${running.pid}) and run \`dekc\` at the project root, which serves every deck`;
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
export function writeDevServerLock(
|
|
108
138
|
root: string,
|
|
109
139
|
url: string,
|
|
110
140
|
password?: string,
|
|
111
141
|
deck?: string,
|
|
112
142
|
): void {
|
|
113
|
-
assertNoDevServer(root);
|
|
114
|
-
claimedRoots.set(root, url);
|
|
143
|
+
assertNoDevServer(root, deck);
|
|
144
|
+
claimedRoots.set(root, { url, pid: process.pid, ...(deck ? { deck } : {}) });
|
|
115
145
|
try {
|
|
116
146
|
// A fresh file renamed into place, so it may hold the --remote password: its owner's only.
|
|
117
147
|
writeInside(
|
|
@@ -133,8 +163,8 @@ export function removeDevServerLock(root: string): void {
|
|
|
133
163
|
return;
|
|
134
164
|
}
|
|
135
165
|
try {
|
|
136
|
-
|
|
166
|
+
removeInside(serverLockPath(root), root);
|
|
137
167
|
} catch {
|
|
138
|
-
/*
|
|
168
|
+
/* a lock left behind names a process that is gone, which the next server sees */
|
|
139
169
|
}
|
|
140
170
|
}
|
package/src/server/serial.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `run` one at a time: a call while it runs queues one more run after it, however many
|
|
3
|
+
* calls come. Each caller's `run` reports its own failures, as diagnostics or a line on stderr;
|
|
4
|
+
* one that still throws is a bug, logged to the dev server's stderr as Bun.serve's error handler
|
|
5
|
+
* logs one, and the queued run still runs.
|
|
6
|
+
*/
|
|
1
7
|
export function createSerialTask(run: () => Promise<void>): () => void {
|
|
2
8
|
let running = false;
|
|
3
9
|
let queued = false;
|
|
@@ -13,8 +19,8 @@ export function createSerialTask(run: () => Promise<void>): () => void {
|
|
|
13
19
|
queued = false;
|
|
14
20
|
try {
|
|
15
21
|
await run();
|
|
16
|
-
} catch {
|
|
17
|
-
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(error);
|
|
18
24
|
}
|
|
19
25
|
} while (queued);
|
|
20
26
|
} finally {
|
package/src/video/mux.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// biome-ignore lint/style/noRestrictedImports: mkdtempSync, rmSync, and writeFileSync, for ffmpeg's frame list in a folder of its own in the system's temp folder.
|
|
1
2
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -5,16 +6,39 @@ import { DekError } from "../core/error.ts";
|
|
|
5
6
|
import { runPiped, workerCommand } from "../core/spawn.ts";
|
|
6
7
|
import type { VideoFrame } from "./recorder.ts";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** `ffmpeg -version` prints and exits at once; one that takes longer is stuck. */
|
|
10
|
+
const FFMPEG_PROBE_TIMEOUT_MS = 10_000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* True when ffmpeg is there to run: DEK_FFMPEG names one, or `ffmpeg -version` on PATH exits 0.
|
|
14
|
+
* An ffmpeg that does not answer within `timeoutMs` is killed, and is an error that says so.
|
|
15
|
+
*/
|
|
16
|
+
export function ffmpegResolved(timeoutMs = FFMPEG_PROBE_TIMEOUT_MS): boolean {
|
|
9
17
|
if (process.env.DEK_FFMPEG) {
|
|
10
18
|
return true;
|
|
11
19
|
}
|
|
20
|
+
let result: Bun.SyncSubprocess<"ignore", "ignore">;
|
|
12
21
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
// The environment as it is now, as runPiped passes it: left out, Bun passes the one dek
|
|
23
|
+
// started with. Nothing is piped, so a process it leaves behind cannot hold dek.
|
|
24
|
+
// biome-ignore lint/nursery/noJsRestrictedProperties: ffmpegResolved is synchronous, for tests too; the probe has its own timeout and SIGKILL, and the encode runs through runPiped.
|
|
25
|
+
result = Bun.spawnSync(["ffmpeg", "-version"], {
|
|
26
|
+
stdin: "ignore",
|
|
27
|
+
stdout: "ignore",
|
|
28
|
+
stderr: "ignore",
|
|
29
|
+
env: process.env,
|
|
30
|
+
timeout: timeoutMs,
|
|
31
|
+
killSignal: "SIGKILL",
|
|
32
|
+
});
|
|
15
33
|
} catch {
|
|
16
34
|
return false;
|
|
17
35
|
}
|
|
36
|
+
if (result.exitedDueToTimeout) {
|
|
37
|
+
throw new DekError(`ffmpeg did not answer \`ffmpeg -version\` within ${timeoutMs / 1000}s`, {
|
|
38
|
+
hint: "check the ffmpeg on PATH, or set DEK_FFMPEG to an ffmpeg that runs, then run the command again",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return result.exitCode === 0;
|
|
18
42
|
}
|
|
19
43
|
|
|
20
44
|
function ffmpegCommand(args: string[]): string[] {
|
package/src/video/recorder.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DekError } from "../core/error.ts";
|
|
2
2
|
import { moduleFilePath } from "../core/path.ts";
|
|
3
3
|
import { playwrightResolved } from "../core/playwright.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { runDrawingWorker } from "../core/sandbox.ts";
|
|
5
|
+
import { workerCommand } from "../core/spawn.ts";
|
|
5
6
|
import type { Position } from "../core/step.ts";
|
|
6
7
|
import { playbackSchedule, type Timeline } from "../core/timeline.ts";
|
|
7
8
|
|
|
@@ -85,7 +86,7 @@ function spawnVideoRunner(
|
|
|
85
86
|
request: VideoCaptureRequest,
|
|
86
87
|
options: { timeoutMs?: number },
|
|
87
88
|
): Promise<VideoCaptureResponse> {
|
|
88
|
-
return
|
|
89
|
+
return runDrawingWorker(workerCommand(bin), request, parseVideoResponse, {
|
|
89
90
|
label: "video capture failed",
|
|
90
91
|
hint: "install Playwright or set DEK_VIDEO",
|
|
91
92
|
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
package/src/video/worker.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
// biome-ignore lint/style/noRestrictedImports: mkdirSync, in a worker, for the folder its parent made with outputDir, which Chromium writes into.
|
|
2
3
|
import { mkdirSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import type { Browser } from "playwright";
|
package/src/voice/engine.ts
CHANGED
|
@@ -37,41 +37,78 @@ export function engineBaseUrl(engine: string): string {
|
|
|
37
37
|
return `http://127.0.0.1:${resolved}`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
export type EngineOptions = {
|
|
41
|
+
/** Covers the whole call, from connecting to the last byte of the answer. */
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** /speakers and /version answer from what the engine has loaded; a stall this long is a hang. */
|
|
46
|
+
const PROBE_TIMEOUT_MS = 10 * 1000;
|
|
47
|
+
|
|
48
|
+
/** Reading a sentence's accents takes well under a second; this leaves room for a busy machine. */
|
|
49
|
+
const QUERY_TIMEOUT_MS = 60 * 1000;
|
|
50
|
+
|
|
51
|
+
/** Synthesis on a CPU can take longer than the sentence lasts; a long one fits inside this. */
|
|
52
|
+
const SYNTHESIS_TIMEOUT_MS = 5 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
export async function fetchSpeakers(
|
|
55
|
+
baseUrl: string,
|
|
56
|
+
options: EngineOptions = {},
|
|
57
|
+
): Promise<VoiceSpeaker[]> {
|
|
58
|
+
return engineFetch(
|
|
59
|
+
baseUrl,
|
|
60
|
+
"/speakers",
|
|
61
|
+
options.timeoutMs ?? PROBE_TIMEOUT_MS,
|
|
62
|
+
(res) => res.json() as Promise<VoiceSpeaker[]>,
|
|
63
|
+
);
|
|
43
64
|
}
|
|
44
65
|
|
|
45
|
-
export async function fetchEngineVersion(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
export async function fetchEngineVersion(
|
|
67
|
+
baseUrl: string,
|
|
68
|
+
options: EngineOptions = {},
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
const text = await engineFetch(
|
|
71
|
+
baseUrl,
|
|
72
|
+
"/version",
|
|
73
|
+
options.timeoutMs ?? PROBE_TIMEOUT_MS,
|
|
74
|
+
(res) => res.text(),
|
|
75
|
+
);
|
|
76
|
+
return text.trim().replace(/^"|"$/g, "");
|
|
49
77
|
}
|
|
50
78
|
|
|
51
79
|
export async function fetchAudioQuery(
|
|
52
80
|
baseUrl: string,
|
|
53
81
|
text: string,
|
|
54
82
|
speaker: number,
|
|
83
|
+
options: EngineOptions = {},
|
|
55
84
|
): Promise<AudioQuery> {
|
|
56
|
-
|
|
85
|
+
return engineFetch(
|
|
57
86
|
baseUrl,
|
|
58
87
|
`/audio_query?text=${encodeURIComponent(text)}&speaker=${speaker}`,
|
|
88
|
+
options.timeoutMs ?? QUERY_TIMEOUT_MS,
|
|
89
|
+
(res) => res.json() as Promise<AudioQuery>,
|
|
59
90
|
{ method: "POST" },
|
|
60
91
|
);
|
|
61
|
-
return (await res.json()) as AudioQuery;
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
export async function fetchSynthesis(
|
|
65
95
|
baseUrl: string,
|
|
66
96
|
query: AudioQuery,
|
|
67
97
|
speaker: number,
|
|
98
|
+
options: EngineOptions = {},
|
|
68
99
|
): Promise<Buffer> {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
const body = await engineFetch(
|
|
101
|
+
baseUrl,
|
|
102
|
+
`/synthesis?speaker=${speaker}`,
|
|
103
|
+
options.timeoutMs ?? SYNTHESIS_TIMEOUT_MS,
|
|
104
|
+
(res) => res.arrayBuffer(),
|
|
105
|
+
{
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
body: JSON.stringify(query),
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
return Buffer.from(body);
|
|
75
112
|
}
|
|
76
113
|
|
|
77
114
|
export function resolveStyleId(speakers: VoiceSpeaker[], speaker: string): number {
|
|
@@ -161,11 +198,30 @@ export async function withEngine<T>(
|
|
|
161
198
|
}
|
|
162
199
|
}
|
|
163
200
|
|
|
164
|
-
|
|
201
|
+
/**
|
|
202
|
+
* What the engine answers at `path`, read by `read` within `timeoutMs`. An engine that cannot be
|
|
203
|
+
* reached is missing; one that is reached but does not answer in time is stuck, and says so.
|
|
204
|
+
*/
|
|
205
|
+
async function engineFetch<T>(
|
|
206
|
+
baseUrl: string,
|
|
207
|
+
path: string,
|
|
208
|
+
timeoutMs: number,
|
|
209
|
+
read: (res: Response) => Promise<T>,
|
|
210
|
+
init?: RequestInit,
|
|
211
|
+
): Promise<T> {
|
|
212
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
213
|
+
const timedOut = () =>
|
|
214
|
+
new DekError(`voice engine did not answer ${path.split("?")[0]} within ${timeoutMs / 1000}s`, {
|
|
215
|
+
hint: "check the engine log, and restart the engine if it is stuck, then run the command again",
|
|
216
|
+
});
|
|
165
217
|
let res: Response;
|
|
166
218
|
try {
|
|
167
|
-
|
|
219
|
+
// biome-ignore lint/style/noRestrictedGlobals: signal, an AbortSignal.timeout, bounds the request and read(res).
|
|
220
|
+
res = await fetch(`${baseUrl}${path}`, { ...init, signal });
|
|
168
221
|
} catch {
|
|
222
|
+
if (signal.aborted) {
|
|
223
|
+
throw timedOut();
|
|
224
|
+
}
|
|
169
225
|
throw engineMissingError(engineNameForUrl(baseUrl) ?? "voice engine", baseUrl);
|
|
170
226
|
}
|
|
171
227
|
if (!res.ok) {
|
|
@@ -173,5 +229,12 @@ async function engineFetch(baseUrl: string, path: string, init?: RequestInit): P
|
|
|
173
229
|
hint: "check the engine log",
|
|
174
230
|
});
|
|
175
231
|
}
|
|
176
|
-
|
|
232
|
+
try {
|
|
233
|
+
return await read(res);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (signal.aborted) {
|
|
236
|
+
throw timedOut();
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
177
240
|
}
|
package/tsconfig.json
CHANGED