@hajimism/dek 0.3.3 → 0.5.0

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.ja.md +2 -2
  3. package/README.md +2 -2
  4. package/package.json +5 -3
  5. package/skills/dek/SKILL.md +10 -3
  6. package/skills/dek-design/SKILL.md +42 -0
  7. package/src/cli/address.ts +95 -18
  8. package/src/cli/build.ts +2 -2
  9. package/src/cli/check.ts +2 -1
  10. package/src/cli/commands.ts +57 -19
  11. package/src/cli/contract.ts +58 -4
  12. package/src/cli/files.ts +2 -5
  13. package/src/cli/flags.ts +1 -1
  14. package/src/cli/format.ts +5 -4
  15. package/src/cli/goto.ts +44 -11
  16. package/src/cli/init.ts +10 -5
  17. package/src/cli/lint.ts +36 -16
  18. package/src/cli/ls.ts +5 -1
  19. package/src/cli/main.ts +51 -14
  20. package/src/cli/new.ts +28 -8
  21. package/src/cli/pptx.ts +9 -4
  22. package/src/cli/ref.ts +92 -9
  23. package/src/cli/result.ts +47 -6
  24. package/src/cli/scope.ts +57 -29
  25. package/src/cli/shot.ts +78 -6
  26. package/src/cli/show.ts +15 -2
  27. package/src/cli/text.ts +88 -11
  28. package/src/cli/theme.ts +58 -10
  29. package/src/cli/tty.ts +16 -2
  30. package/src/cli/voice.ts +15 -9
  31. package/src/core/alignment.ts +320 -0
  32. package/src/core/annotation-rows.ts +16 -4
  33. package/src/core/annotations.ts +19 -1
  34. package/src/core/assets.ts +18 -10
  35. package/src/core/attribute-value.ts +188 -0
  36. package/src/core/build.ts +3 -3
  37. package/src/core/config.ts +61 -12
  38. package/src/core/css-scan.ts +173 -39
  39. package/src/core/css-transform.ts +57 -11
  40. package/src/core/css.ts +19 -13
  41. package/src/core/diagnostic.ts +5 -1
  42. package/src/core/escape.ts +43 -0
  43. package/src/core/foreign-text.ts +138 -0
  44. package/src/core/github.ts +1 -0
  45. package/src/core/html-entities.ts +318 -0
  46. package/src/core/html-scan.ts +49 -27
  47. package/src/core/html.ts +21 -15
  48. package/src/core/line-break.ts +275 -0
  49. package/src/core/lint/asset-refs.ts +50 -8
  50. package/src/core/lint/files.ts +30 -5
  51. package/src/core/lint/keys.ts +7 -3
  52. package/src/core/lint/slide-html.ts +24 -19
  53. package/src/core/lint/slide-script.ts +7 -5
  54. package/src/core/mv.ts +57 -27
  55. package/src/core/order.ts +14 -0
  56. package/src/core/pack.ts +2 -2
  57. package/src/core/path.ts +8 -2
  58. package/src/core/playwright-module.d.ts +13 -1
  59. package/src/core/playwright-motion.ts +1 -0
  60. package/src/core/playwright-sheet.ts +1 -0
  61. package/src/core/playwright-visual.ts +207 -30
  62. package/src/core/playwright.ts +279 -44
  63. package/src/core/pptx-package.ts +461 -33
  64. package/src/core/pptx-paint.ts +1168 -0
  65. package/src/core/pptx-plan.ts +879 -0
  66. package/src/core/pptx-text.ts +204 -47
  67. package/src/core/pptx-theme.ts +222 -0
  68. package/src/core/pptx.ts +56 -8
  69. package/src/core/prose.ts +23 -0
  70. package/src/core/pseudo-text.ts +32 -10
  71. package/src/core/ref-name.ts +52 -8
  72. package/src/core/ref.ts +308 -54
  73. package/src/core/resolve.ts +23 -11
  74. package/src/core/rumdl.ts +51 -25
  75. package/src/core/safe-fs.ts +19 -2
  76. package/src/core/sandbox.ts +217 -0
  77. package/src/core/shot/morph.ts +1 -1
  78. package/src/core/shot/motion.ts +2 -3
  79. package/src/core/shot/still.ts +2 -2
  80. package/src/core/shot-cache.ts +26 -17
  81. package/src/core/slide-eval-worker.ts +1 -0
  82. package/src/core/slide-measure.ts +51 -1
  83. package/src/core/slide-script-eval.ts +12 -8
  84. package/src/core/slide-script.ts +114 -20
  85. package/src/core/spawn.ts +131 -32
  86. package/src/core/sync.ts +5 -14
  87. package/src/core/tar.ts +315 -0
  88. package/src/core/text-contrast.ts +2 -0
  89. package/src/core/toml-keys.ts +116 -51
  90. package/src/core/url-attributes.ts +27 -9
  91. package/src/core/visual-cache.ts +2 -0
  92. package/src/core/visual.ts +182 -24
  93. package/src/core/voice.ts +5 -6
  94. package/src/core/zod.ts +9 -1
  95. package/src/server/auth.ts +21 -0
  96. package/src/server/dev.ts +26 -7
  97. package/src/server/lock.ts +50 -7
  98. package/src/server/routes.ts +7 -2
  99. package/src/server/serial.ts +8 -2
  100. package/src/theme/default.css +4 -0
  101. package/src/video/bake.ts +6 -2
  102. package/src/video/mux.ts +27 -3
  103. package/src/video/recorder.ts +3 -2
  104. package/src/video/worker.ts +1 -0
  105. package/src/voice/engine.ts +81 -18
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
- export function ffmpegResolved(): boolean {
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
- const result = Bun.spawnSync(["ffmpeg", "-version"], { stdout: "pipe", stderr: "pipe" });
14
- return result.exitCode === 0;
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[] {
@@ -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 { runJsonWorker, workerCommand } from "../core/spawn.ts";
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 runJsonWorker(workerCommand(bin), request, parseVideoResponse, {
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 } : {}),
@@ -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";
@@ -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 async function fetchSpeakers(baseUrl: string): Promise<VoiceSpeaker[]> {
41
- const res = await engineFetch(baseUrl, "/speakers");
42
- return (await res.json()) as VoiceSpeaker[];
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(baseUrl: string): Promise<string> {
46
- const res = await engineFetch(baseUrl, "/version");
47
- const text = (await res.text()).trim();
48
- return text.replace(/^"|"$/g, "");
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
- const res = await engineFetch(
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 res = await engineFetch(baseUrl, `/synthesis?speaker=${speaker}`, {
70
- method: "POST",
71
- headers: { "content-type": "application/json" },
72
- body: JSON.stringify(query),
73
- });
74
- return Buffer.from(await res.arrayBuffer());
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
- async function engineFetch(baseUrl: string, path: string, init?: RequestInit): Promise<Response> {
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
- res = await fetch(`${baseUrl}${path}`, init);
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
- return res;
232
+ try {
233
+ return await read(res);
234
+ } catch (error) {
235
+ if (signal.aborted) {
236
+ throw timedOut();
237
+ }
238
+ throw error;
239
+ }
177
240
  }