@outerlayer/cli 0.32.0 → 0.33.0-next.20261021171625

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 (93) hide show
  1. package/README.md +15 -1
  2. package/dist/{agent-setup-E7CMDUR5.js → agent-setup-NSUX64RU.js} +10 -10
  3. package/dist/{artifact-link-cmd-VY6OM7S5.js → artifact-link-cmd-IGVFGHC2.js} +2 -2
  4. package/dist/{build-7QD3VJ6U.js → build-HU24C46Z.js} +3 -2
  5. package/dist/build-info.json +1 -1
  6. package/dist/{builder-REQJGZJP.js → builder-QECLC243.js} +5 -4
  7. package/dist/{check-COMHCLXW.js → check-4BL6XRWJ.js} +29 -28
  8. package/dist/{chunk-JH2AMJ4K.js → chunk-2FPEB36J.js} +2 -2
  9. package/dist/{chunk-HSUUTD4E.js → chunk-3H7NZPYG.js} +1 -1
  10. package/dist/{chunk-JFLVUA43.js → chunk-3RS3RNGH.js} +3 -3
  11. package/dist/{chunk-IWFYKNAO.js → chunk-3WW2TL7V.js} +45 -2
  12. package/dist/{chunk-SJZL5F3F.js → chunk-3Z3MPR55.js} +1 -51
  13. package/dist/{chunk-ZWBRHH3L.js → chunk-4LU7MFJO.js} +3 -3
  14. package/dist/{chunk-2JPVI45I.js → chunk-5T2VOSIL.js} +3 -2
  15. package/dist/{chunk-G2RRBBOX.js → chunk-6C3ZVE62.js} +1 -1
  16. package/dist/{chunk-Z7HAIZ6X.js → chunk-BSHUTMXV.js} +54 -5
  17. package/dist/{chunk-NIS2PIS4.js → chunk-D73OLAYE.js} +3 -3
  18. package/dist/{chunk-V6IYYQU3.js → chunk-FE3RTGJN.js} +2 -2
  19. package/dist/{chunk-XB7TRWI2.js → chunk-GI6LOIHS.js} +4 -4
  20. package/dist/{chunk-5TNEVWFJ.js → chunk-GXKWPOK6.js} +2 -2
  21. package/dist/{chunk-4XPAQXJF.js → chunk-H6Y5D6J7.js} +6 -6
  22. package/dist/{chunk-XKZPL3GE.js → chunk-INCKTGMM.js} +4 -4
  23. package/dist/{chunk-7F2VPHP7.js → chunk-JJ4W5LVF.js} +9 -9
  24. package/dist/{chunk-ZUOPFE22.js → chunk-JUXATN46.js} +1 -1
  25. package/dist/{chunk-BCWVTWS7.js → chunk-KURUM765.js} +4 -4
  26. package/dist/{chunk-IGIGNYZK.js → chunk-LESWVXRE.js} +4 -4
  27. package/dist/chunk-LK2S5XZH.js +99 -0
  28. package/dist/{emit-artifact-cmd-NCK3VCDM.js → chunk-LRBOBYQI.js} +8 -36
  29. package/dist/{chunk-VIHQLW4L.js → chunk-MV52FCSG.js} +9 -8
  30. package/dist/{chunk-WRW6QCI6.js → chunk-NHXJU7CM.js} +2 -2
  31. package/dist/{chunk-4I3ZDQFU.js → chunk-OF4TFWKR.js} +2 -2
  32. package/dist/{chunk-XAV6XCI2.js → chunk-OX7CHZTJ.js} +7 -7
  33. package/dist/{chunk-XTKH5VAC.js → chunk-QCDVKCHA.js} +2 -2
  34. package/dist/{chunk-SXJVL7NS.js → chunk-QNVIGO5X.js} +2 -2
  35. package/dist/{chunk-FKNOPMUD.js → chunk-RHBFNZ7J.js} +4 -4
  36. package/dist/{chunk-3PYMMGYO.js → chunk-UJIG5BU7.js} +10 -10
  37. package/dist/{chunk-HJUGOAM7.js → chunk-WXEPMPHK.js} +1 -1
  38. package/dist/{chunk-3DSC2FN3.js → chunk-X2Y3B4JH.js} +2 -2
  39. package/dist/{chunk-U4HMGT3L.js → chunk-X7ML37UF.js} +5 -5
  40. package/dist/{chunk-KCB6V5XH.js → chunk-XFS2TMOB.js} +5 -5
  41. package/dist/{chunk-UAFMC3PL.js → chunk-XRQ7DABQ.js} +1 -1
  42. package/dist/{chunk-V36AOTBP.js → chunk-XT5SOJRC.js} +379 -45
  43. package/dist/{chunk-CVGMB4N7.js → chunk-YGXKXL4Q.js} +13 -13
  44. package/dist/{chunk-AKKVPUGK.js → chunk-YW4PEGLN.js} +1 -1
  45. package/dist/{chunk-QDDPVBIS.js → chunk-ZAJKEHIE.js} +2 -2
  46. package/dist/{claude-launcher-IG7GKYIT.js → claude-launcher-RNMUZZ3Y.js} +5 -5
  47. package/dist/{cli-DLUADCTG.js → cli-PNQX4HRX.js} +144 -89
  48. package/dist/{command-JRWYFDNM.js → command-LA7T5YFX.js} +19 -18
  49. package/dist/{connect-cmd-K73AOVUR.js → connect-cmd-2AXEPDT2.js} +8 -8
  50. package/dist/{container-start-4RJRBPVB.js → container-start-4LIRN2LX.js} +20 -19
  51. package/dist/{context-cleanup-D5EGQHQV.js → context-cleanup-VSDT77BZ.js} +3 -3
  52. package/dist/{control-plane-launch-JCUNTTC3.js → control-plane-launch-DHMD5WBU.js} +6 -6
  53. package/dist/{dist-BE3LYGNH.js → dist-DY2ZNEBS.js} +2 -2
  54. package/dist/{docker-CVRCHNYC.js → docker-EOP32YLH.js} +5 -4
  55. package/dist/{doctor-A3A4X2HJ.js → doctor-FDXHHPDJ.js} +21 -20
  56. package/dist/emit-artifact-cmd-JY5V6WQI.js +43 -0
  57. package/dist/{emit-cmd-PSD7AAKJ.js → emit-cmd-T7NIBSF3.js} +6 -6
  58. package/dist/{emit-criteria-cmd-GTCZWV4B.js → emit-criteria-cmd-XRFBNFN7.js} +9 -9
  59. package/dist/emit-demo-cmd-JOVCVXIS.js +888 -0
  60. package/dist/{emit-finding-cmd-OUG72OQA.js → emit-finding-cmd-5RHKQHE6.js} +10 -10
  61. package/dist/{emit-result-cmd-YQ5DZMMR.js → emit-result-cmd-65KJQ2UM.js} +12 -12
  62. package/dist/{emit-step-cmd-QJXSHSQF.js → emit-step-cmd-YZIN6B6P.js} +10 -10
  63. package/dist/{findings-cmd-DYDEHJFI.js → findings-cmd-Z6CKWAG4.js} +10 -10
  64. package/dist/{fleet-readiness-JEQ5ITV6.js → fleet-readiness-7ROJ54ZF.js} +8 -8
  65. package/dist/{governing-source-JSYW4RCL.js → governing-source-NMGGSA6Q.js} +13 -13
  66. package/dist/{hook-fast-UFHR6EKZ.js → hook-fast-WQQYEXO5.js} +1 -1
  67. package/dist/index.js +4 -4
  68. package/dist/{init-SYNDV44H.js → init-OV6IOD2G.js} +20 -19
  69. package/dist/{init-CSXI3OQH.js → init-UBDZRWNE.js} +20 -19
  70. package/dist/{init-cmd-ALBPEG7I.js → init-cmd-YP4TVN2S.js} +18 -18
  71. package/dist/{init-update-PMDRKPA2.js → init-update-24K7ZZWX.js} +17 -17
  72. package/dist/{layout-3ISIED6A.js → layout-OQ7EJUMH.js} +4 -4
  73. package/dist/{login-browser-NTVVDJF7.js → login-browser-KP7YVYTN.js} +2 -2
  74. package/dist/{loop-XDJYLT4D.js → loop-4FEA6E2S.js} +29 -28
  75. package/dist/{machine-GC4MZB5W.js → machine-3BMBF33L.js} +19 -18
  76. package/dist/{machine-skills-I4DB2CP3.js → machine-skills-XWWYFNOZ.js} +4 -4
  77. package/dist/{runner-identity-D3XUB36T.js → runner-identity-HTQNM5AG.js} +5 -4
  78. package/dist/{runner-key-7QNJNPQ6.js → runner-key-7I5FDQKR.js} +20 -19
  79. package/dist/{session-sender-S6KG6VOM.js → session-sender-MKDKMZJD.js} +13 -13
  80. package/dist/{spec-cmd-4RD3A55B.js → spec-cmd-V7W3UV6X.js} +10 -10
  81. package/dist/{starter-pack-2WFNRGFA.js → starter-pack-6O7F6P7C.js} +5 -5
  82. package/dist/{status-ZR2N44KU.js → status-TJDVDR4H.js} +23 -22
  83. package/dist/{sync-cmd-NISS7HJJ.js → sync-cmd-AI4KTJ56.js} +12 -12
  84. package/dist/{watch-VSTJUIRH.js → watch-C7IS6VNS.js} +12 -12
  85. package/dist/{work-claim-cmd-27OJUZ56.js → work-claim-cmd-GFXIZWMU.js} +12 -12
  86. package/dist/{work-cmd-B7YCAK5M.js → work-cmd-UTD4Y7W3.js} +10 -10
  87. package/dist/{work-comment-cmd-2X55NLNL.js → work-comment-cmd-J3SRBK6A.js} +10 -10
  88. package/dist/{work-logs-cmd-2CE5B2DP.js → work-logs-cmd-SH7MQPA3.js} +10 -10
  89. package/dist/{work-open-pr-cmd-Q3HZJPSD.js → work-open-pr-cmd-FIX5QLK7.js} +11 -11
  90. package/dist/{work-pr-cmd-MT246E2A.js → work-pr-cmd-S4IBR7DE.js} +10 -10
  91. package/dist/{work-stop-cmd-CWNLRQKE.js → work-stop-cmd-NECXJEJN.js} +10 -10
  92. package/package.json +1 -1
  93. package/skill-pack/maintained/outerlayer-evidence/SKILL.md +39 -0
@@ -0,0 +1,888 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { runEmitArtifact } from './chunk-LRBOBYQI.js';
4
+ import './chunk-XFS2TMOB.js';
5
+ import './chunk-INCKTGMM.js';
6
+ import './chunk-ESVW4MJY.js';
7
+ import './chunk-TBT347UY.js';
8
+ import { VOICE_PACK_FILES, VOICE_PACK_VERSION } from './chunk-LK2S5XZH.js';
9
+ import { detectActiveSession, autoConnect } from './chunk-X2Y3B4JH.js';
10
+ import './chunk-LESWVXRE.js';
11
+ import './chunk-3H7NZPYG.js';
12
+ import { resolveRepoIdentity } from './chunk-XT5SOJRC.js';
13
+ import './chunk-ZAJKEHIE.js';
14
+ import './chunk-6C3ZVE62.js';
15
+ import './chunk-G2JWZDFA.js';
16
+ import { C } from './chunk-ANPNT6PM.js';
17
+ import { defaultFetch, resolveGatewayCredentials } from './chunk-F3EF56LQ.js';
18
+ import './chunk-PJDQPF5N.js';
19
+ import './chunk-APSYTJ6I.js';
20
+ import './chunk-KP6R7CV3.js';
21
+ import './chunk-HDSDUTX4.js';
22
+ import './chunk-NHXJU7CM.js';
23
+ import './chunk-ZJSNLVCE.js';
24
+ import './chunk-2O5KA67H.js';
25
+ import './chunk-BKO6JEZI.js';
26
+ import './chunk-HCVLP7XA.js';
27
+ import './chunk-BJ3KTMDH.js';
28
+ import './chunk-YW4PEGLN.js';
29
+ import './chunk-5BHIBJDA.js';
30
+ import './chunk-CDDED64E.js';
31
+ import './chunk-RA3O54FC.js';
32
+ import { launchedItemForSession } from './chunk-T2Q7ISPE.js';
33
+ import './chunk-WZBHBH3J.js';
34
+ import './chunk-CRB3POP4.js';
35
+ import './chunk-URIZRCRY.js';
36
+ import './chunk-TUSYOL2O.js';
37
+ import './chunk-JUXATN46.js';
38
+ import { ARTIFACT_MAX_BYTES, ARTIFACT_MAX_CAPTION_LENGTH, DEMO_RECORD_MEDIA_TYPE, DemoManifestSchema } from './chunk-3WW2TL7V.js';
39
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
40
+ import './chunk-XD4ZCMA4.js';
41
+ import './chunk-MESOOLF4.js';
42
+ import './chunk-Z5GEFMRW.js';
43
+ import { createHash } from 'crypto';
44
+ import { mkdtempSync, statSync, readFileSync, writeFileSync, rmSync, mkdirSync, accessSync, constants, existsSync, renameSync, createReadStream, createWriteStream } from 'fs';
45
+ import { tmpdir } from 'os';
46
+ import { resolve, join, dirname, delimiter, normalize, sep } from 'path';
47
+ import { execFileSync } from 'child_process';
48
+ import { once } from 'events';
49
+ import { createRequire as createRequire$1 } from 'module';
50
+ import { createGunzip } from 'zlib';
51
+
52
+ createRequire(import.meta.url);
53
+
54
+ // src/demo/criteria.ts
55
+ async function readRecordedCriteriaIds(params) {
56
+ const endpoint = new URL(`/v1/work-items/${params.item}`, params.url).toString();
57
+ const response = await params.fetchImpl(endpoint, {
58
+ method: "GET",
59
+ headers: { authorization: `Bearer ${params.apiKey}`, "x-outerlayer-app-id": params.appId }
60
+ });
61
+ if (!response.ok) throw new Error(`reading item ${params.item} failed (${response.status})`);
62
+ const body = await response.json();
63
+ const items = body.data?.criteria?.items;
64
+ if (!Array.isArray(items)) return void 0;
65
+ return new Set(items.flatMap((entry) => typeof entry.id === "string" ? [entry.id] : []));
66
+ }
67
+
68
+ // src/demo/fit.ts
69
+ var MAX_TEMPO = 1.3;
70
+ var SPEECH_LEAD_IN = 0.2;
71
+ var SPEECH_TAIL = 0.2;
72
+ var EPSILON = 1e-6;
73
+ function fitSpeech(speechSeconds, chapterSeconds) {
74
+ const budget = chapterSeconds - SPEECH_LEAD_IN - SPEECH_TAIL;
75
+ if (budget > 0 && speechSeconds <= budget + EPSILON) return { ok: true, tempo: 1 };
76
+ if (budget > 0 && speechSeconds / budget <= MAX_TEMPO + EPSILON) {
77
+ return { ok: true, tempo: Math.min(MAX_TEMPO, speechSeconds / budget) };
78
+ }
79
+ return { ok: false, overrunSeconds: speechSeconds / MAX_TEMPO - budget };
80
+ }
81
+
82
+ // src/demo/manifest-checks.ts
83
+ var END_TOLERANCE_SECONDS = 0.05;
84
+ function chapterLabel(index, title) {
85
+ return `chapter ${index + 1} ("${title}")`;
86
+ }
87
+ function checkManifest(params) {
88
+ const { manifest, videoSeconds, recordedIds } = params;
89
+ const seen = /* @__PURE__ */ new Map();
90
+ let previousEnd = 0;
91
+ for (const [index, chapter] of manifest.chapters.entries()) {
92
+ const label = chapterLabel(index, chapter.title);
93
+ if (chapter.end > videoSeconds + END_TOLERANCE_SECONDS) {
94
+ return `${label} ends at ${chapter.end} s, but the video is ${videoSeconds.toFixed(1)} s long`;
95
+ }
96
+ if (chapter.start < previousEnd - END_TOLERANCE_SECONDS) {
97
+ return `${label} starts at ${chapter.start} s, before the previous chapter ends at ${previousEnd} s \u2014 chapters must be in order and must not overlap`;
98
+ }
99
+ previousEnd = chapter.end;
100
+ if (chapter.criterionId === null) {
101
+ if (index !== 0) {
102
+ return `${label} names no criterion \u2014 only the opening chapter may omit one`;
103
+ }
104
+ continue;
105
+ }
106
+ const first = seen.get(chapter.criterionId);
107
+ if (first !== void 0) {
108
+ return `${label} names criterion ${chapter.criterionId}, which chapter ${first + 1} already shows \u2014 one chapter proves each criterion`;
109
+ }
110
+ seen.set(chapter.criterionId, index);
111
+ if (recordedIds !== void 0 && !recordedIds.has(chapter.criterionId)) {
112
+ return `${label} names criterion ${chapter.criterionId}, which is not on the item's recorded criteria \u2014 record it with \`emit criteria\`, or fix the id`;
113
+ }
114
+ }
115
+ return void 0;
116
+ }
117
+ function findProgram(name, env, platform = process.platform) {
118
+ const dirs = (env.PATH ?? env.Path ?? "").split(platform === "win32" ? ";" : delimiter).filter((dir) => dir !== "");
119
+ const extensions = platform === "win32" ? ["", ...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((ext) => ext !== "")] : [""];
120
+ for (const dir of dirs) {
121
+ for (const extension of extensions) {
122
+ const candidate = join(dir, name + extension);
123
+ try {
124
+ if (!statSync(candidate).isFile()) continue;
125
+ if (platform !== "win32") accessSync(candidate, constants.X_OK);
126
+ return candidate;
127
+ } catch {
128
+ }
129
+ }
130
+ }
131
+ return void 0;
132
+ }
133
+ var INSTALL_HINT = {
134
+ ffmpeg: "ffmpeg (apt install ffmpeg, brew install ffmpeg, or winget install Gyan.FFmpeg)",
135
+ ffprobe: "ffmpeg, which includes ffprobe (apt install ffmpeg, brew install ffmpeg, or winget install Gyan.FFmpeg)",
136
+ "espeak-ng": "espeak-ng (apt install espeak-ng, brew install espeak-ng, or the eSpeak NG installer on Windows)"
137
+ };
138
+ function missingProgramMessage(name, narrated) {
139
+ const hint = INSTALL_HINT[name] ?? name;
140
+ const base = `install ${hint}, then run the command again`;
141
+ return name === "espeak-ng" && narrated ? `espeak-ng is not on PATH \u2014 ${base}, or pass --no-narration to store the clips without speech` : `${name} is not on PATH \u2014 ${base}`;
142
+ }
143
+
144
+ // src/demo/media.ts
145
+ function clipArgs(plan) {
146
+ const duration = plan.end - plan.start;
147
+ const audio = plan.audio;
148
+ const delayMs = audio === void 0 ? 0 : Math.round(audio.delaySeconds * 1e3);
149
+ const speech = audio === void 0 ? [] : [
150
+ "-filter_complex",
151
+ `[1:a]${audio.tempo === 1 ? "" : `atempo=${audio.tempo.toFixed(4)},`}adelay=${delayMs}:all=1,aresample=48000[speech]`,
152
+ "-map",
153
+ "[speech]",
154
+ "-c:a",
155
+ "libopus",
156
+ "-b:a",
157
+ "48k"
158
+ ];
159
+ return [
160
+ "-y",
161
+ "-v",
162
+ "error",
163
+ // The cut sits before the input and the clip is re-encoded, so it starts on the exact frame, not the nearest key frame.
164
+ "-ss",
165
+ plan.start.toFixed(3),
166
+ "-t",
167
+ duration.toFixed(3),
168
+ "-i",
169
+ plan.video,
170
+ ...audio === void 0 ? [] : ["-i", audio.wavPath],
171
+ "-map",
172
+ "0:v:0",
173
+ ...audio === void 0 ? ["-an"] : speech,
174
+ "-c:v",
175
+ "libvpx-vp9",
176
+ "-b:v",
177
+ "0",
178
+ "-crf",
179
+ "38",
180
+ "-deadline",
181
+ "realtime",
182
+ "-cpu-used",
183
+ "6",
184
+ "-row-mt",
185
+ "1",
186
+ "-pix_fmt",
187
+ "yuv420p",
188
+ // Without these, the WebM muxer writes a random segment id, and the same cut gives different bytes on every run.
189
+ "-fflags",
190
+ "+bitexact",
191
+ "-flags:v",
192
+ "+bitexact",
193
+ "-flags:a",
194
+ "+bitexact",
195
+ "-t",
196
+ duration.toFixed(3),
197
+ plan.output
198
+ ];
199
+ }
200
+ function durationFromProbe(formatDuration, lastPacketLines) {
201
+ const declared = Number(formatDuration);
202
+ if (Number.isFinite(declared) && declared > 0) return declared;
203
+ const packets = lastPacketLines.split("\n").filter((line) => line.trim() !== "").map((line) => line.split(",").map(Number)).filter((fields) => Number.isFinite(fields[0]));
204
+ const last = packets.at(-1);
205
+ if (last === void 0) return void 0;
206
+ return last[0] + (Number.isFinite(last[1]) ? last[1] : 0);
207
+ }
208
+ var TOOL_TIMEOUT_MS = 10 * 60 * 1e3;
209
+ function createMediaTools(env, exec = execFileSync) {
210
+ const ffmpeg = findProgram("ffmpeg", env) ?? "ffmpeg";
211
+ const ffprobe = findProgram("ffprobe", env) ?? "ffprobe";
212
+ const run = (program, args) => exec(program, args, {
213
+ encoding: "utf8",
214
+ env,
215
+ maxBuffer: 64 * 1024 * 1024,
216
+ stdio: ["ignore", "pipe", "pipe"],
217
+ timeout: TOOL_TIMEOUT_MS
218
+ });
219
+ return {
220
+ probe(video) {
221
+ const format = run(ffprobe, ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", video]);
222
+ const declared = durationFromProbe(format, "");
223
+ if (declared !== void 0) return declared;
224
+ const lines = run(ffprobe, ["-v", "error", "-select_streams", "v:0", "-show_entries", "packet=pts_time,duration_time", "-of", "csv=p=0", video]);
225
+ const duration = durationFromProbe("", lines);
226
+ if (duration === void 0) throw new Error(`cannot read the length of ${video}`);
227
+ return duration;
228
+ },
229
+ renderClip(plan) {
230
+ run(ffmpeg, clipArgs(plan));
231
+ }
232
+ };
233
+ }
234
+
235
+ // src/demo/record.ts
236
+ function chapterPassed(checks) {
237
+ return checks.length > 0 && checks.every((check) => check.passed);
238
+ }
239
+ function buildDemoRecord(manifest, clips) {
240
+ if (clips.length !== manifest.chapters.length) {
241
+ throw new Error(`the demo has ${manifest.chapters.length} chapters but ${clips.length} clips`);
242
+ }
243
+ return {
244
+ schemaVersion: 1,
245
+ chapters: manifest.chapters.map((chapter, index) => ({
246
+ criterionId: chapter.criterionId,
247
+ start: chapter.start,
248
+ end: chapter.end,
249
+ title: chapter.title,
250
+ narration: chapter.narration,
251
+ checks: chapter.checks,
252
+ clipArtifactId: clips[index].clipArtifactId,
253
+ bound: clips[index].bound
254
+ }))
255
+ };
256
+ }
257
+ var VoiceDownloadError = class extends Error {
258
+ constructor(message, retryable = false) {
259
+ super(message);
260
+ this.retryable = retryable;
261
+ }
262
+ };
263
+ async function sha256OfFile(path) {
264
+ const hash = createHash("sha256");
265
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
266
+ return hash.digest("hex");
267
+ }
268
+ function withoutCredentials(url) {
269
+ try {
270
+ const parsed = new URL(url);
271
+ parsed.username = "";
272
+ parsed.password = "";
273
+ return parsed.toString();
274
+ } catch {
275
+ return "(an invalid URL)";
276
+ }
277
+ }
278
+ var ATTEMPTS = 3;
279
+ async function requestRest(params) {
280
+ const { url, part, name, fetchImpl, progress } = params;
281
+ const offset = existsSync(part) ? statSync(part).size : 0;
282
+ const response = await fetchImpl(url, offset > 0 ? { headers: { range: `bytes=${offset}-` } } : {});
283
+ if (response.status === 416 && offset > 0) {
284
+ await response.body?.cancel();
285
+ return;
286
+ }
287
+ if (!response.ok || response.body === null) {
288
+ await response.body?.cancel();
289
+ throw new VoiceDownloadError(`cannot download ${name} from ${withoutCredentials(url)}: the server answered ${response.status}`, response.status >= 500);
290
+ }
291
+ const resumed = response.status === 206;
292
+ const start = resumed ? offset : 0;
293
+ const total = Number(response.headers.get("content-length") ?? 0) + start;
294
+ const out = createWriteStream(part, { flags: resumed ? "a" : "w" });
295
+ let written = start;
296
+ let lastReport = 0;
297
+ try {
298
+ for await (const chunk of response.body) {
299
+ if (!out.write(chunk)) await once(out, "drain");
300
+ written += chunk.length;
301
+ const now = Date.now();
302
+ if (progress !== void 0 && now - lastReport >= 1e3) {
303
+ lastReport = now;
304
+ progress(` downloading ${name}: ${(written / 1048576).toFixed(0)}${total > start ? ` of ${(total / 1048576).toFixed(0)}` : ""} MiB`);
305
+ }
306
+ }
307
+ } finally {
308
+ out.end();
309
+ await once(out, "close");
310
+ }
311
+ }
312
+ async function downloadPinned(params) {
313
+ const { url, dest, sha256, name } = params;
314
+ mkdirSync(dirname(dest), { recursive: true });
315
+ const part = `${dest}.part`;
316
+ for (let attempt = 1; ; attempt += 1) {
317
+ try {
318
+ await requestRest({ ...params, part });
319
+ break;
320
+ } catch (err) {
321
+ const refused = err instanceof VoiceDownloadError && !err.retryable;
322
+ if (refused || attempt === ATTEMPTS) {
323
+ throw err instanceof VoiceDownloadError ? err : new VoiceDownloadError(`cannot download ${name} from ${withoutCredentials(url)}: ${String(err)}`);
324
+ }
325
+ await new Promise((resolve2) => setTimeout(resolve2, params.retryDelayMs ?? 500));
326
+ }
327
+ }
328
+ const digest = await sha256OfFile(part);
329
+ if (digest !== sha256) {
330
+ rmSync(part, { force: true });
331
+ throw new VoiceDownloadError(
332
+ `the downloaded ${name} has sha256 ${digest}, not the pinned ${sha256} \u2014 it was discarded and nothing was spoken`
333
+ );
334
+ }
335
+ renameSync(part, dest);
336
+ }
337
+ var PUNCTUATION = ';:,.!?\xA1\xBF\u2014\u2026"\xAB\xBB\u201C\u201D(){}[]';
338
+ var PUNCTUATION_RUN = new RegExp(`(\\s*[${PUNCTUATION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}]+\\s*)+`, "g");
339
+ function splitYear(text) {
340
+ if (text.includes(".")) return text;
341
+ if (text.includes(":")) {
342
+ const [hours, minutes] = text.split(":").map(Number);
343
+ return minutes === 0 ? `${hours} o'clock` : minutes < 10 ? `${hours} oh ${minutes}` : `${hours} ${minutes}`;
344
+ }
345
+ const year = parseInt(text.slice(0, 4), 10);
346
+ if (year < 1100 || year % 1e3 < 10) return text;
347
+ const high = text.slice(0, 2);
348
+ const low = parseInt(text.slice(2, 4), 10);
349
+ const plural = text.endsWith("s") ? "s" : "";
350
+ if (low === 0) return `${high} hundred${plural}`;
351
+ if (low < 10) return `${high} oh ${low}${plural}`;
352
+ return `${high} ${low}${plural}`;
353
+ }
354
+ function spellMoney(text) {
355
+ const unit = text[0] === "$" ? "dollar" : "pound";
356
+ if (Number.isNaN(Number(text.slice(1)))) return `${text.slice(1)} ${unit}s`;
357
+ if (!text.includes(".")) return `${text.slice(1)} ${unit}${text.slice(1) === "1" ? "" : "s"}`;
358
+ const [whole, fraction] = text.slice(1).split(".");
359
+ const cents = parseInt(fraction.padEnd(2, "0"), 10);
360
+ const small = text[0] === "$" ? cents === 1 ? "cent" : "cents" : cents === 1 ? "penny" : "pence";
361
+ return `${whole} ${unit}${whole === "1" ? "" : "s"} and ${cents} ${small}`;
362
+ }
363
+ function spellDecimal(text) {
364
+ const [whole, fraction] = text.split(".");
365
+ return `${whole} point ${fraction.split("").join(" ")}`;
366
+ }
367
+ function prepareText(text) {
368
+ return text.replace(/[‘’]/g, "'").replace(/«/g, "\u201C").replace(/»/g, "\u201D").replace(/[“”]/g, '"').replace(/\(/g, "\xAB").replace(/\)/g, "\xBB").replace(/、/g, ", ").replace(/。/g, ". ").replace(/!/g, "! ").replace(/,/g, ", ").replace(/:/g, ": ").replace(/;/g, "; ").replace(/?/g, "? ").replace(/[^\S \n]/g, " ").replace(/ {2,}/g, " ").replace(/(?<=\n) +(?=\n)/g, "").replace(/\bD[Rr]\.(?= [A-Z])/g, "Doctor").replace(/\b(?:Mr\.|MR\.(?= [A-Z]))/g, "Mister").replace(/\b(?:Ms\.|MS\.(?= [A-Z]))/g, "Miss").replace(/\b(?:Mrs\.|MRS\.(?= [A-Z]))/g, "Mrs").replace(/\betc\.(?! [A-Z])/gi, "etc").replace(/\b(y)eah?\b/gi, "$1e'a").replace(/\d*\.\d+|\b\d{4}s?\b|(?<!:)\b(?:[1-9]|1[0-2]):[0-5]\d\b(?!:)/g, splitYear).replace(/(?<=\d),(?=\d)/g, "").replace(/[$£]\d+(?:\.\d+)?(?: hundred| thousand| (?:[bm]|tr)illion)*\b|[$£]\d+\.\d\d?\b/gi, spellMoney).replace(/\d*\.\d+/g, spellDecimal).replace(/(?<=\d)-(?=\d)/g, " to ").replace(/(?<=\d)S/g, " S").replace(/(?<=[BCDFGHJ-NP-TV-Z])'?s\b/g, "'S").replace(/(?<=X')S\b/g, "s").replace(/(?:[A-Za-z]\.){2,} [a-z]/g, (match) => match.replace(/\./g, "-")).replace(/(?<=[A-Z])\.(?=[A-Z])/gi, "-").trim();
369
+ }
370
+ function splitOnPunctuation(text) {
371
+ const parts = [];
372
+ let at = 0;
373
+ for (const match of text.matchAll(PUNCTUATION_RUN)) {
374
+ const index = match.index ?? 0;
375
+ if (at < index) parts.push({ punctuation: false, text: text.slice(at, index) });
376
+ parts.push({ punctuation: true, text: match[0] });
377
+ at = index + match[0].length;
378
+ }
379
+ if (at < text.length) parts.push({ punctuation: false, text: text.slice(at) });
380
+ return parts;
381
+ }
382
+ function normalizePhonemes(ipa) {
383
+ return ipa.replace(/kəkˈoːɹoʊ/g, "k\u02C8o\u028Ak\u0259\u0279o\u028A").replace(/kəkˈɔːɹəʊ/g, "k\u02C8\u0259\u028Ak\u0259\u0279\u0259\u028A").replace(/ʲ/g, "j").replace(/r/g, "\u0279").replace(/x/g, "k").replace(/ɬ/g, "l").replace(/(?<=[a-zɹː])(?=hˈʌndɹɪd)/g, " ").replace(/ z(?=[;:,.!?¡¿—…"«»“” ]|$)/g, "z").replace(/(?<=nˈaɪn)ti(?!ː)/g, "di").trim();
384
+ }
385
+ var ESPEAK_TIMEOUT_MS = 60 * 1e3;
386
+ function espeakRunner(espeakPath, run = execFileSync) {
387
+ return (words) => run(espeakPath, ["-q", "--ipa", "-v", "en-us"], { input: words, encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: ESPEAK_TIMEOUT_MS }).split("\n").map((line) => line.trim()).filter((line) => line !== "").join(" ");
388
+ }
389
+ function phonemize(text, ipa) {
390
+ const parts = splitOnPunctuation(prepareText(text));
391
+ return normalizePhonemes(parts.map((part) => part.punctuation ? part.text : ipa(part.text)).join(""));
392
+ }
393
+
394
+ // src/demo/voice/kokoro.ts
395
+ var SAMPLE_RATE = 24e3;
396
+ var VOICE_ROWS = 510;
397
+ var STYLE_SIZE = 256;
398
+ var SENTENCE_GAP = 0.18;
399
+ function tokenize(phonemes, vocab) {
400
+ const ids = [];
401
+ for (const symbol of phonemes) {
402
+ const id = vocab[symbol];
403
+ if (id !== void 0) ids.push(id);
404
+ }
405
+ return [0, ...ids, 0];
406
+ }
407
+ function styleRow(voice, tokenCount) {
408
+ const row = Math.min(Math.max(tokenCount - 2, 0), VOICE_ROWS - 1);
409
+ return voice.slice(row * STYLE_SIZE, (row + 1) * STYLE_SIZE);
410
+ }
411
+ function splitSentences(text) {
412
+ return text.split(/(?<=[.!?])\s+/).map((sentence) => sentence.trim()).filter((sentence) => sentence !== "");
413
+ }
414
+ function encodeWav(samples, sampleRate = SAMPLE_RATE) {
415
+ const data = Buffer.alloc(samples.length * 2);
416
+ samples.forEach((sample, index) => data.writeInt16LE(Math.round(Math.max(-1, Math.min(1, sample)) * 32767), index * 2));
417
+ const header = Buffer.alloc(44);
418
+ header.write("RIFF", 0);
419
+ header.writeUInt32LE(36 + data.length, 4);
420
+ header.write("WAVEfmt ", 8);
421
+ header.writeUInt32LE(16, 16);
422
+ header.writeUInt16LE(1, 20);
423
+ header.writeUInt16LE(1, 22);
424
+ header.writeUInt32LE(sampleRate, 24);
425
+ header.writeUInt32LE(sampleRate * 2, 28);
426
+ header.writeUInt16LE(2, 32);
427
+ header.writeUInt16LE(16, 34);
428
+ header.write("data", 36);
429
+ header.writeUInt32LE(data.length, 40);
430
+ return Buffer.concat([header, data]);
431
+ }
432
+ function createKokoroSpeaker(opts) {
433
+ const ipa = opts.ipa ?? espeakRunner(opts.espeakPath);
434
+ let loaded;
435
+ let counter = 0;
436
+ const load = async () => {
437
+ if (loaded !== void 0) return loaded;
438
+ const ort = createRequire$1(join(opts.runtimeDir, "package.json"))("onnxruntime-node");
439
+ const session = await ort.InferenceSession.create(join(opts.packDir, "model.onnx"));
440
+ const vocab = JSON.parse(readFileSync(join(opts.packDir, "tokenizer.json"), "utf8")).model.vocab;
441
+ const bytes = readFileSync(join(opts.packDir, "af_heart.bin"));
442
+ const voice = new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4);
443
+ loaded = { ort, session, vocab, voice };
444
+ return loaded;
445
+ };
446
+ return async (text) => {
447
+ const { ort, session, vocab, voice } = await load();
448
+ const pieces = [];
449
+ for (const sentence of splitSentences(text)) {
450
+ const ids = tokenize(phonemize(sentence, ipa), vocab).slice(0, VOICE_ROWS + 1);
451
+ if (ids.length <= 2) continue;
452
+ const out = await session.run({
453
+ input_ids: new ort.Tensor("int64", BigInt64Array.from(ids.map(BigInt)), [1, ids.length]),
454
+ style: new ort.Tensor("float32", styleRow(voice, ids.length), [1, STYLE_SIZE]),
455
+ speed: new ort.Tensor("float32", Float32Array.of(1), [1])
456
+ });
457
+ if (pieces.length > 0) pieces.push(new Float32Array(Math.round(SENTENCE_GAP * SAMPLE_RATE)));
458
+ pieces.push(out.waveform.data);
459
+ }
460
+ const length = pieces.reduce((sum, piece) => sum + piece.length, 0);
461
+ if (length === 0) throw new Error(`the narration line "${text}" has nothing to say`);
462
+ const samples = new Float32Array(length);
463
+ let at = 0;
464
+ for (const piece of pieces) {
465
+ samples.set(piece, at);
466
+ at += piece.length;
467
+ }
468
+ counter += 1;
469
+ const wavPath = join(opts.workDir, `speech-${counter}.wav`);
470
+ writeFileSync(wavPath, encodeWav(samples));
471
+ return { wavPath, seconds: samples.length / SAMPLE_RATE };
472
+ };
473
+ }
474
+ var BLOCK = 512;
475
+ function field(block, start, length) {
476
+ const raw = block.subarray(start, start + length);
477
+ const end = raw.indexOf(0);
478
+ return raw.subarray(0, end === -1 ? raw.length : end).toString("utf8");
479
+ }
480
+ function octal(block, start, length) {
481
+ return parseInt(field(block, start, length), 8) || 0;
482
+ }
483
+ function paxPath(data) {
484
+ let at = 0;
485
+ while (at < data.length) {
486
+ const space = data.indexOf(32, at);
487
+ if (space === -1) return void 0;
488
+ const length = Number(data.subarray(at, space).toString("utf8"));
489
+ if (!Number.isFinite(length) || length <= 0) return void 0;
490
+ const record = data.subarray(space + 1, at + length - 1).toString("utf8");
491
+ if (record.startsWith("path=")) return record.slice("path=".length);
492
+ at += length;
493
+ }
494
+ return void 0;
495
+ }
496
+ async function extractNpmTarball(tarball, destDir, keep = () => true) {
497
+ let pending = Buffer.alloc(0);
498
+ let header;
499
+ let longPath;
500
+ let remaining = 0;
501
+ let padding = 0;
502
+ let collected = [];
503
+ let sink;
504
+ const finished = [];
505
+ const closeSink = () => {
506
+ const done = sink;
507
+ sink = void 0;
508
+ if (done !== void 0) {
509
+ finished.push(new Promise((resolve2, reject) => done.on("close", resolve2).on("error", reject)));
510
+ done.end();
511
+ }
512
+ };
513
+ const startEntry = (entry) => {
514
+ header = entry;
515
+ remaining = entry.size;
516
+ padding = (BLOCK - entry.size % BLOCK) % BLOCK;
517
+ collected = [];
518
+ if (entry.type !== "0" && entry.type !== "\0") return;
519
+ const relative = entry.path.split("/").slice(1).join("/");
520
+ if (relative === "" || !keep(relative)) return;
521
+ const target = normalize(join(destDir, relative));
522
+ if (!target.startsWith(normalize(destDir) + sep)) throw new Error(`the archive has a path outside its folder: ${entry.path}`);
523
+ mkdirSync(dirname(target), { recursive: true });
524
+ sink = createWriteStream(target, { mode: 493 });
525
+ };
526
+ const consume = (chunk) => {
527
+ pending = Buffer.concat([pending, chunk]);
528
+ for (; ; ) {
529
+ if (header === void 0) {
530
+ if (pending.length < BLOCK) return;
531
+ const block = pending.subarray(0, BLOCK);
532
+ pending = pending.subarray(BLOCK);
533
+ const name = field(block, 0, 100);
534
+ const prefix = field(block, 345, 155);
535
+ const type = String.fromCharCode(block[156] ?? 48);
536
+ const path = longPath ?? (prefix === "" ? name : `${prefix}/${name}`);
537
+ longPath = void 0;
538
+ startEntry({ path, size: octal(block, 124, 12), type });
539
+ continue;
540
+ }
541
+ if (remaining > 0) {
542
+ if (pending.length === 0) return;
543
+ const take = Math.min(remaining, pending.length);
544
+ const part = pending.subarray(0, take);
545
+ pending = pending.subarray(take);
546
+ remaining -= take;
547
+ if (sink !== void 0) sink.write(part);
548
+ else if (header.type === "x") collected.push(Buffer.from(part));
549
+ continue;
550
+ }
551
+ if (pending.length < padding) return;
552
+ pending = pending.subarray(padding);
553
+ if (header.type === "x") longPath = paxPath(Buffer.concat(collected));
554
+ closeSink();
555
+ header = void 0;
556
+ }
557
+ };
558
+ await new Promise((resolve2, reject) => {
559
+ const input = createReadStream(tarball).on("error", reject);
560
+ const gunzip = createGunzip();
561
+ input.pipe(gunzip);
562
+ gunzip.on("data", (chunk) => {
563
+ try {
564
+ consume(chunk);
565
+ } catch (err) {
566
+ input.destroy();
567
+ reject(err);
568
+ return;
569
+ }
570
+ const writing = sink;
571
+ if (writing !== void 0 && writing.writableNeedDrain) {
572
+ gunzip.pause();
573
+ writing.once("drain", () => gunzip.resume());
574
+ }
575
+ }).on("end", resolve2).on("error", reject);
576
+ });
577
+ closeSink();
578
+ await Promise.all(finished);
579
+ }
580
+
581
+ // src/demo/voice/pack.ts
582
+ var VoicePackError = class extends Error {
583
+ };
584
+ var DEFAULT_VOICE_PACK_PINS = { version: VOICE_PACK_VERSION, files: VOICE_PACK_FILES };
585
+ var IMAGE_VOICE_ROOT = "/opt/outerlayer/voice";
586
+ function voicePackHomeDir(home, version) {
587
+ return join(home, ".outerlayer", "voice", version);
588
+ }
589
+ function mismatch(file, actual, dir) {
590
+ return new VoicePackError(
591
+ `the voice pack file ${file.name} does not match its pinned sha256 (expected ${file.sha256}, found ${actual}). Delete ${join(dir, file.name)} to download it again. Nothing was spoken.`
592
+ );
593
+ }
594
+ async function verifyPresent(dir, pins) {
595
+ const missing = [];
596
+ for (const file of pins.files) {
597
+ const path = join(dir, file.name);
598
+ if (!existsSync(path)) {
599
+ missing.push(file.name);
600
+ continue;
601
+ }
602
+ const actual = await sha256OfFile(path);
603
+ if (actual !== file.sha256) throw mismatch(file, actual, dir);
604
+ }
605
+ return missing;
606
+ }
607
+ async function ensureVoicePack(opts) {
608
+ const pins = opts.pins ?? DEFAULT_VOICE_PACK_PINS;
609
+ const env = opts.env ?? process.env;
610
+ const log = opts.log ?? (() => void 0);
611
+ const installed = join(opts.installedRoot ?? IMAGE_VOICE_ROOT, pins.version);
612
+ if (existsSync(installed)) {
613
+ const missing2 = await verifyPresent(installed, pins);
614
+ if (missing2.length === 0) return { dir: installed, version: pins.version };
615
+ }
616
+ const dir = voicePackHomeDir(opts.home, pins.version);
617
+ const missing = await verifyPresent(dir, pins);
618
+ if (missing.length === 0) return { dir, version: pins.version };
619
+ const mirror = env.OUTERLAYER_VOICE_MIRROR?.replace(/\/+$/, "");
620
+ log(`Downloading the narration voice pack to ${dir} (first narrated run only; later runs use the copy there)`);
621
+ mkdirSync(dir, { recursive: true });
622
+ for (const file of pins.files.filter((entry) => missing.includes(entry.name))) {
623
+ await downloadPinned({
624
+ url: mirror !== void 0 ? `${mirror}/${encodeURIComponent(file.name)}` : file.url,
625
+ dest: join(dir, file.name),
626
+ sha256: file.sha256,
627
+ name: file.name,
628
+ fetchImpl: opts.fetchImpl ?? fetch,
629
+ progress: log
630
+ });
631
+ }
632
+ return { dir, version: pins.version };
633
+ }
634
+ var RUNTIME_PACKAGES = [
635
+ { prefix: "onnxruntime-node-", folder: "onnxruntime-node" },
636
+ { prefix: "onnxruntime-common-", folder: "onnxruntime-common" }
637
+ ];
638
+ async function ensureVoiceRuntime(pack, pins, home) {
639
+ const dir = join(voicePackHomeDir(home, pack.version), "runtime", `${process.platform}-${process.arch}`);
640
+ const archives = RUNTIME_PACKAGES.map(({ prefix, folder }) => {
641
+ const file = pins.files.find((entry) => entry.name.startsWith(prefix) && entry.name.endsWith(".tgz"));
642
+ if (file === void 0) throw new VoicePackError(`the voice pack pins no ${folder} archive`);
643
+ return { file, folder };
644
+ });
645
+ const marker = join(dir, ".complete");
646
+ const wanted = JSON.stringify(archives.map(({ file }) => [file.name, file.sha256]));
647
+ if (existsSync(marker) && readFileSync(marker, "utf8") === wanted) return { dir };
648
+ rmSync(dir, { recursive: true, force: true });
649
+ mkdirSync(dir, { recursive: true });
650
+ const nativeDir = `bin/napi-v3/${process.platform}/${process.arch}/`;
651
+ for (const { file, folder } of archives) {
652
+ await extractNpmTarball(join(pack.dir, file.name), join(dir, "node_modules", folder), (path) => {
653
+ if (path.startsWith("bin/")) return path.startsWith(nativeDir);
654
+ return path === "package.json" || path === "LICENSE" || path.startsWith("dist/");
655
+ });
656
+ }
657
+ if (!existsSync(join(dir, "node_modules", "onnxruntime-node", nativeDir))) {
658
+ rmSync(dir, { recursive: true, force: true });
659
+ throw new VoicePackError(`the ONNX Runtime has no build for ${process.platform} ${process.arch}, so narration cannot run here; pass --no-narration`);
660
+ }
661
+ writeFileSync(join(dir, "package.json"), '{"private":true}\n');
662
+ writeFileSync(marker, wanted);
663
+ return { dir };
664
+ }
665
+
666
+ // src/emit-demo-cmd.ts
667
+ var EmitDemoError = class extends Error {
668
+ };
669
+ var MIB = 1024 * 1024;
670
+ function parseManifest(path) {
671
+ let text;
672
+ try {
673
+ text = readFileSync(path, "utf8");
674
+ } catch (err) {
675
+ throw new EmitDemoError(`cannot read the manifest "${path}": ${String(err)}`);
676
+ }
677
+ let raw;
678
+ try {
679
+ raw = JSON.parse(text);
680
+ } catch (err) {
681
+ throw new EmitDemoError(`the manifest "${path}" is not valid JSON: ${String(err)}`);
682
+ }
683
+ const parsed = DemoManifestSchema.safeParse(raw);
684
+ if (!parsed.success) {
685
+ const issue = parsed.error.issues[0];
686
+ const where = issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
687
+ throw new EmitDemoError(`the manifest "${path}" is not valid${where}: ${issue.message}`);
688
+ }
689
+ const videoPath = resolve(dirname(path), parsed.data.video);
690
+ try {
691
+ if (!statSync(videoPath).isFile()) throw new Error("not a file");
692
+ } catch {
693
+ throw new EmitDemoError(`the manifest names the video "${parsed.data.video}", but ${videoPath} does not exist`);
694
+ }
695
+ return { manifest: parsed.data, videoPath, text };
696
+ }
697
+ async function runEmitDemo(opts) {
698
+ const env = opts.env ?? process.env;
699
+ const cwd = opts.cwd ?? process.cwd();
700
+ const home = opts.home ?? homeFromEnv(env);
701
+ const say = (line) => {
702
+ if (!opts.quiet) process.stderr.write(`${line}
703
+ `);
704
+ };
705
+ const { manifest, videoPath, text: manifestText } = parseManifest(resolve(cwd, opts.manifest));
706
+ const narrated = opts.noNarration !== true && manifest.chapters.some((chapter) => chapter.narration.trim() !== "");
707
+ for (const program of narrated ? ["ffmpeg", "ffprobe", "espeak-ng"] : ["ffmpeg", "ffprobe"]) {
708
+ if (findProgram(program, env) === void 0) throw new EmitDemoError(missingProgramMessage(program, narrated));
709
+ }
710
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
711
+ const media = opts.media ?? createMediaTools(env);
712
+ const recordedIds = await recordedCriteria({ opts, env, cwd, home, fetchImpl, say });
713
+ let videoSeconds;
714
+ try {
715
+ videoSeconds = media.probe(videoPath);
716
+ } catch (err) {
717
+ throw new EmitDemoError(`cannot read the video ${videoPath}: ${String(err)}`);
718
+ }
719
+ const problem = checkManifest({ manifest, videoSeconds, recordedIds });
720
+ if (problem !== void 0) throw new EmitDemoError(problem);
721
+ const work = mkdtempSync(join(tmpdir(), "outerlayer-demo-"));
722
+ try {
723
+ const speaker = narrated ? opts.speaker ?? await realSpeaker({ env, home, work, say }) : void 0;
724
+ const plans = [];
725
+ for (const [index, chapter] of manifest.chapters.entries()) {
726
+ const plan = {
727
+ video: videoPath,
728
+ start: chapter.start,
729
+ end: chapter.end,
730
+ output: join(work, `chapter-${String(index + 1).padStart(2, "0")}.webm`)
731
+ };
732
+ if (speaker !== void 0 && chapter.narration.trim() !== "") {
733
+ let speech;
734
+ try {
735
+ speech = await speaker(chapter.narration);
736
+ } catch (err) {
737
+ throw new EmitDemoError(`${chapterLabel(index, chapter.title)}: the narration could not be spoken: ${err instanceof Error ? err.message : String(err)}`);
738
+ }
739
+ const fit = fitSpeech(speech.seconds, chapter.end - chapter.start);
740
+ if (!fit.ok) {
741
+ throw new EmitDemoError(
742
+ `${chapterLabel(index, chapter.title)}: the narration runs ${fit.overrunSeconds.toFixed(1)} s too long even at 1.3 times speed \u2014 shorten the line or lengthen the chapter`
743
+ );
744
+ }
745
+ plan.audio = { wavPath: speech.wavPath, tempo: fit.tempo, delaySeconds: SPEECH_LEAD_IN };
746
+ }
747
+ plans.push(plan);
748
+ }
749
+ for (const [index, plan] of plans.entries()) {
750
+ const chapter = manifest.chapters[index];
751
+ try {
752
+ media.renderClip(plan);
753
+ } catch (err) {
754
+ throw new EmitDemoError(`${chapterLabel(index, chapter.title)}: ffmpeg could not cut the clip: ${String(err)}`);
755
+ }
756
+ const bytes = statSync(plan.output).size;
757
+ if (bytes > ARTIFACT_MAX_BYTES) {
758
+ throw new EmitDemoError(
759
+ `${chapterLabel(index, chapter.title)}: the clip is ${(bytes / MIB).toFixed(1)} MiB, over the ${ARTIFACT_MAX_BYTES / MIB} MiB limit \u2014 shorten the chapter or record a smaller window`
760
+ );
761
+ }
762
+ }
763
+ const seed = createHash("sha256").update(manifestText).update("\0").update(readFileSync(videoPath)).update(`\0${opts.item ?? ""}\0${opts.pr ?? ""}`).digest("hex");
764
+ const idFor = (label, file) => `demo-${createHash("sha256").update(seed).update("\0").update(label).update("\0").update(readFileSync(file)).digest("hex").slice(0, 40)}`;
765
+ const clipIds = plans.map((plan, index) => idFor(`clip-${index + 1}`, plan.output));
766
+ const plannedRecord = JSON.stringify(
767
+ buildDemoRecord(
768
+ manifest,
769
+ manifest.chapters.map((chapter, index) => ({ clipArtifactId: clipIds[index], bound: chapter.criterionId !== null && chapterPassed(chapter.checks) }))
770
+ )
771
+ );
772
+ const recordBytes = Buffer.byteLength(plannedRecord);
773
+ if (recordBytes > ARTIFACT_MAX_BYTES) {
774
+ throw new EmitDemoError(
775
+ `the demo record is ${(recordBytes / MIB).toFixed(1)} MiB, over the ${ARTIFACT_MAX_BYTES / MIB} MiB limit \u2014 shorten the checks, titles and narration`
776
+ );
777
+ }
778
+ const emit = opts.emitArtifact ?? runEmitArtifact;
779
+ const base = {
780
+ pr: opts.pr,
781
+ url: opts.url,
782
+ apiKey: opts.apiKey,
783
+ appId: opts.appId,
784
+ quiet: true,
785
+ json: true,
786
+ cwd,
787
+ home,
788
+ env,
789
+ fetchImpl: opts.fetchImpl
790
+ };
791
+ const clips = [];
792
+ const failures = [];
793
+ for (const [index, chapter] of manifest.chapters.entries()) {
794
+ const bound = chapter.criterionId !== null && chapterPassed(chapter.checks);
795
+ const failed = chapter.checks.filter((check) => !check.passed);
796
+ if (failed.length > 0) {
797
+ failures.push(`${chapterLabel(index, chapter.title)} failed: ${failed.map((check) => `"${check.text}"`).join(", ")}`);
798
+ } else if (chapter.criterionId !== null && chapter.checks.length === 0) {
799
+ say(`${C.yellow}!${C.reset} ${chapterLabel(index, chapter.title)} ran no checks, so its clip is not bound to ${chapter.criterionId}`);
800
+ }
801
+ const caption = `Demo chapter ${index + 1}: ${chapter.title}${failed.length > 0 ? ` (${failed.length} failed check${failed.length === 1 ? "" : "s"})` : ""}`;
802
+ const result = await emitOrExplain(emit, {
803
+ ...base,
804
+ file: plans[index].output,
805
+ artifactId: clipIds[index],
806
+ caption: caption.slice(0, ARTIFACT_MAX_CAPTION_LENGTH),
807
+ ...bound ? { criterionId: chapter.criterionId } : {}
808
+ }, clips.length, manifest.chapters.length);
809
+ clips.push({ clipArtifactId: artifactIdOf(result), bound, title: chapter.title });
810
+ }
811
+ const record = buildDemoRecord(manifest, clips);
812
+ const recordPath = join(work, "demo.json");
813
+ writeFileSync(recordPath, JSON.stringify(record));
814
+ const recorded = await emitOrExplain(emit, {
815
+ ...base,
816
+ file: recordPath,
817
+ artifactId: idFor("record", recordPath),
818
+ caption: `Demo record: ${clips.length} chapter${clips.length === 1 ? "" : "s"}, ${clips.filter((clip) => clip.bound).length} bound to a criterion`,
819
+ mediaType: DEMO_RECORD_MEDIA_TYPE
820
+ }, clips.length, manifest.chapters.length);
821
+ const recordArtifactId = artifactIdOf(recorded);
822
+ const exitCode = failures.length > 0 ? 1 : 0;
823
+ const output = opts.json ? JSON.stringify({ recordArtifactId, clips, failures, exitCode }) : `${C.green}\u2713${C.reset} demo emitted ${C.dim}\xB7${C.reset} ${clips.length} clip${clips.length === 1 ? "" : "s"}, ${clips.filter((clip) => clip.bound).length} bound to a criterion ${C.dim}\xB7${C.reset} record ${recordArtifactId}`;
824
+ if (!opts.quiet) process.stdout.write(output + "\n");
825
+ return { exitCode, output, failures, recordArtifactId, clips };
826
+ } finally {
827
+ rmSync(work, { recursive: true, force: true });
828
+ }
829
+ }
830
+ function artifactIdOf(result) {
831
+ const id = result.record?.artifactId ?? result.data?.clientArtifactId;
832
+ if (typeof id !== "string") throw new EmitDemoError("the artifact was emitted but its id was not returned");
833
+ return id;
834
+ }
835
+ async function emitOrExplain(emit, opts, emitted, total) {
836
+ try {
837
+ return await emit(opts);
838
+ } catch (err) {
839
+ const message = err instanceof Error ? err.message : String(err);
840
+ throw new EmitDemoError(
841
+ emitted > 0 ? `${message} \u2014 ${emitted} of ${total} clips were already emitted; run the command again to emit the whole demo` : message
842
+ );
843
+ }
844
+ }
845
+ async function recordedCriteria(params) {
846
+ const { opts, env, cwd, home, fetchImpl, say } = params;
847
+ let item = opts.item;
848
+ if (item === void 0) {
849
+ const identity = resolveRepoIdentity(cwd);
850
+ const sessionId = detectActiveSession({
851
+ env,
852
+ home,
853
+ cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, cwd] : [cwd],
854
+ nowMs: Date.now()
855
+ });
856
+ item = sessionId === void 0 ? void 0 : launchedItemForSession(sessionId, home);
857
+ }
858
+ if (item === void 0) {
859
+ say(`${C.yellow}!${C.reset} no work item, so the chapters' criterion ids are not checked against the recorded criteria (pass --item <number>)`);
860
+ return void 0;
861
+ }
862
+ try {
863
+ const refusal = await autoConnect({ url: opts.url, apiKey: opts.apiKey, appId: opts.appId, cwd, home, env, fetchImpl: opts.fetchImpl });
864
+ if (refusal !== void 0) throw new Error(refusal);
865
+ const resolved = resolveGatewayCredentials({ url: opts.url, apiKey: opts.apiKey, appId: opts.appId, cwd, home, env });
866
+ if (!resolved.ok) throw new Error(resolved.message);
867
+ const ids = await readRecordedCriteriaIds({ url: resolved.url, apiKey: resolved.credential, appId: resolved.appId, item, fetchImpl });
868
+ if (ids === void 0) say(`${C.yellow}!${C.reset} item ${item} has no recorded criteria, so the chapters' criterion ids are not checked`);
869
+ return ids;
870
+ } catch (err) {
871
+ say(`${C.yellow}!${C.reset} could not read the recorded criteria of item ${item}, so the chapters' criterion ids are not checked: ${err instanceof Error ? err.message : String(err)}`);
872
+ return void 0;
873
+ }
874
+ }
875
+ async function realSpeaker(params) {
876
+ const { env, home, work, say } = params;
877
+ try {
878
+ const pack = await ensureVoicePack({ home, env, log: say });
879
+ const runtime = await ensureVoiceRuntime(pack, DEFAULT_VOICE_PACK_PINS, home);
880
+ mkdirSync(work, { recursive: true });
881
+ return createKokoroSpeaker({ packDir: pack.dir, runtimeDir: runtime.dir, espeakPath: findProgram("espeak-ng", env) ?? "espeak-ng", workDir: work });
882
+ } catch (err) {
883
+ if (err instanceof VoicePackError || err instanceof VoiceDownloadError) throw new EmitDemoError(err.message);
884
+ throw err;
885
+ }
886
+ }
887
+
888
+ export { EmitDemoError, runEmitDemo };