@nhtio/adk 1.20260609.0 → 1.20260610.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 (134) hide show
  1. package/CHANGELOG.md +132 -9
  2. package/batteries/llm/ollama/helpers.cjs +9 -0
  3. package/batteries/llm/ollama/helpers.cjs.map +1 -1
  4. package/batteries/llm/ollama/helpers.mjs +9 -0
  5. package/batteries/llm/ollama/helpers.mjs.map +1 -1
  6. package/batteries/llm/openai_chat_completions/helpers.cjs +19 -0
  7. package/batteries/llm/openai_chat_completions/helpers.cjs.map +1 -1
  8. package/batteries/llm/openai_chat_completions/helpers.mjs +19 -0
  9. package/batteries/llm/openai_chat_completions/helpers.mjs.map +1 -1
  10. package/batteries/media/builder.d.ts +245 -0
  11. package/batteries/media/contracts.cjs +119 -0
  12. package/batteries/media/contracts.cjs.map +1 -0
  13. package/batteries/media/contracts.d.ts +321 -0
  14. package/batteries/media/contracts.mjs +110 -0
  15. package/batteries/media/contracts.mjs.map +1 -0
  16. package/batteries/media/engines/audio_decode.cjs +92 -0
  17. package/batteries/media/engines/audio_decode.cjs.map +1 -0
  18. package/batteries/media/engines/audio_decode.d.ts +46 -0
  19. package/batteries/media/engines/audio_decode.mjs +90 -0
  20. package/batteries/media/engines/audio_decode.mjs.map +1 -0
  21. package/batteries/media/engines/execa_executor.cjs +64 -0
  22. package/batteries/media/engines/execa_executor.cjs.map +1 -0
  23. package/batteries/media/engines/execa_executor.d.ts +54 -0
  24. package/batteries/media/engines/execa_executor.mjs +62 -0
  25. package/batteries/media/engines/execa_executor.mjs.map +1 -0
  26. package/batteries/media/engines/fs_workspace.cjs +84 -0
  27. package/batteries/media/engines/fs_workspace.cjs.map +1 -0
  28. package/batteries/media/engines/fs_workspace.d.ts +51 -0
  29. package/batteries/media/engines/fs_workspace.mjs +82 -0
  30. package/batteries/media/engines/fs_workspace.mjs.map +1 -0
  31. package/batteries/media/engines/jimp.cjs +116 -0
  32. package/batteries/media/engines/jimp.cjs.map +1 -0
  33. package/batteries/media/engines/jimp.d.ts +32 -0
  34. package/batteries/media/engines/jimp.mjs +114 -0
  35. package/batteries/media/engines/jimp.mjs.map +1 -0
  36. package/batteries/media/engines/sharp.cjs +120 -0
  37. package/batteries/media/engines/sharp.cjs.map +1 -0
  38. package/batteries/media/engines/sharp.d.ts +42 -0
  39. package/batteries/media/engines/sharp.mjs +117 -0
  40. package/batteries/media/engines/sharp.mjs.map +1 -0
  41. package/batteries/media/engines/soffice.cjs +246 -0
  42. package/batteries/media/engines/soffice.cjs.map +1 -0
  43. package/batteries/media/engines/soffice.d.ts +39 -0
  44. package/batteries/media/engines/soffice.mjs +244 -0
  45. package/batteries/media/engines/soffice.mjs.map +1 -0
  46. package/batteries/media/engines/tesseract_js.cjs +87 -0
  47. package/batteries/media/engines/tesseract_js.cjs.map +1 -0
  48. package/batteries/media/engines/tesseract_js.d.ts +41 -0
  49. package/batteries/media/engines/tesseract_js.mjs +85 -0
  50. package/batteries/media/engines/tesseract_js.mjs.map +1 -0
  51. package/batteries/media/engines/transformers_asr.cjs +111 -0
  52. package/batteries/media/engines/transformers_asr.cjs.map +1 -0
  53. package/batteries/media/engines/transformers_asr.d.ts +41 -0
  54. package/batteries/media/engines/transformers_asr.mjs +109 -0
  55. package/batteries/media/engines/transformers_asr.mjs.map +1 -0
  56. package/batteries/media/exceptions.d.ts +103 -0
  57. package/batteries/media/forge.cjs +403 -0
  58. package/batteries/media/forge.cjs.map +1 -0
  59. package/batteries/media/forge.d.ts +90 -0
  60. package/batteries/media/forge.mjs +399 -0
  61. package/batteries/media/forge.mjs.map +1 -0
  62. package/batteries/media/formats.d.ts +72 -0
  63. package/batteries/media/index.d.ts +136 -0
  64. package/batteries/media/lint.cjs +339 -0
  65. package/batteries/media/lint.cjs.map +1 -0
  66. package/batteries/media/lint.d.ts +117 -0
  67. package/batteries/media/lint.mjs +331 -0
  68. package/batteries/media/lint.mjs.map +1 -0
  69. package/batteries/media/pipe.d.ts +66 -0
  70. package/batteries/media/plan.d.ts +133 -0
  71. package/batteries/media/registry.d.ts +92 -0
  72. package/batteries/media/runtime.d.ts +105 -0
  73. package/batteries/media/steps/doc.d.ts +33 -0
  74. package/batteries/media/steps/image_audio.d.ts +24 -0
  75. package/batteries/media/steps/ingest.d.ts +25 -0
  76. package/batteries/media/steps/pages.d.ts +18 -0
  77. package/batteries/media/steps/sheet.d.ts +36 -0
  78. package/batteries/media/steps/slides.d.ts +35 -0
  79. package/batteries/media/steps/text.d.ts +43 -0
  80. package/batteries/media/validate.d.ts +49 -0
  81. package/batteries/media/verbs.d.ts +126 -0
  82. package/batteries/media.cjs +3049 -0
  83. package/batteries/media.cjs.map +1 -0
  84. package/batteries/media.mjs +3009 -0
  85. package/batteries/media.mjs.map +1 -0
  86. package/batteries/tools/_shared/index.d.ts +142 -0
  87. package/batteries/tools/_shared.cjs +173 -0
  88. package/batteries/tools/_shared.cjs.map +1 -0
  89. package/batteries/tools/_shared.mjs +164 -0
  90. package/batteries/tools/_shared.mjs.map +1 -0
  91. package/batteries/tools/index.d.ts +2 -0
  92. package/batteries/tools/scrapper/exceptions.d.ts +21 -0
  93. package/batteries/tools/scrapper/index.d.ts +172 -0
  94. package/batteries/tools/scrapper/shared.d.ts +146 -0
  95. package/batteries/tools/scrapper.cjs +8 -0
  96. package/batteries/tools/scrapper.mjs +2 -0
  97. package/batteries/tools/searxng/index.d.ts +54 -20
  98. package/batteries/tools/searxng.cjs +2 -1
  99. package/batteries/tools/searxng.mjs +2 -2
  100. package/batteries/tools/web_retrieval/index.d.ts +186 -0
  101. package/batteries/tools/web_retrieval.cjs +206 -0
  102. package/batteries/tools/web_retrieval.cjs.map +1 -0
  103. package/batteries/tools/web_retrieval.mjs +201 -0
  104. package/batteries/tools/web_retrieval.mjs.map +1 -0
  105. package/batteries/tools.cjs +13 -1
  106. package/batteries/tools.mjs +4 -2
  107. package/batteries.cjs +13 -1
  108. package/batteries.mjs +4 -2
  109. package/common.d.ts +1 -1
  110. package/eslint.cjs +1 -1
  111. package/eslint.mjs +1 -1
  112. package/exceptions-C7FSHEnV.mjs +87 -0
  113. package/exceptions-C7FSHEnV.mjs.map +1 -0
  114. package/exceptions-CQi_lNs1.js +152 -0
  115. package/exceptions-CQi_lNs1.js.map +1 -0
  116. package/index.cjs +2 -2
  117. package/index.mjs +2 -2
  118. package/mcp/adk-docs-corpus.json +1 -1
  119. package/package.json +301 -178
  120. package/scrapper-BOLWYGbD.js +463 -0
  121. package/scrapper-BOLWYGbD.js.map +1 -0
  122. package/scrapper-hDKlNuCT.mjs +433 -0
  123. package/scrapper-hDKlNuCT.mjs.map +1 -0
  124. package/{searxng-Bkrwhwhw.js → searxng-CJtEpa8p.js} +82 -85
  125. package/searxng-CJtEpa8p.js.map +1 -0
  126. package/{searxng-CyA-nEu5.mjs → searxng-riarj_0u.mjs} +76 -85
  127. package/searxng-riarj_0u.mjs.map +1 -0
  128. package/skills/adk-assembly/SKILL.md +2 -2
  129. package/validate-BFaUYHDN.js +1298 -0
  130. package/validate-BFaUYHDN.js.map +1 -0
  131. package/validate-DSZ3wicB.mjs +1215 -0
  132. package/validate-DSZ3wicB.mjs.map +1 -0
  133. package/searxng-Bkrwhwhw.js.map +0 -1
  134. package/searxng-CyA-nEu5.mjs.map +0 -1
@@ -0,0 +1,3009 @@
1
+ import { c as isObject, o as isError, s as isInstanceOf } from "../tool_registry-791Vrjtf.mjs";
2
+ import "../guards.mjs";
3
+ import { PCM_MIME, bytesToPcm, implementsMediaEngine, pcmToBytes } from "./media/contracts.mjs";
4
+ import { a as E_MEDIA_NOT_PIPE_EXPRESSIBLE, c as E_MEDIA_STEP_UNAVAILABLE, d as E_MEDIA_UNSUPPORTED_OP, i as E_MEDIA_MISSING_ARG, l as E_MEDIA_UNKNOWN_ARG, n as E_MEDIA_BAD_ARG, o as E_MEDIA_PIPE_SYNTAX, r as E_MEDIA_ENGINE_REQUIRED, s as E_MEDIA_STEP_FAILED, t as E_INVALID_MEDIA_PIPELINE_CONFIG, u as E_MEDIA_UNKNOWN_VERB } from "../exceptions-C7FSHEnV.mjs";
5
+ import { a as VERBS, c as suggestVerbs, d as isMediaRef, f as isRegExpRef, i as FOLDED_VERBS, l as canonicalFlags, m as toPipe, n as validateOps, o as VERB_INDEX, p as toOps, r as validateSegments, s as foldVerb, t as availableVerbs, u as fromOps } from "../validate-DSZ3wicB.mjs";
6
+ import { validator } from "@nhtio/validation";
7
+ import { Middleware } from "@nhtio/middleware";
8
+ import moo from "moo";
9
+ //#region src/batteries/media/pipe.ts
10
+ /**
11
+ * The pipe-expression front-end: a moo lexer plus a hand-rolled recursive-descent parser that
12
+ * compiles `select pages=2-5 | redact match=/…/ | convert to=pdf` into a {@link MediaPlan}.
13
+ *
14
+ * @remarks
15
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. The grammar is frozen in the
16
+ * design doc (section 0):
17
+ *
18
+ * - `pipeline := segment ('|' segment)*` ; `segment := verb arg*` ; `arg := IDENT '=' value` —
19
+ * named args only, no positionals (an IDENT followed by `=` is an arg; otherwise it is the
20
+ * verb's second word — 2-token lookahead, no verb table needed at parse time).
21
+ * - Verb matching is separator-insensitive (`extract_text` ≡ `extract text` ≡ `extract.text`).
22
+ * - Values: bareword idents, ints/floats, `a-b` ranges, comma lists, `true`/`false`, quoted
23
+ * strings (single or double), class-aware `/regex/flags` literals, `@id` media refs, and
24
+ * quoted-JSON structured payloads (a quoted string that the verb's arg schema declares as
25
+ * `json` is JSON-parsed).
26
+ * - `#` line comments are tolerated (models add them; ignoring them is free robustness).
27
+ * - Two error layers: syntactic ({@link E_MEDIA_PIPE_SYNTAX}, with line/col and a corrective
28
+ * exemplar) and semantic ({@link E_MEDIA_UNKNOWN_VERB} etc., produced by the validator in
29
+ * `validate.ts` — this module only parses to a raw AST and lowers to the plan).
30
+ *
31
+ * Parsing is deployment-independent: the full verb table drives folding, and engine narrowing
32
+ * happens later in validation (frozen 0.3).
33
+ */
34
+ /**
35
+ * The moo ruleset. Order matters: first match wins, so `range` must precede `int`, `float`
36
+ * precedes `int`, and `comment` precedes nothing it could shadow. String/regex bodies exclude
37
+ * raw newlines (escape them); `ws` carries `lineBreaks` so spans stay accurate across lines.
38
+ */
39
+ var lexer = moo.compile({
40
+ ws: {
41
+ match: /[ \t\r\n]+/,
42
+ lineBreaks: true
43
+ },
44
+ comment: /#[^\n]*/,
45
+ pipe: "|",
46
+ eq: "=",
47
+ comma: ",",
48
+ ref: /@[A-Za-z0-9_-]+/,
49
+ regex: /\/(?:\\.|\[(?:\\.|[^\]\\\n])*\]|[^/\\[\n])+\/[a-z]*/,
50
+ dstring: /"(?:\\.|[^"\\\n])*"/,
51
+ sstring: /'(?:\\.|[^'\\\n])*'/,
52
+ range: /\d+-\d+/,
53
+ float: /\d+\.\d+/,
54
+ int: /\d+/,
55
+ ident: /[A-Za-z_][A-Za-z0-9_.]*/,
56
+ dash: "-"
57
+ });
58
+ /** Tokenize, skipping ws/comments, surfacing lexer errors as syntax exceptions. */
59
+ var tokenize = (input) => {
60
+ lexer.reset(input);
61
+ const out = [];
62
+ let tok;
63
+ try {
64
+ while (tok = lexer.next()) {
65
+ if (tok.type === "ws" || tok.type === "comment") continue;
66
+ out.push({
67
+ type: tok.type,
68
+ value: tok.value,
69
+ text: tok.text,
70
+ offset: tok.offset,
71
+ line: tok.line,
72
+ col: tok.col
73
+ });
74
+ }
75
+ } catch (err) {
76
+ throw new E_MEDIA_PIPE_SYNTAX([`${isError(err) ? err.message.split("\n")[0] : String(err)}. Values containing special characters must be quoted — write it like: name="my-value"`]);
77
+ }
78
+ return out;
79
+ };
80
+ var spanOf = (tok, length) => ({
81
+ offset: tok.offset,
82
+ line: tok.line,
83
+ col: tok.col,
84
+ length: length ?? tok.text.length
85
+ });
86
+ var unquote = (raw) => raw.slice(1, -1).replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\(["'\\/])/g, "$1");
87
+ var fail$5 = (tok, message, exemplar) => {
88
+ throw new E_MEDIA_PIPE_SYNTAX([`${message}${tok ? ` at line ${tok.line}, col ${tok.col}` : " at end of input"}. Write it like: ${exemplar}`]);
89
+ };
90
+ var Parser = class {
91
+ #toks;
92
+ #pos = 0;
93
+ constructor(toks) {
94
+ this.#toks = toks;
95
+ }
96
+ #peek(ahead = 0) {
97
+ return this.#toks[this.#pos + ahead];
98
+ }
99
+ #next() {
100
+ return this.#toks[this.#pos++];
101
+ }
102
+ parsePipeline() {
103
+ const segments = [];
104
+ if (this.#toks.length === 0) fail$5(void 0, "empty pipe expression", "extract text | chunk by=sentence");
105
+ segments.push(this.#parseSegment());
106
+ while (this.#peek()?.type === "pipe") {
107
+ this.#next();
108
+ segments.push(this.#parseSegment());
109
+ }
110
+ const trailing = this.#peek();
111
+ if (trailing) fail$5(trailing, `unexpected "${trailing.text}" after a complete segment`, "segment | segment (segments are separated by |)");
112
+ return segments;
113
+ }
114
+ #parseSegment() {
115
+ const first = this.#peek();
116
+ if (!first || first.type !== "ident") fail$5(first, "expected a verb to start the segment", "convert to=pdf");
117
+ const head = this.#next();
118
+ const collected = [head.value];
119
+ const posAfterWord = [this.#pos];
120
+ while (collected.length < 3) {
121
+ if (this.#peek()?.type !== "ident" || this.#peek(1)?.type === "eq") break;
122
+ collected.push(this.#next().value);
123
+ posAfterWord.push(this.#pos);
124
+ }
125
+ let take = collected.length;
126
+ if (foldVerb(collected) === void 0) {
127
+ let prefix = collected.length - 1;
128
+ while (prefix >= 1 && foldVerb(collected.slice(0, prefix)) === void 0) prefix -= 1;
129
+ if (prefix >= 1) take = prefix;
130
+ }
131
+ const words = collected.slice(0, take);
132
+ this.#pos = posAfterWord[take - 1];
133
+ const folded = foldVerb(words);
134
+ const verbText = words.join(" ");
135
+ const args = /* @__PURE__ */ new Map();
136
+ for (;;) {
137
+ const tok = this.#peek();
138
+ if (!tok || tok.type === "pipe") break;
139
+ if (tok.type === "dash") fail$5(tok, "unexpected \"-\" — values containing dashes must be quoted", `${verbText} name="value-with-dashes"`);
140
+ if (tok.type !== "ident" || this.#peek(1)?.type !== "eq") fail$5(tok, `unexpected "${tok.text}" — args must be name=value`, `${verbText} name=value`);
141
+ const name = this.#next().value;
142
+ this.#next();
143
+ const value = this.#parseValue(verbText, name);
144
+ if (args.has(name)) fail$5(tok, `duplicate arg "${name}"`, `${verbText} ${name}=value (give each arg once)`);
145
+ args.set(name, value);
146
+ }
147
+ return {
148
+ verb: folded ?? verbText,
149
+ known: folded !== void 0,
150
+ args,
151
+ span: spanOf(head, verbText.length)
152
+ };
153
+ }
154
+ #parseValue(verbText, argName) {
155
+ const first = this.#parseScalar(verbText, argName);
156
+ if (this.#peek()?.type !== "comma") return first;
157
+ const items = [...asScalarList(first, verbText, argName)];
158
+ let quoted = first.quoted;
159
+ while (this.#peek()?.type === "comma") {
160
+ this.#next();
161
+ const next = this.#parseScalar(verbText, argName);
162
+ quoted = quoted || next.quoted;
163
+ items.push(...asScalarList(next, verbText, argName));
164
+ }
165
+ return {
166
+ value: items,
167
+ quoted,
168
+ span: first.span
169
+ };
170
+ }
171
+ #parseScalar(verbText, argName) {
172
+ const tok = this.#peek();
173
+ if (!tok) fail$5(tok, `missing value for "${argName}"`, `${verbText} ${argName}=value`);
174
+ const t = tok;
175
+ switch (t.type) {
176
+ case "int":
177
+ this.#next();
178
+ return {
179
+ value: Number(t.value),
180
+ quoted: false,
181
+ span: spanOf(t)
182
+ };
183
+ case "float":
184
+ this.#next();
185
+ return {
186
+ value: Number(t.value),
187
+ quoted: false,
188
+ span: spanOf(t)
189
+ };
190
+ case "range": {
191
+ this.#next();
192
+ const [lo, hi] = t.value.split("-").map(Number);
193
+ if (lo > hi) fail$5(t, `range ${t.value} is descending (start must be ≤ end)`, `${argName}=${hi}-${lo}`);
194
+ const nums = [];
195
+ for (let n = lo; n <= hi; n++) nums.push(n);
196
+ return {
197
+ value: nums,
198
+ quoted: false,
199
+ span: spanOf(t)
200
+ };
201
+ }
202
+ case "ident":
203
+ this.#next();
204
+ if (t.value === "true") return {
205
+ value: true,
206
+ quoted: false,
207
+ span: spanOf(t)
208
+ };
209
+ if (t.value === "false") return {
210
+ value: false,
211
+ quoted: false,
212
+ span: spanOf(t)
213
+ };
214
+ return {
215
+ value: t.value,
216
+ quoted: false,
217
+ span: spanOf(t)
218
+ };
219
+ case "dstring":
220
+ case "sstring":
221
+ this.#next();
222
+ return {
223
+ value: unquote(t.text),
224
+ quoted: true,
225
+ span: spanOf(t)
226
+ };
227
+ case "regex": {
228
+ this.#next();
229
+ const lastSlash = t.text.lastIndexOf("/");
230
+ const source = t.text.slice(1, lastSlash);
231
+ const flags = canonicalFlags(t.text.slice(lastSlash + 1));
232
+ try {
233
+ new RegExp(source, flags);
234
+ } catch (err) {
235
+ const detail = isError(err) ? err.message : String(err);
236
+ fail$5(t, `invalid regex ${t.text}: ${detail}`, `${argName}=/pattern/flags`);
237
+ }
238
+ return {
239
+ value: {
240
+ source,
241
+ flags
242
+ },
243
+ quoted: false,
244
+ span: spanOf(t)
245
+ };
246
+ }
247
+ case "ref":
248
+ this.#next();
249
+ return {
250
+ value: {
251
+ kind: "id",
252
+ id: t.value.slice(1)
253
+ },
254
+ quoted: false,
255
+ span: spanOf(t)
256
+ };
257
+ case "dash":
258
+ fail$5(t, "unexpected \"-\" in a bare value", `${argName}="value-with-dashes" (quote values containing dashes)`);
259
+ break;
260
+ default: fail$5(t, `unexpected "${t.text}"`, `${verbText} ${argName}=value`);
261
+ }
262
+ throw new E_MEDIA_PIPE_SYNTAX(["internal parser error"]);
263
+ }
264
+ };
265
+ var asScalarList = (raw, verbText, argName) => {
266
+ if (Array.isArray(raw.value)) return raw.value;
267
+ if (raw.value === null || typeof raw.value === "object") {
268
+ const v = raw.value;
269
+ if (isObject(v) && !Array.isArray(v) && !("source" in v) && !("kind" in v)) throw new E_MEDIA_PIPE_SYNTAX([`structured values cannot appear in a comma list for "${argName}" on "${verbText}"`]);
270
+ }
271
+ return [raw.value];
272
+ };
273
+ /**
274
+ * Parse a pipe expression into raw, position-bearing segments.
275
+ *
276
+ * @remarks
277
+ * Purely syntactic — verbs are fold-matched against the full verb table for canonicalization
278
+ * but unknown verbs are NOT an error here (the validator reports them with did-you-mean and the
279
+ * deployment's narrowed verb list). Use {@link parsePipe} for the validated path.
280
+ *
281
+ * @param input - The pipe expression.
282
+ * @returns The raw segments.
283
+ */
284
+ var parsePipeRaw = (input) => {
285
+ return new Parser(tokenize(input)).parsePipeline();
286
+ };
287
+ /**
288
+ * Lower raw segments to an (unvalidated) {@link MediaPlan}. Quoted-JSON arg parsing and
289
+ * type/enum checks happen in the validator, which consumes the raw segments — this lowering
290
+ * exists for tooling that wants the structural plan without validation.
291
+ *
292
+ * @param segments - Output of {@link parsePipeRaw}.
293
+ * @returns The structural plan (args carried as parsed, json args still strings).
294
+ */
295
+ var lowerSegments = (segments) => ({ steps: segments.map((seg) => ({
296
+ verb: seg.verb,
297
+ args: Object.fromEntries(Array.from(seg.args.entries(), ([k, v]) => [k, v.value])),
298
+ span: seg.span
299
+ })) });
300
+ //#endregion
301
+ //#region src/batteries/media/builder.ts
302
+ /**
303
+ * The chainable, thenable media builder — the implementor-facing front-end that compiles to
304
+ * the same {@link MediaPlan} as the pipe string and JSON ops.
305
+ *
306
+ * @remarks
307
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. The knex lessons applied:
308
+ * `mp(input)` opens a fresh immutable builder; every verb returns a new builder; awaiting the
309
+ * builder executes the chain (no `.execute()`). Domain verbs hang off typed namespaces
310
+ * (`.sheet`, `.slides`, `.image`, `.audio`); shared transforms live on the root.
311
+ *
312
+ * Each verb method appends an op and revalidates lazily — the full plan validates (verb table,
313
+ * arg schemas, engine narrowing) when the chain executes, so building is cheap and the same
314
+ * validator serves all three front-ends.
315
+ */
316
+ var refOf = (other) => typeof other === "string" ? {
317
+ kind: "id",
318
+ id: other
319
+ } : {
320
+ kind: "id",
321
+ id: other.mediaId
322
+ };
323
+ /**
324
+ * The chainable builder. Immutable — every verb returns a new instance sharing the executor.
325
+ * Thenable — `await` runs the chain and resolves to the terminal step's natural type.
326
+ */
327
+ var MediaChain = class MediaChain {
328
+ #ops;
329
+ #exec;
330
+ constructor(exec, ops = []) {
331
+ this.#exec = exec;
332
+ this.#ops = ops;
333
+ }
334
+ #with(verb, args) {
335
+ return new MediaChain(this.#exec, [...this.#ops, {
336
+ verb,
337
+ args
338
+ }]);
339
+ }
340
+ /** Convert to another format (requires a convert engine). */
341
+ convert(to) {
342
+ return this.#with("convert", { to });
343
+ }
344
+ /** Keep only the listed 1-based pages. */
345
+ select(options) {
346
+ return this.#with("select", { pages: options.pages });
347
+ }
348
+ /** Split by page (optionally grouped `[[start,end], …]`). Terminal: resolves to media list. */
349
+ split(options = {}) {
350
+ const args = {};
351
+ if (options.by) args.by = options.by;
352
+ if (options.ranges) args.ranges = options.ranges;
353
+ return this.#with("split", args);
354
+ }
355
+ /** Merge other media into this one, in order. */
356
+ merge(...others) {
357
+ return this.#with("merge", { with: others.map(refOf) });
358
+ }
359
+ /** Reorder pages by 1-based index. */
360
+ reorder(order) {
361
+ return this.#with("reorder", { order });
362
+ }
363
+ /** Redact matching text (literals or RegExp). */
364
+ redact(options) {
365
+ const args = { match: (Array.isArray(options.match) ? options.match : [options.match]).map((m) => typeof m === "string" ? m : {
366
+ source: m.source,
367
+ flags: Array.from(m.flags).sort().join("")
368
+ }) };
369
+ if (options.replace !== void 0) args.replace = options.replace;
370
+ return this.#with("redact", args);
371
+ }
372
+ /** Remove potentially unsafe embedded content. */
373
+ sanitize() {
374
+ return this.#with("sanitize", {});
375
+ }
376
+ /** Normalize structure/encoding. */
377
+ normalize() {
378
+ return this.#with("normalize", {});
379
+ }
380
+ /** Replace the first occurrence of anchor text. */
381
+ updateText(anchor, replace) {
382
+ return this.#with("update_text", {
383
+ anchor,
384
+ replace
385
+ });
386
+ }
387
+ /** Compare against another media. Terminal: resolves to a structured diff. */
388
+ diff(other) {
389
+ return this.#with("diff", { with: refOf(other) });
390
+ }
391
+ /** Apply a unified-diff patch. */
392
+ applyPatch(patch, options = {}) {
393
+ const args = { patch };
394
+ if (options.with) args.with = options.with.map(refOf);
395
+ return this.#with("apply_patch", args);
396
+ }
397
+ /** Extract text (routes by format; OCR engine used when needed). Terminal: resolves to text. */
398
+ extractText(options = {}) {
399
+ const args = {};
400
+ if (options.ocr) args.ocr = options.ocr;
401
+ if (options.ocrOut) args.ocr_out = options.ocrOut;
402
+ if (options.lang) args.lang = options.lang;
403
+ return this.#with("extract.text", args);
404
+ }
405
+ /** Extract metadata. Terminal: resolves to a metadata object. */
406
+ extractMetadata() {
407
+ return this.#with("extract.metadata", {});
408
+ }
409
+ /** Extract embedded assets. Terminal: resolves to a media list. */
410
+ extractAssets(options = {}) {
411
+ const args = {};
412
+ if (options.types) args.types = options.types;
413
+ return this.#with("extract.assets", args);
414
+ }
415
+ /** Chunk extracted text. Terminal: resolves to a chunk array. */
416
+ chunk(options = {}) {
417
+ const args = {};
418
+ if (options.strategy) args.by = options.strategy;
419
+ if (options.size !== void 0) args.size = options.size;
420
+ if (options.overlap !== void 0) args.overlap = options.overlap;
421
+ return this.#with("chunk", args);
422
+ }
423
+ /** Spreadsheet mutations. */
424
+ get sheet() {
425
+ return new SheetNamespace(this);
426
+ }
427
+ /** Presentation mutations. */
428
+ get slides() {
429
+ return new SlidesNamespace(this);
430
+ }
431
+ /** Image transforms. */
432
+ get image() {
433
+ return new ImageNamespace(this);
434
+ }
435
+ /** Audio operations. */
436
+ get audio() {
437
+ return new AudioNamespace(this);
438
+ }
439
+ /** The accumulated ops (a copy). */
440
+ toOps() {
441
+ return this.#ops.map((op) => ({
442
+ verb: op.verb,
443
+ args: { ...op.args }
444
+ }));
445
+ }
446
+ /** The canonical pipe form of the accumulated chain. */
447
+ toPipe() {
448
+ return toPipe({ steps: this.#ops.map((op) => ({
449
+ verb: op.verb,
450
+ args: op.args
451
+ })) });
452
+ }
453
+ /** Internal — used by namespaces to append. */
454
+ withOp(verb, args) {
455
+ return this.#with(verb, args);
456
+ }
457
+ /** Thenable: awaiting the chain executes it. */
458
+ then(onfulfilled, onrejected) {
459
+ return this.#exec(this.toOps()).then(unwrapResult).then(onfulfilled, onrejected);
460
+ }
461
+ /** Execute and return the raw {@link PlanResult} (no unwrapping). */
462
+ async run() {
463
+ return this.#exec(this.toOps());
464
+ }
465
+ };
466
+ /** Resolve a plan result to its natural awaited type. */
467
+ var unwrapResult = (result) => {
468
+ if (result.kind === "media") return result.payload;
469
+ if (result.kind === "media-list") return result.payloads;
470
+ return result.data;
471
+ };
472
+ /** `sheet.*` verbs, mirrored from the verb table. */
473
+ var SheetNamespace = class {
474
+ #chain;
475
+ constructor(chain) {
476
+ this.#chain = chain;
477
+ }
478
+ /** Insert rows. */
479
+ addRows(rows, options = {}) {
480
+ return this.#chain.withOp("sheet.add_rows", compact({
481
+ rows,
482
+ ...targetArgs(options),
483
+ before: options.before,
484
+ after: options.after
485
+ }));
486
+ }
487
+ /** Insert columns. */
488
+ addColumns(options) {
489
+ return this.#chain.withOp("sheet.add_columns", compact({
490
+ ...targetArgs(options),
491
+ headers: options.headers,
492
+ columns: options.columns,
493
+ before: options.before,
494
+ after: options.after
495
+ }));
496
+ }
497
+ /** Update cells. */
498
+ updateCells(updates, options = {}) {
499
+ return this.#chain.withOp("sheet.update_cells", compact({
500
+ updates,
501
+ ...targetArgs(options)
502
+ }));
503
+ }
504
+ /** Delete rows by 1-based index. */
505
+ deleteRows(rows, options = {}) {
506
+ return this.#chain.withOp("sheet.delete_rows", compact({
507
+ rows,
508
+ ...targetArgs(options)
509
+ }));
510
+ }
511
+ /** Delete columns by 1-based index. */
512
+ deleteColumns(columns, options = {}) {
513
+ return this.#chain.withOp("sheet.delete_columns", compact({
514
+ columns,
515
+ ...targetArgs(options)
516
+ }));
517
+ }
518
+ /** Rename a worksheet (name-targeted). */
519
+ renameSheet(sheet, to) {
520
+ return this.#chain.withOp("sheet.rename_sheet", {
521
+ sheet,
522
+ to
523
+ });
524
+ }
525
+ /** Add a worksheet. */
526
+ addSheet(name, options = {}) {
527
+ return this.#chain.withOp("sheet.add_sheet", compact({
528
+ name,
529
+ at: options.at
530
+ }));
531
+ }
532
+ /** Remove a worksheet (name-targeted). */
533
+ removeSheet(sheet) {
534
+ return this.#chain.withOp("sheet.remove_sheet", { sheet });
535
+ }
536
+ /** Reorder worksheets by names and/or 1-based indices. */
537
+ reorderSheets(order) {
538
+ return this.#chain.withOp("sheet.reorder_sheets", { order });
539
+ }
540
+ /** Table transforms by header name. */
541
+ transformTable(options) {
542
+ return this.#chain.withOp("sheet.transform_table", compact({
543
+ ...targetArgs(options),
544
+ header_row: options.headerRow,
545
+ select: options.select,
546
+ drop: options.drop,
547
+ rename: options.rename
548
+ }));
549
+ }
550
+ };
551
+ /** `slides.*` verbs. */
552
+ var SlidesNamespace = class {
553
+ #chain;
554
+ constructor(chain) {
555
+ this.#chain = chain;
556
+ }
557
+ /** Add a slide. */
558
+ add(options = {}) {
559
+ return this.#chain.withOp("slides.add", compact({
560
+ at: options.at,
561
+ title: options.title,
562
+ layout: options.layout
563
+ }));
564
+ }
565
+ /** Update text on a slide. */
566
+ updateText(text, options = {}) {
567
+ return this.#chain.withOp("slides.update_text", compact({
568
+ text,
569
+ slide: slideArg(options.slide),
570
+ placeholder: options.placeholder
571
+ }));
572
+ }
573
+ /** Update table cells on a slide. */
574
+ updateTable(updates, options = {}) {
575
+ return this.#chain.withOp("slides.update_table", compact({
576
+ updates,
577
+ slide: slideArg(options.slide)
578
+ }));
579
+ }
580
+ /** Replace an image on a slide with another media. */
581
+ updateImage(withMedia, options = {}) {
582
+ return this.#chain.withOp("slides.update_image", compact({
583
+ with: refOf(withMedia),
584
+ slide: slideArg(options.slide),
585
+ placeholder: options.placeholder
586
+ }));
587
+ }
588
+ /** Update chart data on a slide. */
589
+ updateChart(data, options = {}) {
590
+ return this.#chain.withOp("slides.update_chart", compact({
591
+ data,
592
+ slide: slideArg(options.slide)
593
+ }));
594
+ }
595
+ /** Delete slides by 1-based index. */
596
+ delete(slides) {
597
+ return this.#chain.withOp("slides.delete", { slides });
598
+ }
599
+ /** Reorder slides. */
600
+ reorder(order) {
601
+ return this.#chain.withOp("slides.reorder", { order });
602
+ }
603
+ /** Duplicate a slide. */
604
+ duplicate(slide, options = {}) {
605
+ return this.#chain.withOp("slides.duplicate", compact({
606
+ slide,
607
+ at: options.at
608
+ }));
609
+ }
610
+ };
611
+ /** `image.*` verbs (the runtime fuses adjacent image steps into one engine pass). */
612
+ var ImageNamespace = class {
613
+ #chain;
614
+ constructor(chain) {
615
+ this.#chain = chain;
616
+ }
617
+ /** Resize. */
618
+ resize(options) {
619
+ return this.#chain.withOp("image.resize", compact({
620
+ width: options.width,
621
+ height: options.height,
622
+ fit: options.fit
623
+ }));
624
+ }
625
+ /** Re-encode to another format. */
626
+ format(to, options = {}) {
627
+ return this.#chain.withOp("image.format", compact({
628
+ to,
629
+ quality: options.quality,
630
+ strip_metadata: options.stripMetadata
631
+ }));
632
+ }
633
+ /** Rotate clockwise. */
634
+ rotate(deg) {
635
+ return this.#chain.withOp("image.rotate", { deg: String(deg) });
636
+ }
637
+ /** Flip. */
638
+ flip(axis) {
639
+ return this.#chain.withOp("image.flip", { axis });
640
+ }
641
+ /** Strip EXIF/ICC metadata. */
642
+ stripMetadata() {
643
+ return this.#chain.withOp("image.strip_metadata", {});
644
+ }
645
+ };
646
+ /** `audio.*` verbs. */
647
+ var AudioNamespace = class {
648
+ #chain;
649
+ constructor(chain) {
650
+ this.#chain = chain;
651
+ }
652
+ /** Transcribe speech. Terminal: resolves to text (or srt/vtt/json per `out`). */
653
+ transcribe(options = {}) {
654
+ return this.#chain.withOp("audio.transcribe", compact({
655
+ lang: options.language,
656
+ out: options.out,
657
+ translate: options.translate
658
+ }));
659
+ }
660
+ };
661
+ var targetArgs = (options) => options.sheet === void 0 ? {} : { sheet: options.sheet };
662
+ var slideArg = (slide) => slide;
663
+ var compact = (args) => {
664
+ const out = {};
665
+ for (const [k, v] of Object.entries(args)) if (v !== void 0) out[k] = v;
666
+ return out;
667
+ };
668
+ //#endregion
669
+ //#region src/batteries/media/runtime.ts
670
+ /**
671
+ * The step runtime: executes a validated {@link MediaPlan} as an `@nhtio/middleware` onion,
672
+ * one stage per step, with consumer interceptors wrapping every step.
673
+ *
674
+ * @remarks
675
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. A fresh `Runner` is minted per
676
+ * execution (middleware runners are single-use), so the same compiled chain can be awaited
677
+ * repeatedly. Each step stage:
678
+ *
679
+ * 1. runs the consumer's `use` interceptors (the documented seam for DLP/AV scanning, caching
680
+ * via `shortCircuit(bytes)`, byte limits, and telemetry — the battery ships no built-in
681
+ * scanner/cache/limiter: those are policies, this is the seam), then
682
+ * 2. dispatches to the verb's registered {@link StepImpl}, validating the output at the step
683
+ * boundary so a misbehaving engine produces a clear error instead of corrupt bytes.
684
+ *
685
+ * Steps stream bytes in memory; nothing touches disk except inside a binary engine's
686
+ * `ScratchWorkspace`. Adjacent `image.*` steps are fused by the compiler before execution so
687
+ * a resize→format→rotate chain costs a single decode/encode.
688
+ */
689
+ /** Internal sentinel for {@link StepContext.shortCircuit}. */
690
+ var SHORT_CIRCUIT = Symbol("adk.media.shortCircuit");
691
+ var isShortCircuit = (value) => isObject(value) && value[SHORT_CIRCUIT] === true;
692
+ /**
693
+ * Execute a validated plan. Returns the final step's result; intermediate steps must produce
694
+ * `media` results (the compiler guarantees non-terminal steps are media-shaped).
695
+ *
696
+ * @param plan - The validated plan.
697
+ * @param options - Input payload, step registry, interceptors, engine access.
698
+ * @returns The terminal step's result.
699
+ */
700
+ var executePlan = async (plan, options) => {
701
+ const stash = /* @__PURE__ */ new Map();
702
+ let payload = options.input;
703
+ let result = {
704
+ kind: "media",
705
+ payload
706
+ };
707
+ for (let i = 0; i < plan.steps.length; i++) {
708
+ const step = plan.steps[i];
709
+ options.signal?.throwIfAborted();
710
+ const impl = options.steps.get(step.verb);
711
+ if (!impl) throw new E_MEDIA_STEP_UNAVAILABLE([step.verb]);
712
+ const ctx = {
713
+ plan,
714
+ stepIndex: i,
715
+ step,
716
+ payload,
717
+ signal: options.signal,
718
+ stash,
719
+ shortCircuit: (sc) => {
720
+ throw {
721
+ [SHORT_CIRCUIT]: true,
722
+ payload: sc
723
+ };
724
+ },
725
+ engines: options.engines,
726
+ resolveRef: options.resolveRef
727
+ };
728
+ try {
729
+ result = await runStep(ctx, impl, options.use);
730
+ } catch (err) {
731
+ if (isShortCircuit(err)) result = {
732
+ kind: "media",
733
+ payload: err.payload
734
+ };
735
+ else if (isError(err) && err.name.startsWith("E_MEDIA_")) throw err;
736
+ else {
737
+ const detail = isError(err) ? err.message : String(err);
738
+ throw new E_MEDIA_STEP_FAILED([step.verb, detail], { cause: err });
739
+ }
740
+ }
741
+ if (result.kind === "media") payload = result.payload;
742
+ else if (i < plan.steps.length - 1) if (result.kind === "data" && typeof result.asText === "string" && VERB_INDEX.get(step.verb)?.output === "text") {
743
+ payload = {
744
+ bytes: new TextEncoder().encode(result.asText),
745
+ mimeType: "text/plain",
746
+ filename: materializedName(payload.filename)
747
+ };
748
+ result = {
749
+ kind: "media",
750
+ payload
751
+ };
752
+ } else throw new E_MEDIA_STEP_FAILED([step.verb, "a non-terminal step produced a terminal result; only the last step may yield data or multiple media"]);
753
+ }
754
+ return result;
755
+ };
756
+ /** Run one step: interceptor onion (fresh runner) terminating in the implementation. */
757
+ var runStep = async (ctx, impl, use) => {
758
+ if (use.length === 0) return validated(ctx, await impl(ctx));
759
+ const mw = new Middleware();
760
+ for (const fn of use) mw.add(fn);
761
+ let result;
762
+ let caught;
763
+ await mw.runner().errorHandler(async (error) => {
764
+ caught = error;
765
+ }).finalHandler(async () => {
766
+ result = validated(ctx, await impl(ctx));
767
+ }).run((fn, next) => Promise.resolve(fn(ctx, next)));
768
+ if (caught !== void 0) throw caught;
769
+ if (result === void 0) throw new E_MEDIA_STEP_FAILED([ctx.step.verb, "a step interceptor did not call next() and did not short-circuit"]);
770
+ return result;
771
+ };
772
+ /** Validate a step implementation's output shape at the boundary. */
773
+ var validated = (ctx, result) => {
774
+ const bad = (msg) => {
775
+ throw new E_MEDIA_STEP_FAILED([ctx.step.verb, `implementation returned ${msg}`]);
776
+ };
777
+ const isBytes = (value) => isInstanceOf(value, "Uint8Array", Uint8Array);
778
+ if (result.kind === "media") {
779
+ if (!isBytes(result.payload?.bytes)) bad("a media result without bytes");
780
+ if (typeof result.payload.mimeType !== "string") bad("a media result without a mimeType");
781
+ return result;
782
+ }
783
+ if (result.kind === "media-list") {
784
+ if (!Array.isArray(result.payloads) || result.payloads.some((p) => !isBytes(p.bytes))) bad("a media-list result with invalid payloads");
785
+ return result;
786
+ }
787
+ if (result.kind === "data") return result;
788
+ return bad("an unknown result kind");
789
+ };
790
+ /** Filename for an R-step's materialized text payload. */
791
+ var materializedName = (filename) => {
792
+ const dot = filename.lastIndexOf(".");
793
+ return `${dot > 0 ? filename.slice(0, dot) : filename}.txt`;
794
+ };
795
+ /** Convenience: read a step arg with a typed cast (validation already ran). */
796
+ var argOf = (step, name) => step.args[name];
797
+ //#endregion
798
+ //#region src/batteries/media/formats.ts
799
+ /** Well-known MIME constants used across the battery. */
800
+ var MIME = {
801
+ DOCX: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
802
+ XLSX: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
803
+ PPTX: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
804
+ ODT: "application/vnd.oasis.opendocument.text",
805
+ ODS: "application/vnd.oasis.opendocument.spreadsheet",
806
+ ODP: "application/vnd.oasis.opendocument.presentation",
807
+ PDF: "application/pdf",
808
+ DOC: "application/msword",
809
+ XLS: "application/vnd.ms-excel",
810
+ PPT: "application/vnd.ms-powerpoint",
811
+ PAGES: "application/x-iwork-pages-sffpages",
812
+ NUMBERS: "application/x-iwork-numbers-sffnumbers",
813
+ KEYNOTE: "application/x-iwork-keynote-sffkey",
814
+ TXT: "text/plain",
815
+ MD: "text/markdown",
816
+ CSV: "text/csv",
817
+ HTML: "text/html",
818
+ JSON: "application/json",
819
+ PNG: "image/png",
820
+ JPEG: "image/jpeg",
821
+ WEBP: "image/webp",
822
+ TIFF: "image/tiff",
823
+ AVIF: "image/avif",
824
+ WAV: "audio/wav",
825
+ MP3: "audio/mpeg"
826
+ };
827
+ /** Legacy Office MIME types (extraction only — mutation is rejected). */
828
+ var LEGACY_OFFICE_MIMES = new Set([
829
+ MIME.DOC,
830
+ MIME.XLS,
831
+ MIME.PPT
832
+ ]);
833
+ /** Apple iWork MIME types (extraction only — mutation is rejected). */
834
+ var IWORK_MIMES = new Set([
835
+ MIME.PAGES,
836
+ MIME.NUMBERS,
837
+ MIME.KEYNOTE
838
+ ]);
839
+ var SPREADSHEET_MIMES = new Set([
840
+ MIME.XLSX,
841
+ MIME.ODS,
842
+ MIME.XLS,
843
+ MIME.CSV
844
+ ]);
845
+ var PRESENTATION_MIMES = new Set([
846
+ MIME.PPTX,
847
+ MIME.ODP,
848
+ MIME.PPT,
849
+ MIME.KEYNOTE
850
+ ]);
851
+ var DOCUMENT_MIMES = new Set([
852
+ MIME.PDF,
853
+ MIME.DOCX,
854
+ MIME.ODT,
855
+ MIME.DOC,
856
+ MIME.PAGES,
857
+ MIME.TXT,
858
+ MIME.MD,
859
+ MIME.HTML,
860
+ MIME.JSON,
861
+ "application/rtf",
862
+ "text/rtf"
863
+ ]);
864
+ /**
865
+ * Classify a MIME type into the verb table's broad format family.
866
+ *
867
+ * @param mimeType - The media MIME type.
868
+ * @returns The family used by verb-applicability checks.
869
+ */
870
+ var familyOf = (mimeType) => {
871
+ const mime = mimeType.toLowerCase().split(";")[0].trim();
872
+ if (SPREADSHEET_MIMES.has(mime)) return "spreadsheet";
873
+ if (PRESENTATION_MIMES.has(mime)) return "presentation";
874
+ if (DOCUMENT_MIMES.has(mime)) return "document";
875
+ if (mime.startsWith("image/")) return "image";
876
+ if (mime.startsWith("audio/")) return "audio";
877
+ if (mime.startsWith("text/")) return "document";
878
+ return "document";
879
+ };
880
+ /**
881
+ * Reason a mutation verb cannot run against this MIME type, or `undefined` when mutation is
882
+ * allowed. Returns a reason string (not a throw) so callers compose model-actionable failures.
883
+ *
884
+ * @param mimeType - The media MIME type.
885
+ * @returns A human-readable reason, or `undefined` when mutation is supported.
886
+ */
887
+ var unsupportedForMutationReason = (mimeType) => {
888
+ const mime = mimeType.toLowerCase().split(";")[0].trim();
889
+ if (LEGACY_OFFICE_MIMES.has(mime)) return `legacy Office format (${mime}) supports extraction only — convert it first (e.g. convert to=docx)`;
890
+ if (IWORK_MIMES.has(mime)) return `Apple iWork format (${mime}) supports extraction only`;
891
+ };
892
+ /** A small extension → MIME lookup for filenames produced by transforms. */
893
+ var EXT_TO_MIME = {
894
+ pdf: MIME.PDF,
895
+ docx: MIME.DOCX,
896
+ xlsx: MIME.XLSX,
897
+ pptx: MIME.PPTX,
898
+ odt: MIME.ODT,
899
+ ods: MIME.ODS,
900
+ odp: MIME.ODP,
901
+ doc: MIME.DOC,
902
+ xls: MIME.XLS,
903
+ ppt: MIME.PPT,
904
+ txt: MIME.TXT,
905
+ md: MIME.MD,
906
+ csv: MIME.CSV,
907
+ html: MIME.HTML,
908
+ json: MIME.JSON,
909
+ rtf: "application/rtf",
910
+ png: MIME.PNG,
911
+ jpg: MIME.JPEG,
912
+ jpeg: MIME.JPEG,
913
+ webp: MIME.WEBP,
914
+ tiff: MIME.TIFF,
915
+ avif: MIME.AVIF,
916
+ wav: MIME.WAV,
917
+ mp3: MIME.MP3
918
+ };
919
+ /**
920
+ * Replace (or add) a filename's extension.
921
+ *
922
+ * @param filename - The original filename.
923
+ * @param ext - The new extension, without the dot.
924
+ * @returns The filename with the new extension.
925
+ */
926
+ var replaceExtension = (filename, ext) => {
927
+ const dot = filename.lastIndexOf(".");
928
+ return `${dot > 0 ? filename.slice(0, dot) : filename}.${ext}`;
929
+ };
930
+ //#endregion
931
+ //#region src/batteries/media/steps/text.ts
932
+ /**
933
+ * Pure text-shaped step implementations: chunking, plain-text extraction, text diff/patch,
934
+ * and the text-mode redact/update_text paths.
935
+ *
936
+ * @remarks
937
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. The split functions are ported
938
+ * verbatim from the source server's `chunk_text` adapter (its green unit suite ports with
939
+ * them). All implementations here operate on in-memory bytes and run in any environment.
940
+ */
941
+ /** Split text on double-newline paragraph boundaries, with optional character overlap. */
942
+ var splitParagraph = (text, overlap) => {
943
+ const blocks = text.split(/\n{2,}/).map((b) => b.trim()).filter(Boolean);
944
+ const chunks = [];
945
+ let pos = 0;
946
+ for (let i = 0; i < blocks.length; i++) {
947
+ const block = blocks[i];
948
+ const overlapText = i > 0 && overlap > 0 ? blocks.slice(Math.max(0, i - 1), i).join("\n\n").slice(-overlap) : "";
949
+ const chunkText = overlapText ? `${overlapText}\n\n${block}` : block;
950
+ chunks.push({
951
+ index: i,
952
+ text: chunkText,
953
+ char_start: pos,
954
+ char_end: pos + block.length
955
+ });
956
+ pos += block.length + 2;
957
+ }
958
+ return chunks;
959
+ };
960
+ /** Split text on sentence-ending punctuation, with optional character overlap. */
961
+ var splitSentence = (text, overlap) => {
962
+ const sentences = text.match(/[^.!?]+[.!?](?:\s|$)|[^.!?]+$/g) ?? [text];
963
+ const chunks = [];
964
+ let pos = 0;
965
+ for (let i = 0; i < sentences.length; i++) {
966
+ const sentence = sentences[i].trim();
967
+ if (!sentence) continue;
968
+ const overlapText = i > 0 && overlap > 0 ? sentences.slice(Math.max(0, i - 1), i).join(" ").slice(-overlap) : "";
969
+ const chunkText = overlapText ? `${overlapText} ${sentence}` : sentence;
970
+ chunks.push({
971
+ index: chunks.length,
972
+ text: chunkText,
973
+ char_start: pos,
974
+ char_end: pos + sentence.length
975
+ });
976
+ pos += sentence.length + 1;
977
+ }
978
+ return chunks;
979
+ };
980
+ /** Split text into fixed-size character windows with optional overlap. */
981
+ var splitFixed = (text, chunkSize, overlap) => {
982
+ const chunks = [];
983
+ const step = Math.max(1, chunkSize - overlap);
984
+ let i = 0;
985
+ let idx = 0;
986
+ while (i < text.length) {
987
+ const start = Math.max(0, i - overlap);
988
+ const end = i + chunkSize;
989
+ chunks.push({
990
+ index: idx++,
991
+ text: text.slice(start, end),
992
+ char_start: start,
993
+ char_end: Math.min(end, text.length)
994
+ });
995
+ i += step;
996
+ }
997
+ return chunks;
998
+ };
999
+ var decoder = new TextDecoder("utf-8", { fatal: false });
1000
+ var encoder = new TextEncoder();
1001
+ /** Decode payload bytes as UTF-8 text (UTF-16 BOM aware). */
1002
+ var decodeText = (bytes) => {
1003
+ if (bytes.length >= 2) {
1004
+ if (bytes[0] === 255 && bytes[1] === 254) return new TextDecoder("utf-16le").decode(bytes.subarray(2));
1005
+ if (bytes[0] === 254 && bytes[1] === 255) return new TextDecoder("utf-16be").decode(bytes.subarray(2));
1006
+ }
1007
+ return decoder.decode(bytes).replace(/\r\n?/g, "\n");
1008
+ };
1009
+ var textPayload = (source, text, suffix) => ({
1010
+ bytes: encoder.encode(text),
1011
+ mimeType: MIME.TXT,
1012
+ filename: replaceExtension(source.filename, suffix)
1013
+ });
1014
+ /** `chunk` — split text content into retrieval chunks (data result). */
1015
+ var chunkStep = async (ctx) => {
1016
+ const by = argOf(ctx.step, "by") ?? "paragraph";
1017
+ const size = argOf(ctx.step, "size") ?? 1e3;
1018
+ const overlap = argOf(ctx.step, "overlap") ?? 0;
1019
+ const text = decodeText(ctx.payload.bytes);
1020
+ const chunks = by === "sentence" ? splitSentence(text, overlap) : by === "fixed" ? splitFixed(text, size, overlap) : splitParagraph(text, overlap);
1021
+ return {
1022
+ kind: "data",
1023
+ data: chunks,
1024
+ asText: JSON.stringify(chunks)
1025
+ };
1026
+ };
1027
+ /**
1028
+ * `extract.text` — Phase 0 covers the native-text path (txt/md/csv/json/html). Document
1029
+ * formats (pdf/docx/…) and OCR routes are added by later phases; until then they fail with a
1030
+ * clear reason.
1031
+ */
1032
+ var extractTextStep = async (ctx) => {
1033
+ const family = familyOf(ctx.payload.mimeType);
1034
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
1035
+ if (!(mime.startsWith("text/") || mime === MIME.JSON || mime === MIME.MD || mime === MIME.CSV)) throw new E_MEDIA_STEP_FAILED(["extract.text", `extraction for ${family} media (${mime}) is not yet implemented in this build`]);
1036
+ const text = decodeText(ctx.payload.bytes);
1037
+ return {
1038
+ kind: "data",
1039
+ data: text,
1040
+ asText: text
1041
+ };
1042
+ };
1043
+ /** `extract.metadata` — Phase 0: size/MIME/filename basics (format-aware fields come later). */
1044
+ var extractMetadataStep = async (ctx) => {
1045
+ const meta = {
1046
+ filename: ctx.payload.filename,
1047
+ mime_type: ctx.payload.mimeType,
1048
+ size_bytes: ctx.payload.bytes.byteLength,
1049
+ family: familyOf(ctx.payload.mimeType)
1050
+ };
1051
+ return {
1052
+ kind: "data",
1053
+ data: meta,
1054
+ asText: JSON.stringify(meta)
1055
+ };
1056
+ };
1057
+ var toPattern = (value) => {
1058
+ if (typeof value === "string") return value;
1059
+ const ref = value;
1060
+ return new RegExp(ref.source, ref.flags.includes("g") ? ref.flags : `${ref.flags}g`);
1061
+ };
1062
+ var applyRedaction = (text, patterns, replacement) => {
1063
+ let out = text;
1064
+ for (const p of patterns) {
1065
+ const pattern = toPattern(p);
1066
+ out = typeof pattern === "string" ? out.split(pattern).join(replacement) : out.replace(pattern, replacement);
1067
+ }
1068
+ return out;
1069
+ };
1070
+ /** `redact` — Phase 0: the text-media path. Document-format in-place redaction lands later. */
1071
+ var redactStep = async (ctx) => {
1072
+ if (!ctx.payload.mimeType.toLowerCase().startsWith("text/")) throw new E_MEDIA_STEP_FAILED(["redact", `redaction for ${ctx.payload.mimeType} is not yet implemented in this build`]);
1073
+ const raw = ctx.step.args.match;
1074
+ const patterns = Array.isArray(raw) ? raw : [raw];
1075
+ const replacement = argOf(ctx.step, "replace") ?? "█";
1076
+ const text = applyRedaction(decodeText(ctx.payload.bytes), patterns, replacement);
1077
+ return {
1078
+ kind: "media",
1079
+ payload: textPayload(ctx.payload, text, "txt")
1080
+ };
1081
+ };
1082
+ /** `update_text` — Phase 0: the text-media path. */
1083
+ var updateTextStep = async (ctx) => {
1084
+ if (!ctx.payload.mimeType.toLowerCase().startsWith("text/")) throw new E_MEDIA_STEP_FAILED(["update_text", `text update for ${ctx.payload.mimeType} is not yet implemented in this build`]);
1085
+ const anchor = argOf(ctx.step, "anchor");
1086
+ const replace = argOf(ctx.step, "replace") ?? "";
1087
+ const text = decodeText(ctx.payload.bytes);
1088
+ if (!text.includes(anchor)) throw new E_MEDIA_STEP_FAILED(["update_text", `anchor text not found: "${anchor.slice(0, 80)}"`]);
1089
+ const updated = text.replace(anchor, replace);
1090
+ return {
1091
+ kind: "media",
1092
+ payload: {
1093
+ ...ctx.payload,
1094
+ bytes: encoder.encode(updated)
1095
+ }
1096
+ };
1097
+ };
1098
+ /** Build a unified diff between two texts using the `diff` peer (lazy import). */
1099
+ var unifiedDiff = async (aName, bName, a, b) => {
1100
+ return (await import("diff")).createTwoFilesPatch(aName, bName, a, b);
1101
+ };
1102
+ /** `diff` — compare against another media (text-comparable formats in Phase 0). */
1103
+ var diffStep = async (ctx) => {
1104
+ const ref = ctx.step.args.with;
1105
+ if (ref.kind !== "id") throw new E_MEDIA_STEP_FAILED(["diff", "builder refs are not yet supported here"]);
1106
+ const other = await ctx.resolveRef(ref.id);
1107
+ const a = decodeText(ctx.payload.bytes);
1108
+ const b = decodeText(other.bytes);
1109
+ const patch = await unifiedDiff(ctx.payload.filename, other.filename, a, b);
1110
+ return {
1111
+ kind: "data",
1112
+ data: {
1113
+ patch,
1114
+ changes: (await import("diff")).diffLines(a, b).map((part) => ({
1115
+ added: part.added === true,
1116
+ removed: part.removed === true,
1117
+ count: part.count ?? 0,
1118
+ value: part.value
1119
+ }))
1120
+ },
1121
+ asText: patch
1122
+ };
1123
+ };
1124
+ /** `apply_patch` — apply a unified diff to the media text. */
1125
+ var applyPatchStep = async (ctx) => {
1126
+ const patch = argOf(ctx.step, "patch");
1127
+ const text = decodeText(ctx.payload.bytes);
1128
+ const result = (await import("diff")).applyPatch(text, patch);
1129
+ if (result === false) throw new E_MEDIA_STEP_FAILED(["apply_patch", "the patch does not apply to this media content (context mismatch)"]);
1130
+ return {
1131
+ kind: "media",
1132
+ payload: {
1133
+ ...ctx.payload,
1134
+ bytes: encoder.encode(result)
1135
+ }
1136
+ };
1137
+ };
1138
+ //#endregion
1139
+ //#region src/batteries/media/steps/doc.ts
1140
+ /**
1141
+ * Doc-domain step extensions: format-preserving text mutation for DOCX/PPTX, asset
1142
+ * extraction from OOXML/ODF archives and PDFs, and format conversion via the convert engine.
1143
+ *
1144
+ * @remarks
1145
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. Ported from the source server's
1146
+ * doc adapters; the format-dispatch contract follows the server's green e2e suite: DOCX and
1147
+ * PPTX redact/sanitize/normalize/update_text mutate text in place inside the container
1148
+ * (preserving the source format); text-like inputs mutate as text; PDF redact falls back to
1149
+ * text extraction (the server's PDF "visual annotation" path is content-stream-preserving and
1150
+ * out of scope here — the failure message says exactly that).
1151
+ */
1152
+ var zipPromise$2;
1153
+ var jszip$2 = () => {
1154
+ zipPromise$2 ??= import("jszip").then((m) => "default" in m ? m.default : m);
1155
+ return zipPromise$2;
1156
+ };
1157
+ var fail$4 = (verb, message) => {
1158
+ throw new E_MEDIA_STEP_FAILED([verb, message]);
1159
+ };
1160
+ var escapeXml$1 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1161
+ var unescapeXml = (value) => value.replace(/&apos;/g, "'").replace(/&quot;/g, "\"").replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
1162
+ /**
1163
+ * Replace matches inside text nodes of OOXML, aggregating runs per paragraph so patterns
1164
+ * spanning split runs still match (Word splits text across `w:t` runs on formatting changes).
1165
+ */
1166
+ var replaceInOoxml = (xml, paragraphTag, textTag, pattern, replacement) => {
1167
+ let totalMatches = 0;
1168
+ const pRegex = new RegExp(`<${paragraphTag}\\b[^>]*>[\\s\\S]*?</${paragraphTag}>`, "g");
1169
+ return {
1170
+ xml: xml.replace(pRegex, (pXml) => {
1171
+ const tElements = [];
1172
+ const tRegex = new RegExp(`<${textTag}(?:\\s[^>]*)?>([^<]*)</${textTag}>`, "g");
1173
+ let tMatch;
1174
+ while (tMatch = tRegex.exec(pXml)) tElements.push({
1175
+ start: tMatch.index,
1176
+ end: tMatch.index + tMatch[0].length,
1177
+ content: unescapeXml(tMatch[1])
1178
+ });
1179
+ if (tElements.length === 0) return pXml;
1180
+ const aggregated = tElements.map((t) => t.content).join("");
1181
+ const p = new RegExp(pattern.source, pattern.flags);
1182
+ const replaced = aggregated.replace(p, replacement);
1183
+ if (replaced === aggregated) return pXml;
1184
+ const counter = new RegExp(pattern.source, pattern.flags);
1185
+ while (counter.exec(aggregated)) {
1186
+ totalMatches += 1;
1187
+ if (!counter.global) break;
1188
+ }
1189
+ let out = pXml;
1190
+ for (let i = tElements.length - 1; i >= 0; i--) {
1191
+ const el = tElements[i];
1192
+ const space = textTag === "w:t" && i === 0 ? " xml:space=\"preserve\"" : "";
1193
+ const content = i === 0 ? escapeXml$1(replaced) : "";
1194
+ out = out.slice(0, el.start) + `<${textTag}${space}>${content}</${textTag}>` + out.slice(el.end);
1195
+ }
1196
+ return out;
1197
+ }),
1198
+ matchCount: totalMatches
1199
+ };
1200
+ };
1201
+ /** Map text-node mutation over every relevant part of a DOCX or PPTX archive. */
1202
+ var mutateOoxmlText = async (ctx, verb, mutateXml) => {
1203
+ const kind = ctx.payload.mimeType.toLowerCase().split(";")[0].trim() === MIME.DOCX ? "docx" : "pptx";
1204
+ const JSZip = await jszip$2();
1205
+ let zip;
1206
+ try {
1207
+ zip = await JSZip.loadAsync(ctx.payload.bytes);
1208
+ } catch (err) {
1209
+ fail$4(verb, `could not open the document container: ${isError(err) ? err.message : String(err)}`);
1210
+ throw err;
1211
+ }
1212
+ const parts = kind === "docx" ? Object.keys(zip.files).filter((n) => /^word\/(document|header\d*|footer\d*)\.xml$/.test(n)) : Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n));
1213
+ if (parts.length === 0) fail$4(verb, "the container has no text parts to mutate (corrupt file?)");
1214
+ for (const part of parts) {
1215
+ const xml = await zip.file(part).async("text");
1216
+ const next = mutateXml(xml, kind);
1217
+ if (next !== xml) zip.file(part, next);
1218
+ }
1219
+ const bytes = await zip.generateAsync({
1220
+ type: "uint8array",
1221
+ compression: "DEFLATE"
1222
+ });
1223
+ return {
1224
+ kind: "media",
1225
+ payload: {
1226
+ ...ctx.payload,
1227
+ bytes
1228
+ }
1229
+ };
1230
+ };
1231
+ var toGlobalRegex = (value) => {
1232
+ if (typeof value === "string") {
1233
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1234
+ return new RegExp(escaped, "g");
1235
+ }
1236
+ const ref = value;
1237
+ return new RegExp(ref.source, ref.flags.includes("g") ? ref.flags : `${ref.flags}g`);
1238
+ };
1239
+ /** `redact` — format dispatch: text media → text path; DOCX/PPTX → in-place container edit. */
1240
+ var docRedactStep = async (ctx) => {
1241
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
1242
+ if (mime.startsWith("text/")) return redactStep(ctx);
1243
+ if (mime === MIME.DOCX || mime === MIME.PPTX) {
1244
+ const raw = ctx.step.args.match;
1245
+ const patterns = Array.isArray(raw) ? raw : [raw];
1246
+ const replacement = argOf(ctx.step, "replace") ?? "█";
1247
+ return mutateOoxmlText(ctx, "redact", (xml, kind) => {
1248
+ let out = xml;
1249
+ for (const p of patterns) out = replaceInOoxml(out, kind === "docx" ? "w:p" : "a:p", kind === "docx" ? "w:t" : "a:t", toGlobalRegex(p), replacement).xml;
1250
+ return out;
1251
+ });
1252
+ }
1253
+ fail$4("redact", `redaction for ${mime} is not supported in this build (text, DOCX, and PPTX are). For PDF, extract the text first: extract text | redact …`);
1254
+ throw new Error("unreachable");
1255
+ };
1256
+ var BLOCKED_CONTROL = (code) => code >= 0 && code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127;
1257
+ var stripControlChars = (text) => {
1258
+ let out = "";
1259
+ for (const ch of text) {
1260
+ if (BLOCKED_CONTROL(ch.charCodeAt(0))) continue;
1261
+ out += ch;
1262
+ }
1263
+ return out;
1264
+ };
1265
+ var sanitizeTextNodes = (xml, textTag) => xml.replace(new RegExp(`<${textTag}(?:\\s[^>]*)?>([^<]*)</${textTag}>`, "g"), (full, content) => {
1266
+ const cleaned = stripControlChars(unescapeXml(content));
1267
+ if (cleaned === unescapeXml(content)) return full;
1268
+ return full.replace(content, escapeXml$1(cleaned));
1269
+ });
1270
+ /** `sanitize` — strip blocked control characters from text nodes / text media. */
1271
+ var docSanitizeStep = async (ctx) => {
1272
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
1273
+ if (mime.startsWith("text/")) {
1274
+ const cleaned = stripControlChars(new TextDecoder().decode(ctx.payload.bytes));
1275
+ return {
1276
+ kind: "media",
1277
+ payload: {
1278
+ ...ctx.payload,
1279
+ bytes: new TextEncoder().encode(cleaned)
1280
+ }
1281
+ };
1282
+ }
1283
+ if (mime === MIME.DOCX || mime === MIME.PPTX) return mutateOoxmlText(ctx, "sanitize", (xml, kind) => sanitizeTextNodes(xml, kind === "docx" ? "w:t" : "a:t"));
1284
+ fail$4("sanitize", `sanitize for ${mime} is not supported in this build (text, DOCX, PPTX are)`);
1285
+ throw new Error("unreachable");
1286
+ };
1287
+ var normalizeText = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[\t ]+$/gm, "");
1288
+ /** `normalize` — CRLF→LF + trailing-whitespace removal in text nodes / text media. */
1289
+ var docNormalizeStep = async (ctx) => {
1290
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
1291
+ if (mime.startsWith("text/")) {
1292
+ const text = new TextDecoder().decode(ctx.payload.bytes);
1293
+ return {
1294
+ kind: "media",
1295
+ payload: {
1296
+ ...ctx.payload,
1297
+ bytes: new TextEncoder().encode(normalizeText(text))
1298
+ }
1299
+ };
1300
+ }
1301
+ if (mime === MIME.DOCX || mime === MIME.PPTX) return mutateOoxmlText(ctx, "normalize", (xml, kind) => {
1302
+ const tag = kind === "docx" ? "w:t" : "a:t";
1303
+ return xml.replace(new RegExp(`<${tag}(?:\\s[^>]*)?>([^<]*)</${tag}>`, "g"), (full, content) => {
1304
+ const normalized = normalizeText(unescapeXml(content));
1305
+ if (normalized === unescapeXml(content)) return full;
1306
+ return full.replace(content, escapeXml$1(normalized));
1307
+ });
1308
+ });
1309
+ fail$4("normalize", `normalize for ${mime} is not supported in this build (text, DOCX, PPTX are)`);
1310
+ throw new Error("unreachable");
1311
+ };
1312
+ /** `update_text` — anchor replacement; DOCX/PPTX in-place, TARGET_NOT_FOUND when absent. */
1313
+ var docUpdateTextStep = async (ctx) => {
1314
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
1315
+ if (mime.startsWith("text/")) return updateTextStep(ctx);
1316
+ if (mime === MIME.DOCX || mime === MIME.PPTX) {
1317
+ const anchor = argOf(ctx.step, "anchor");
1318
+ const replace = argOf(ctx.step, "replace") ?? "";
1319
+ let found = 0;
1320
+ const result = await mutateOoxmlText(ctx, "update_text", (xml, kind) => {
1321
+ if (found > 0) return xml;
1322
+ const escaped = anchor.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1323
+ const { xml: next, matchCount } = replaceInOoxml(xml, kind === "docx" ? "w:p" : "a:p", kind === "docx" ? "w:t" : "a:t", new RegExp(escaped), replace);
1324
+ found += matchCount;
1325
+ return next;
1326
+ });
1327
+ if (found === 0) fail$4("update_text", `anchor text not found: "${anchor.slice(0, 80)}"`);
1328
+ return result;
1329
+ }
1330
+ fail$4("update_text", `text update for ${mime} is not supported (text, DOCX, PPTX are; PDF text is not reliably editable)`);
1331
+ throw new Error("unreachable");
1332
+ };
1333
+ var ASSET_PATTERNS = {
1334
+ image: /\.(png|jpe?g|gif|bmp|tiff?|emf|wmf|svg|webp)$/i,
1335
+ font: /\.(ttf|otf|woff2?|odttf)$/i,
1336
+ attachment: /\.(bin|ole|docx?|xlsx?|pptx?|pdf|txt|csv)$/i
1337
+ };
1338
+ var EXT_MIME = {
1339
+ png: "image/png",
1340
+ jpg: "image/jpeg",
1341
+ jpeg: "image/jpeg",
1342
+ gif: "image/gif",
1343
+ bmp: "image/bmp",
1344
+ tif: "image/tiff",
1345
+ tiff: "image/tiff",
1346
+ svg: "image/svg+xml",
1347
+ webp: "image/webp",
1348
+ emf: "image/emf",
1349
+ wmf: "image/wmf",
1350
+ ttf: "font/ttf",
1351
+ otf: "font/otf",
1352
+ woff: "font/woff",
1353
+ woff2: "font/woff2"
1354
+ };
1355
+ /**
1356
+ * `extract.assets` — enumerate embedded assets. OOXML/ODF archives are walked natively
1357
+ * (cross-env); PDF inputs use the pdfImages engine.
1358
+ */
1359
+ var extractAssetsStep = async (ctx) => {
1360
+ const verb = "extract assets";
1361
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
1362
+ const requested = ctx.step.args.types ?? ["all"];
1363
+ const wantAll = requested.includes("all");
1364
+ const want = (kind) => wantAll || requested.includes(kind);
1365
+ if (mime === MIME.PDF) {
1366
+ if (!want("image")) fail$4(verb, "PDF asset extraction currently supports images (types=image or all)");
1367
+ if (!ctx.engines.hasConvert(mime, "images")) fail$4(verb, "PDF asset extraction requires an engine that converts application/pdf to \"images\", and none is configured. Do not retry this verb on PDFs in this deployment.");
1368
+ const format = argOf(ctx.step, "format");
1369
+ const result = await ctx.engines.convert({
1370
+ bytes: ctx.payload.bytes,
1371
+ mimeType: mime,
1372
+ filename: ctx.payload.filename,
1373
+ to: "images",
1374
+ options: format ? { format } : void 0,
1375
+ signal: ctx.signal
1376
+ });
1377
+ if (result.outputs.length === 0) fail$4(verb, "no embedded images were found in the PDF");
1378
+ const wantedMime = format ? EXT_MIME[format] ?? `image/${format}` : void 0;
1379
+ const payloads = [];
1380
+ for (const [i, img] of result.outputs.entries()) {
1381
+ let bytes = img.bytes;
1382
+ let mimeType = img.mimeType;
1383
+ if (wantedMime && mimeType.toLowerCase() !== wantedMime) {
1384
+ const reencoded = await ctx.engines.mutate({
1385
+ bytes,
1386
+ mimeType,
1387
+ format: { to: format },
1388
+ signal: ctx.signal
1389
+ });
1390
+ bytes = reencoded.bytes;
1391
+ mimeType = reencoded.mimeType;
1392
+ }
1393
+ payloads.push({
1394
+ bytes,
1395
+ mimeType,
1396
+ filename: `asset-${i + 1}.${mimeType.split("/")[1]?.replace("jpeg", "jpg") ?? "bin"}`
1397
+ });
1398
+ }
1399
+ return {
1400
+ kind: "media-list",
1401
+ payloads
1402
+ };
1403
+ }
1404
+ if (![
1405
+ MIME.DOCX,
1406
+ MIME.XLSX,
1407
+ MIME.PPTX,
1408
+ MIME.ODT,
1409
+ MIME.ODS,
1410
+ MIME.ODP
1411
+ ].includes(mime)) fail$4(verb, `asset extraction for ${mime} is not supported (OOXML/ODF archives and PDF are)`);
1412
+ const zip = await (await jszip$2()).loadAsync(ctx.payload.bytes);
1413
+ const payloads = [];
1414
+ for (const [name, entry] of Object.entries(zip.files)) {
1415
+ if (entry.dir) continue;
1416
+ const basename = name.split("/").pop() ?? name;
1417
+ const matchKind = Object.entries(ASSET_PATTERNS).find(([, re]) => re.test(basename))?.[0];
1418
+ if (!matchKind || !want(matchKind)) continue;
1419
+ if (!/(^|\/)(media|embeddings|fonts|Pictures|ObjectReplacements)\//i.test(name)) continue;
1420
+ const ext = basename.split(".").pop()?.toLowerCase() ?? "bin";
1421
+ payloads.push({
1422
+ bytes: await entry.async("uint8array"),
1423
+ mimeType: EXT_MIME[ext] ?? "application/octet-stream",
1424
+ filename: basename
1425
+ });
1426
+ }
1427
+ if (payloads.length === 0) fail$4(verb, "no embedded assets matched the requested types");
1428
+ return {
1429
+ kind: "media-list",
1430
+ payloads
1431
+ };
1432
+ };
1433
+ /** `convert` — dispatch through the engine registry (direct edge or computed path). */
1434
+ var convertStep = async (ctx) => {
1435
+ const verb = "convert";
1436
+ const to = argOf(ctx.step, "to");
1437
+ const supported = ctx.engines.convertTargets(ctx.payload.mimeType);
1438
+ if (!supported.includes(to)) fail$4(verb, `no configured engine (or computed path) can produce "${to}" from ${ctx.payload.mimeType}; reachable targets: ${supported.join(", ") || "(none)"}`);
1439
+ const output = (await ctx.engines.convert({
1440
+ bytes: ctx.payload.bytes,
1441
+ mimeType: ctx.payload.mimeType,
1442
+ filename: ctx.payload.filename,
1443
+ to,
1444
+ signal: ctx.signal
1445
+ })).outputs[0];
1446
+ if (!output) fail$4(verb, "the conversion produced no output");
1447
+ const dot = ctx.payload.filename.lastIndexOf(".");
1448
+ const base = dot > 0 ? ctx.payload.filename.slice(0, dot) : ctx.payload.filename;
1449
+ return {
1450
+ kind: "media",
1451
+ payload: {
1452
+ bytes: output.bytes,
1453
+ mimeType: output.mimeType,
1454
+ filename: `${base}.${to}`
1455
+ }
1456
+ };
1457
+ };
1458
+ /** The doc step registry fragment (overrides the Phase 0 text-only redact/update_text). */
1459
+ var DOC_STEPS = [
1460
+ ["redact", docRedactStep],
1461
+ ["sanitize", docSanitizeStep],
1462
+ ["normalize", docNormalizeStep],
1463
+ ["update_text", docUpdateTextStep],
1464
+ ["extract.assets", extractAssetsStep],
1465
+ ["convert", convertStep]
1466
+ ];
1467
+ //#endregion
1468
+ //#region src/batteries/media/steps/sheet.ts
1469
+ /**
1470
+ * `sheet.*` step implementations: ExcelJS workbook mutations on in-memory bytes.
1471
+ *
1472
+ * @remarks
1473
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. Ported from the source server's
1474
+ * sheet adapters (its green e2e suite informs the spec coverage). All ten mutations operate on
1475
+ * xlsx natively via ExcelJS (optional peer, lazily imported); ODS and legacy-xls inputs are
1476
+ * normalized to xlsx first through the configured `sheetNormalize` engine, degrading to a
1477
+ * readable failure when none is configured. Output is always xlsx.
1478
+ */
1479
+ var excelPromise;
1480
+ var excel = () => {
1481
+ excelPromise ??= import("exceljs").then((m) => "default" in m ? m.default : m);
1482
+ return excelPromise;
1483
+ };
1484
+ var fail$3 = (verb, message) => {
1485
+ throw new E_MEDIA_STEP_FAILED([verb, message]);
1486
+ };
1487
+ /** Acquire xlsx bytes for the step, converting ODS/xls to xlsx via the engine registry. */
1488
+ var acquireXlsx = async (ctx, verb) => {
1489
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
1490
+ if (mime === MIME.XLSX) return ctx.payload.bytes;
1491
+ const mutationBlock = unsupportedForMutationReason(mime);
1492
+ if (mime === MIME.ODS || mime === MIME.XLS) {
1493
+ if (!ctx.engines.hasConvert(mime, "xlsx")) fail$3(verb, `this input is ${mime} — an engine that converts it to xlsx is required first, and none is configured. Do not retry this verb on this media in this deployment.`);
1494
+ const output = (await ctx.engines.convert({
1495
+ bytes: ctx.payload.bytes,
1496
+ mimeType: mime,
1497
+ filename: ctx.payload.filename,
1498
+ to: "xlsx",
1499
+ signal: ctx.signal
1500
+ })).outputs[0];
1501
+ if (!output) fail$3(verb, "normalizing the workbook to xlsx produced no output");
1502
+ return output.bytes;
1503
+ }
1504
+ if (mutationBlock) fail$3(verb, mutationBlock);
1505
+ fail$3(verb, `sheet operations expect a spreadsheet; the media is ${mime}`);
1506
+ throw new Error("unreachable");
1507
+ };
1508
+ /** The shared open → mutate → write-back lifecycle every sheet verb uses. */
1509
+ var withWorkbook = async (ctx, verb, mutate) => {
1510
+ const xlsxBytes = await acquireXlsx(ctx, verb);
1511
+ const wb = new (await (excel())).Workbook();
1512
+ try {
1513
+ await wb.xlsx.load(xlsxBytes.buffer.slice(xlsxBytes.byteOffset, xlsxBytes.byteOffset + xlsxBytes.byteLength));
1514
+ } catch (err) {
1515
+ fail$3(verb, `could not open the spreadsheet: ${isError(err) ? err.message : String(err)}`);
1516
+ }
1517
+ await mutate(wb);
1518
+ return {
1519
+ kind: "media",
1520
+ payload: {
1521
+ bytes: new Uint8Array(await wb.xlsx.writeBuffer()),
1522
+ mimeType: MIME.XLSX,
1523
+ filename: replaceExtension(ctx.payload.filename, "xlsx")
1524
+ }
1525
+ };
1526
+ };
1527
+ /** Resolve the target worksheet from the frozen `sheet=` rule (bare number=index, string=name). */
1528
+ var resolveSheet = (verb, wb, sheetArg) => {
1529
+ if (sheetArg === void 0) {
1530
+ const first = wb.worksheets[0];
1531
+ if (!first) fail$3(verb, "the workbook has no worksheets");
1532
+ return first;
1533
+ }
1534
+ if (typeof sheetArg === "number") {
1535
+ const byIndex = wb.worksheets[sheetArg - 1];
1536
+ if (byIndex) return byIndex;
1537
+ if (wb.getWorksheet(String(sheetArg))) fail$3(verb, `no sheet at index ${sheetArg}, but a sheet NAMED "${sheetArg}" exists — quote it: sheet="${sheetArg}"`);
1538
+ fail$3(verb, `sheet index ${sheetArg} is out of range (1-based; the workbook has ${wb.worksheets.length})`);
1539
+ }
1540
+ const ws = wb.getWorksheet(sheetArg);
1541
+ if (!ws) fail$3(verb, `no sheet named "${sheetArg}". Sheets: ${wb.worksheets.map((w) => `"${w.name}"`).join(", ")}`);
1542
+ return ws;
1543
+ };
1544
+ var columnLetterToNumber = (col) => {
1545
+ let n = 0;
1546
+ for (const ch of col.toUpperCase()) n = n * 26 + (ch.charCodeAt(0) - 64);
1547
+ return n;
1548
+ };
1549
+ var asCellValue = (verb, value) => {
1550
+ if (value === null) return null;
1551
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
1552
+ fail$3(verb, `cell values must be string, number, boolean, or null; got ${typeof value}`);
1553
+ throw new Error("unreachable");
1554
+ };
1555
+ /** `sheet.add_rows` — insert rows before/after an index, or append. */
1556
+ var sheetAddRowsStep = async (ctx) => {
1557
+ const verb = "sheet add_rows";
1558
+ const rows = ctx.step.args.rows;
1559
+ if (!Array.isArray(rows) || rows.length === 0 || !rows.every(Array.isArray)) fail$3(verb, `rows must be a non-empty JSON array of arrays, e.g. rows='[["a",1]]'`);
1560
+ const before = argOf(ctx.step, "before");
1561
+ const after = argOf(ctx.step, "after");
1562
+ return withWorkbook(ctx, verb, (wb) => {
1563
+ const ws = resolveSheet(verb, wb, argOf(ctx.step, "sheet"));
1564
+ const insertAt = before !== void 0 ? before : after !== void 0 ? after + 1 : ws.rowCount + 1;
1565
+ const cellRows = rows.map((r) => r.map((v) => asCellValue(verb, v)));
1566
+ ws.spliceRows(insertAt, 0, ...cellRows);
1567
+ return { added: cellRows.length };
1568
+ });
1569
+ };
1570
+ /** `sheet.add_columns` — insert columns with headers or full descriptors. */
1571
+ var sheetAddColumnsStep = async (ctx) => {
1572
+ const verb = "sheet add_columns";
1573
+ const headers = ctx.step.args.headers;
1574
+ const columns = ctx.step.args.columns ?? headers?.map((header) => ({ header }));
1575
+ if (!Array.isArray(columns) || columns.length === 0) fail$3(verb, `provide headers=a,b or columns='[{"header":"X","values":[1]}]'`);
1576
+ const before = argOf(ctx.step, "before");
1577
+ const after = argOf(ctx.step, "after");
1578
+ return withWorkbook(ctx, verb, (wb) => {
1579
+ const ws = resolveSheet(verb, wb, argOf(ctx.step, "sheet"));
1580
+ const insertAt = before !== void 0 ? before : after !== void 0 ? after + 1 : ws.columnCount + 1;
1581
+ for (const [i, col] of columns.entries()) {
1582
+ if (typeof col?.header !== "string") fail$3(verb, "every column needs a string header");
1583
+ ws.spliceColumns(insertAt + i, 0, [col.header, ...(col.values ?? []).map((v) => asCellValue(verb, v))]);
1584
+ }
1585
+ return { added: columns.length };
1586
+ });
1587
+ };
1588
+ /** `sheet.update_cells` — set cells by A1 address or row/col. Leading `=` writes a formula. */
1589
+ var sheetUpdateCellsStep = async (ctx) => {
1590
+ const verb = "sheet update_cells";
1591
+ const updates = ctx.step.args.updates;
1592
+ if (!Array.isArray(updates) || updates.length === 0) fail$3(verb, `updates must be a non-empty JSON array, e.g. updates='[{"address":"B2","value":3}]'`);
1593
+ return withWorkbook(ctx, verb, (wb) => {
1594
+ const ws = resolveSheet(verb, wb, argOf(ctx.step, "sheet"));
1595
+ let modified = 0;
1596
+ for (const raw of updates) {
1597
+ const address = raw.address;
1598
+ const row = raw.row;
1599
+ const col = raw.col;
1600
+ let cell;
1601
+ if (address) cell = ws.getCell(address);
1602
+ else if (row !== void 0 && col !== void 0) cell = typeof col === "string" ? ws.getCell(row, columnLetterToNumber(col)) : ws.getCell(row, col);
1603
+ else {
1604
+ fail$3(verb, "each update needs address, or both row and col");
1605
+ continue;
1606
+ }
1607
+ const value = raw.value;
1608
+ if (typeof value === "string" && value.startsWith("=")) cell.value = { formula: value.slice(1) };
1609
+ else cell.value = asCellValue(verb, value);
1610
+ modified += 1;
1611
+ }
1612
+ return { modified };
1613
+ });
1614
+ };
1615
+ /** `sheet.delete_rows` — delete 1-based rows (bottom-up so indices stay valid). */
1616
+ var sheetDeleteRowsStep = async (ctx) => {
1617
+ const verb = "sheet delete_rows";
1618
+ const rows = ctx.step.args.rows;
1619
+ return withWorkbook(ctx, verb, (wb) => {
1620
+ const ws = resolveSheet(verb, wb, argOf(ctx.step, "sheet"));
1621
+ for (const r of [...rows].sort((a, b) => b - a)) ws.spliceRows(r, 1);
1622
+ return { removed: rows.length };
1623
+ });
1624
+ };
1625
+ /** `sheet.delete_columns` — delete 1-based columns. */
1626
+ var sheetDeleteColumnsStep = async (ctx) => {
1627
+ const verb = "sheet delete_columns";
1628
+ const columns = ctx.step.args.columns;
1629
+ return withWorkbook(ctx, verb, (wb) => {
1630
+ const ws = resolveSheet(verb, wb, argOf(ctx.step, "sheet"));
1631
+ for (const c of [...columns].sort((a, b) => b - a)) ws.spliceColumns(c, 1);
1632
+ return { removed: columns.length };
1633
+ });
1634
+ };
1635
+ /** `sheet.rename_sheet` — rename by NAME (the server contract; index targeting is rejected). */
1636
+ var sheetRenameSheetStep = async (ctx) => {
1637
+ const verb = "sheet rename_sheet";
1638
+ const sheet = argOf(ctx.step, "sheet");
1639
+ const to = argOf(ctx.step, "to");
1640
+ return withWorkbook(ctx, verb, (wb) => {
1641
+ const ws = wb.getWorksheet(sheet);
1642
+ if (!ws) fail$3(verb, `rename targets a sheet NAME; no sheet named "${sheet}". Sheets: ${wb.worksheets.map((w) => `"${w.name}"`).join(", ")}`);
1643
+ ws.name = to;
1644
+ return { modified: 1 };
1645
+ });
1646
+ };
1647
+ /** `sheet.add_sheet` — add a worksheet, optionally at a 1-based position. */
1648
+ var sheetAddSheetStep = async (ctx) => {
1649
+ const verb = "sheet add_sheet";
1650
+ const name = argOf(ctx.step, "name");
1651
+ const at = argOf(ctx.step, "at");
1652
+ return withWorkbook(ctx, verb, (wb) => {
1653
+ if (wb.getWorksheet(name)) fail$3(verb, `a sheet named "${name}" already exists`);
1654
+ const ws = wb.addWorksheet(name);
1655
+ if (at !== void 0) {
1656
+ const all = wb.worksheets;
1657
+ const wsIdx = all.indexOf(ws);
1658
+ if (wsIdx !== at - 1) all.forEach((w, i) => {
1659
+ w.orderNo = w === ws ? at : i + 1 >= at && i < wsIdx ? i + 2 : i + 1;
1660
+ });
1661
+ }
1662
+ return { added: 1 };
1663
+ });
1664
+ };
1665
+ /** `sheet.remove_sheet` — remove by NAME (the server contract). */
1666
+ var sheetRemoveSheetStep = async (ctx) => {
1667
+ const verb = "sheet remove_sheet";
1668
+ const sheet = argOf(ctx.step, "sheet");
1669
+ return withWorkbook(ctx, verb, (wb) => {
1670
+ const ws = wb.getWorksheet(sheet);
1671
+ if (!ws) fail$3(verb, `remove targets a sheet NAME; no sheet named "${sheet}". Sheets: ${wb.worksheets.map((w) => `"${w.name}"`).join(", ")}`);
1672
+ wb.removeWorksheet(ws.id);
1673
+ return { removed: 1 };
1674
+ });
1675
+ };
1676
+ /** `sheet.reorder_sheets` — every sheet exactly once, by name or 1-based index. */
1677
+ var sheetReorderSheetsStep = async (ctx) => {
1678
+ const verb = "sheet reorder_sheets";
1679
+ const order = ctx.step.args.order;
1680
+ if (!Array.isArray(order) || order.length === 0) fail$3(verb, `order must be a JSON array of names/indices, e.g. order='["Summary",2]'`);
1681
+ return withWorkbook(ctx, verb, (wb) => {
1682
+ const refs = order;
1683
+ if (refs.length !== wb.worksheets.length) fail$3(verb, `order must include every worksheet exactly once (the workbook has ${wb.worksheets.length})`);
1684
+ const reordered = [];
1685
+ const seen = /* @__PURE__ */ new Set();
1686
+ for (const ref of refs) {
1687
+ const ws = typeof ref === "number" ? wb.worksheets[ref - 1] : wb.getWorksheet(ref);
1688
+ if (!ws) fail$3(verb, `sheet not found: ${String(ref)}`);
1689
+ if (seen.has(ws.id)) fail$3(verb, `duplicate sheet reference: ${String(ref)}`);
1690
+ seen.add(ws.id);
1691
+ reordered.push(ws);
1692
+ }
1693
+ reordered.forEach((ws, i) => {
1694
+ ws.orderNo = i + 1;
1695
+ });
1696
+ return { modified: reordered.length };
1697
+ });
1698
+ };
1699
+ /** `sheet.transform_table` — rename/select/drop columns by header name. */
1700
+ var sheetTransformTableStep = async (ctx) => {
1701
+ const verb = "sheet transform_table";
1702
+ const headerRow = argOf(ctx.step, "header_row") ?? 1;
1703
+ const rename = ctx.step.args.rename;
1704
+ const select = ctx.step.args.select;
1705
+ const drop = ctx.step.args.drop;
1706
+ return withWorkbook(ctx, verb, (wb) => {
1707
+ const ws = resolveSheet(verb, wb, argOf(ctx.step, "sheet"));
1708
+ let modified = 0;
1709
+ const headerMap = /* @__PURE__ */ new Map();
1710
+ ws.getRow(headerRow).eachCell({ includeEmpty: false }, (cell, colNumber) => {
1711
+ if (typeof cell.value === "string") headerMap.set(cell.value, colNumber);
1712
+ });
1713
+ if (rename) for (const { from, to } of rename) {
1714
+ const colNum = headerMap.get(from);
1715
+ if (colNum !== void 0) {
1716
+ ws.getRow(headerRow).getCell(colNum).value = to;
1717
+ headerMap.delete(from);
1718
+ headerMap.set(to, colNum);
1719
+ modified += 1;
1720
+ }
1721
+ }
1722
+ if (drop) {
1723
+ const cols = drop.map((name) => headerMap.get(name)).filter((n) => n !== void 0).sort((a, b) => b - a);
1724
+ for (const col of cols) {
1725
+ ws.spliceColumns(col, 1);
1726
+ modified += 1;
1727
+ }
1728
+ }
1729
+ if (select) {
1730
+ const keep = new Set(select);
1731
+ const cols = [];
1732
+ ws.getRow(headerRow).eachCell({ includeEmpty: false }, (cell, colNumber) => {
1733
+ if (typeof cell.value === "string" && !keep.has(cell.value)) cols.push(colNumber);
1734
+ });
1735
+ cols.sort((a, b) => b - a);
1736
+ for (const col of cols) {
1737
+ ws.spliceColumns(col, 1);
1738
+ modified += 1;
1739
+ }
1740
+ }
1741
+ return { modified };
1742
+ });
1743
+ };
1744
+ /** The sheet step registry fragment, keyed by canonical verb id. */
1745
+ var SHEET_STEPS = [
1746
+ ["sheet.add_rows", sheetAddRowsStep],
1747
+ ["sheet.add_columns", sheetAddColumnsStep],
1748
+ ["sheet.update_cells", sheetUpdateCellsStep],
1749
+ ["sheet.delete_rows", sheetDeleteRowsStep],
1750
+ ["sheet.delete_columns", sheetDeleteColumnsStep],
1751
+ ["sheet.rename_sheet", sheetRenameSheetStep],
1752
+ ["sheet.add_sheet", sheetAddSheetStep],
1753
+ ["sheet.remove_sheet", sheetRemoveSheetStep],
1754
+ ["sheet.reorder_sheets", sheetReorderSheetsStep],
1755
+ ["sheet.transform_table", sheetTransformTableStep]
1756
+ ];
1757
+ //#endregion
1758
+ //#region src/batteries/media/steps/slides.ts
1759
+ /**
1760
+ * `slides.*` step implementations: native PPTX container mutation via JSZip + targeted XML
1761
+ * edits, on in-memory bytes.
1762
+ *
1763
+ * @remarks
1764
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. Ported from the source server's
1765
+ * slides adapters (regex-over-zip XML surgery — no XML parser dependency). PPTX is mutated
1766
+ * natively; ODP inputs are normalized to PPTX through the configured `convert` engine first,
1767
+ * degrading to a readable failure when none is configured. All implementations are
1768
+ * cross-environment (jszip operates on Uint8Array).
1769
+ */
1770
+ var zipPromise$1;
1771
+ var jszip$1 = () => {
1772
+ zipPromise$1 ??= import("jszip").then((m) => "default" in m ? m.default : m);
1773
+ return zipPromise$1;
1774
+ };
1775
+ var SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
1776
+ var fail$2 = (verb, message) => {
1777
+ throw new E_MEDIA_STEP_FAILED([verb, message]);
1778
+ };
1779
+ var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1780
+ var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1781
+ /** Pure posix-style path resolution for zip entries (no node:path). */
1782
+ var resolveZipPath = (basePart, relTarget) => {
1783
+ const parts = [...basePart.split("/").slice(0, -1), ...relTarget.split("/")];
1784
+ const out = [];
1785
+ for (const part of parts) {
1786
+ if (part === "" || part === ".") continue;
1787
+ if (part === "..") out.pop();
1788
+ else out.push(part);
1789
+ }
1790
+ return out.join("/");
1791
+ };
1792
+ var readZipText = async (verb, zip, path) => {
1793
+ const file = zip.file(path);
1794
+ if (!file) fail$2(verb, `the presentation is missing an expected part: ${path}`);
1795
+ return file.async("text");
1796
+ };
1797
+ var parseRelationshipTarget = (relsXml, relId) => {
1798
+ const re = new RegExp(`<Relationship\\b[^>]*Id="${escapeRegex(relId)}"[^>]*Target="([^"]+)"[^>]*>`, "i");
1799
+ return relsXml.match(re)?.[1] ?? null;
1800
+ };
1801
+ var parsePresentation = async (verb, zip) => {
1802
+ const presentationXml = await readZipText(verb, zip, "ppt/presentation.xml");
1803
+ const relsXml = await readZipText(verb, zip, "ppt/_rels/presentation.xml.rels");
1804
+ const sldIdListMatch = presentationXml.match(/<p:sldIdLst>([\s\S]*?)<\/p:sldIdLst>/);
1805
+ if (!sldIdListMatch) fail$2(verb, "the presentation has no slide list (corrupt PPTX?)");
1806
+ const sldIdTags = sldIdListMatch[1].match(/<p:sldId\b[^>]*\/>/g) ?? [];
1807
+ const entries = [];
1808
+ for (const tag of sldIdTags) {
1809
+ const rid = tag.match(/r:id="([^"]+)"/)?.[1];
1810
+ const idText = tag.match(/\bid="(\d+)"/)?.[1];
1811
+ if (!rid || !idText) continue;
1812
+ const target = parseRelationshipTarget(relsXml, rid);
1813
+ if (!target) continue;
1814
+ const slidePath = resolveZipPath("ppt/presentation.xml", target);
1815
+ let title;
1816
+ const slideFile = zip.file(slidePath);
1817
+ if (slideFile) title = (await slideFile.async("text")).match(/<p:cSld\b[^>]*\bname="([^"]*)"/)?.[1];
1818
+ entries.push({
1819
+ rid,
1820
+ slideId: Number(idText),
1821
+ sldIdTag: tag,
1822
+ target,
1823
+ title
1824
+ });
1825
+ }
1826
+ return {
1827
+ presentationXml,
1828
+ relsXml,
1829
+ entries
1830
+ };
1831
+ };
1832
+ var replaceSlideIdList = (presentationXml, tags) => presentationXml.replace(/<p:sldIdLst>[\s\S]*?<\/p:sldIdLst>/, `<p:sldIdLst>${tags.join("")}</p:sldIdLst>`);
1833
+ /** Resolve a slide by the frozen `slide=` rule: digits = 1-based index, otherwise title. */
1834
+ var resolveSlide = (verb, entries, slideArg) => {
1835
+ if (slideArg === void 0) {
1836
+ const first = entries[0];
1837
+ if (!first) fail$2(verb, "the presentation has no slides");
1838
+ return first;
1839
+ }
1840
+ if (typeof slideArg === "number") {
1841
+ const found = entries[slideArg - 1];
1842
+ if (!found) fail$2(verb, `slide index ${slideArg} is out of range (1-based; ${entries.length} slides)`);
1843
+ return found;
1844
+ }
1845
+ const byTitle = entries.find((e) => e.title === slideArg);
1846
+ if (!byTitle) fail$2(verb, `no slide titled "${slideArg}"`);
1847
+ return byTitle;
1848
+ };
1849
+ var getMaxRidNumber = (relsXml) => {
1850
+ let max = 0;
1851
+ for (const match of relsXml.matchAll(/\bId="rId(\d+)"/g)) max = Math.max(max, Number(match[1]));
1852
+ return max;
1853
+ };
1854
+ var getMaxSlideNumber = (zip) => {
1855
+ let max = 0;
1856
+ for (const name of Object.keys(zip.files)) {
1857
+ const match = name.match(/^ppt\/slides\/slide(\d+)\.xml$/);
1858
+ if (match) max = Math.max(max, Number(match[1]));
1859
+ }
1860
+ return max;
1861
+ };
1862
+ var getMaxSlideId = (entries) => entries.reduce((max, e) => Math.max(max, e.slideId), 255);
1863
+ var ensureSlideContentType = (contentTypesXml, slideNumber) => {
1864
+ const partName = `/ppt/slides/slide${slideNumber}.xml`;
1865
+ if (contentTypesXml.includes(`PartName="${partName}"`)) return contentTypesXml;
1866
+ const override = `<Override PartName="${partName}" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>`;
1867
+ return contentTypesXml.replace("</Types>", `${override}</Types>`);
1868
+ };
1869
+ var setSlideTitle = (slideXml, title) => {
1870
+ if (/<p:cSld\b[^>]*\bname="[^"]*"/.test(slideXml)) return slideXml.replace(/(<p:cSld\b[^>]*\bname=")[^"]*(")/, `$1${escapeXml(title)}$2`);
1871
+ return slideXml;
1872
+ };
1873
+ /** Acquire pptx bytes, converting ODP to PPTX via the engine registry. */
1874
+ var acquirePptx = async (ctx, verb) => {
1875
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
1876
+ if (mime === MIME.PPTX) return ctx.payload.bytes;
1877
+ if (mime === MIME.ODP) {
1878
+ if (!ctx.engines.hasConvert(mime, "pptx")) fail$2(verb, "this input is ODP — an engine that converts it to PPTX is required first, and none is configured. Do not retry this verb on this media in this deployment.");
1879
+ const output = (await ctx.engines.convert({
1880
+ bytes: ctx.payload.bytes,
1881
+ mimeType: mime,
1882
+ filename: ctx.payload.filename,
1883
+ to: "pptx",
1884
+ signal: ctx.signal
1885
+ })).outputs[0];
1886
+ if (!output) fail$2(verb, "normalizing the presentation to PPTX produced no output");
1887
+ return output.bytes;
1888
+ }
1889
+ const reason = unsupportedForMutationReason(mime);
1890
+ if (reason) fail$2(verb, reason);
1891
+ fail$2(verb, `slides operations expect a presentation; the media is ${mime}`);
1892
+ throw new Error("unreachable");
1893
+ };
1894
+ var withPresentation = async (ctx, verb, mutate) => {
1895
+ const bytes = await acquirePptx(ctx, verb);
1896
+ const JSZip = await jszip$1();
1897
+ let zip;
1898
+ try {
1899
+ zip = await JSZip.loadAsync(bytes);
1900
+ } catch (err) {
1901
+ fail$2(verb, `could not open the presentation: ${isError(err) ? err.message : String(err)}`);
1902
+ throw err;
1903
+ }
1904
+ const parsed = await parsePresentation(verb, zip);
1905
+ await mutate(zip, parsed);
1906
+ return {
1907
+ kind: "media",
1908
+ payload: {
1909
+ bytes: await zip.generateAsync({
1910
+ type: "uint8array",
1911
+ compression: "DEFLATE"
1912
+ }),
1913
+ mimeType: MIME.PPTX,
1914
+ filename: replaceExtension(ctx.payload.filename, "pptx")
1915
+ }
1916
+ };
1917
+ };
1918
+ /** `slides.add` — clone the last slide as a template, optionally retitle/position it. */
1919
+ var slidesAddStep = async (ctx) => {
1920
+ const verb = "slides add";
1921
+ const at = argOf(ctx.step, "at");
1922
+ const title = argOf(ctx.step, "title");
1923
+ return withPresentation(ctx, verb, async (zip, parsed) => {
1924
+ if (parsed.entries.length === 0) fail$2(verb, "the presentation has no slides to clone from");
1925
+ const template = parsed.entries[parsed.entries.length - 1];
1926
+ const templatePath = resolveZipPath("ppt/presentation.xml", template.target);
1927
+ const templateXml = await readZipText(verb, zip, templatePath);
1928
+ const newSlideNumber = getMaxSlideNumber(zip) + 1;
1929
+ const newSlidePath = `ppt/slides/slide${newSlideNumber}.xml`;
1930
+ zip.file(newSlidePath, title ? setSlideTitle(templateXml, title) : templateXml);
1931
+ const templateNum = templatePath.match(/slide(\d+)\.xml$/)?.[1];
1932
+ const templateRels = templateNum ? zip.file(`ppt/slides/_rels/slide${templateNum}.xml.rels`) : null;
1933
+ if (templateRels) {
1934
+ const relsXml = await templateRels.async("text");
1935
+ zip.file(`ppt/slides/_rels/slide${newSlideNumber}.xml.rels`, relsXml.replace(/<Relationship\b[^>]*Type="[^"]*notesSlide"[^>]*\/>/g, ""));
1936
+ }
1937
+ const newRid = `rId${getMaxRidNumber(parsed.relsXml) + 1}`;
1938
+ const newSldTag = `<p:sldId id="${getMaxSlideId(parsed.entries) + 1}" r:id="${newRid}"/>`;
1939
+ const relNode = `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newSlideNumber}.xml"/>`;
1940
+ zip.file("ppt/_rels/presentation.xml.rels", parsed.relsXml.replace("</Relationships>", `${relNode}</Relationships>`));
1941
+ const insertAt = at !== void 0 ? Math.min(Math.max(1, at), parsed.entries.length + 1) : parsed.entries.length + 1;
1942
+ const tags = parsed.entries.map((e) => e.sldIdTag);
1943
+ tags.splice(insertAt - 1, 0, newSldTag);
1944
+ zip.file("ppt/presentation.xml", replaceSlideIdList(parsed.presentationXml, tags));
1945
+ const contentTypes = await readZipText(verb, zip, "[Content_Types].xml");
1946
+ zip.file("[Content_Types].xml", ensureSlideContentType(contentTypes, newSlideNumber));
1947
+ });
1948
+ };
1949
+ /** `slides.update_text` — replace text in the first node or a named placeholder shape. */
1950
+ var slidesUpdateTextStep = async (ctx) => {
1951
+ const verb = "slides update_text";
1952
+ const text = argOf(ctx.step, "text");
1953
+ const placeholder = argOf(ctx.step, "placeholder");
1954
+ return withPresentation(ctx, verb, async (zip, parsed) => {
1955
+ const slidePath = resolveZipPath("ppt/presentation.xml", resolveSlide(verb, parsed.entries, argOf(ctx.step, "slide")).target);
1956
+ const slideXml = await readZipText(verb, zip, slidePath);
1957
+ let updated = slideXml;
1958
+ let changed = false;
1959
+ if (placeholder) {
1960
+ let placeholderFound = false;
1961
+ updated = slideXml.replace(/<p:sp\b[\s\S]*?<\/p:sp>/g, (shapeXml) => {
1962
+ if (!new RegExp(`<p:cNvPr\\b[^>]*\\bname="${escapeRegex(placeholder)}"[^>]*>`, "i").test(shapeXml)) return shapeXml;
1963
+ placeholderFound = true;
1964
+ return shapeXml.replace(/<a:t>[\s\S]*?<\/a:t>/, () => {
1965
+ changed = true;
1966
+ return `<a:t>${escapeXml(text)}</a:t>`;
1967
+ });
1968
+ });
1969
+ if (!placeholderFound) fail$2(verb, `no placeholder named "${placeholder}" on the selected slide`);
1970
+ } else updated = slideXml.replace(/<a:t>[\s\S]*?<\/a:t>/, () => {
1971
+ changed = true;
1972
+ return `<a:t>${escapeXml(text)}</a:t>`;
1973
+ });
1974
+ if (!changed) fail$2(verb, "no text node found on the selected slide");
1975
+ zip.file(slidePath, updated);
1976
+ });
1977
+ };
1978
+ /** `slides.update_table` — set table cell text by 1-based row/col. */
1979
+ var slidesUpdateTableStep = async (ctx) => {
1980
+ const verb = "slides update_table";
1981
+ const updates = ctx.step.args.updates;
1982
+ if (!Array.isArray(updates) || updates.length === 0) fail$2(verb, `updates must be a JSON array: updates='[{"row":1,"col":2,"value":"x"}]'`);
1983
+ return withPresentation(ctx, verb, async (zip, parsed) => {
1984
+ const slidePath = resolveZipPath("ppt/presentation.xml", resolveSlide(verb, parsed.entries, argOf(ctx.step, "slide")).target);
1985
+ let slideXml = await readZipText(verb, zip, slidePath);
1986
+ const tableMatch = slideXml.match(/<a:tbl>[\s\S]*?<\/a:tbl>/);
1987
+ if (!tableMatch) fail$2(verb, "no table found on the selected slide");
1988
+ let tableXml = tableMatch[0];
1989
+ const rowMatches = tableXml.match(/<a:tr\b[\s\S]*?<\/a:tr>/g) ?? [];
1990
+ for (const raw of updates) {
1991
+ const row = Number(raw.row);
1992
+ const col = Number(raw.col);
1993
+ const value = String(raw.value ?? "");
1994
+ const rowIndex = row - 1;
1995
+ const colIndex = col - 1;
1996
+ if (rowIndex < 0 || colIndex < 0 || rowIndex >= rowMatches.length) fail$2(verb, `table cell out of range: r${row}c${col} (1-based)`);
1997
+ const rowXml = rowMatches[rowIndex];
1998
+ const cellMatches = rowXml.match(/<a:tc\b[\s\S]*?<\/a:tc>/g) ?? [];
1999
+ if (colIndex >= cellMatches.length) fail$2(verb, `table cell out of range: r${row}c${col} (1-based)`);
2000
+ const cellXml = cellMatches[colIndex];
2001
+ if (!/<a:t>/.test(cellXml)) fail$2(verb, `cell r${row}c${col} has no text node`);
2002
+ cellMatches[colIndex] = cellXml.replace(/<a:t>[\s\S]*?<\/a:t>/, `<a:t>${escapeXml(value)}</a:t>`);
2003
+ rowMatches[rowIndex] = rowXml.replace(/<a:tc\b[\s\S]*?<\/a:tc>/g, () => cellMatches.shift());
2004
+ }
2005
+ tableXml = tableXml.replace(/<a:tr\b[\s\S]*?<\/a:tr>/g, () => rowMatches.shift());
2006
+ slideXml = slideXml.replace(/<a:tbl>[\s\S]*?<\/a:tbl>/, tableXml);
2007
+ zip.file(slidePath, slideXml);
2008
+ });
2009
+ };
2010
+ /** `slides.update_image` — replace an image part's bytes with another media (`with=@id`). */
2011
+ var slidesUpdateImageStep = async (ctx) => {
2012
+ const verb = "slides update_image";
2013
+ const ref = ctx.step.args.with;
2014
+ const placeholder = argOf(ctx.step, "placeholder");
2015
+ if (ref.kind !== "id") fail$2(verb, "builder refs are not supported here; use an @id ref");
2016
+ const replacement = await ctx.resolveRef(ref.id);
2017
+ return withPresentation(ctx, verb, async (zip, parsed) => {
2018
+ const slidePath = resolveZipPath("ppt/presentation.xml", resolveSlide(verb, parsed.entries, argOf(ctx.step, "slide")).target);
2019
+ const slideXml = await readZipText(verb, zip, slidePath);
2020
+ const slideNum = slidePath.match(/slide(\d+)\.xml$/)?.[1];
2021
+ if (!slideNum) fail$2(verb, "unable to resolve the slide number (corrupt PPTX?)");
2022
+ const relsPath = `ppt/slides/_rels/slide${slideNum}.xml.rels`;
2023
+ const relsXml = await readZipText(verb, zip, relsPath);
2024
+ let imagePartPath = null;
2025
+ if (placeholder) {
2026
+ for (const picXml of slideXml.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) ?? []) {
2027
+ if (!new RegExp(`<p:cNvPr\\b[^>]*\\bname="${escapeRegex(placeholder)}"[^>]*>`, "i").test(picXml)) continue;
2028
+ const rid = picXml.match(/<a:blip\b[^>]*\br:embed="([^"]+)"/i)?.[1];
2029
+ if (rid) {
2030
+ const relTarget = parseRelationshipTarget(relsXml, rid);
2031
+ if (relTarget) imagePartPath = resolveZipPath(relsPath, relTarget);
2032
+ }
2033
+ break;
2034
+ }
2035
+ if (!imagePartPath) fail$2(verb, `no image placeholder named "${placeholder}" on the selected slide`);
2036
+ } else {
2037
+ const fallback = relsXml.match(/<Relationship\b[^>]*Type="[^"]*\/image"[^>]*Target="([^"]+)"[^>]*>/i);
2038
+ if (fallback) imagePartPath = resolveZipPath(relsPath, fallback[1]);
2039
+ }
2040
+ if (!imagePartPath) fail$2(verb, "no image relationship found on the selected slide");
2041
+ zip.file(imagePartPath, replacement.bytes);
2042
+ });
2043
+ };
2044
+ /** `slides.update_chart` — overwrite the chart's cached values in document order. */
2045
+ var slidesUpdateChartStep = async (ctx) => {
2046
+ const verb = "slides update_chart";
2047
+ const data = ctx.step.args.data;
2048
+ if (!Array.isArray(data) || data.length === 0) fail$2(verb, `data must be a non-empty JSON array of arrays: data='[["Q1",10]]'`);
2049
+ return withPresentation(ctx, verb, async (zip, parsed) => {
2050
+ const slideNum = resolveZipPath("ppt/presentation.xml", resolveSlide(verb, parsed.entries, argOf(ctx.step, "slide")).target).match(/slide(\d+)\.xml$/)?.[1];
2051
+ if (!slideNum) fail$2(verb, "unable to resolve the slide number (corrupt PPTX?)");
2052
+ const relsPath = `ppt/slides/_rels/slide${slideNum}.xml.rels`;
2053
+ const chartRel = (await readZipText(verb, zip, relsPath)).match(/<Relationship\b[^>]*Type="[^"]*\/chart"[^>]*Target="([^"]+)"[^>]*>/i);
2054
+ if (!chartRel) fail$2(verb, "no chart found on the selected slide");
2055
+ const chartPath = resolveZipPath(relsPath, chartRel[1]);
2056
+ let chartXml = await readZipText(verb, zip, chartPath);
2057
+ const values = data.flat().map((v) => String(v));
2058
+ let replaced = 0;
2059
+ chartXml = chartXml.replace(/<c:v>[\s\S]*?<\/c:v>/g, (full) => {
2060
+ if (replaced >= values.length) return full;
2061
+ const next = `<c:v>${escapeXml(values[replaced])}</c:v>`;
2062
+ replaced += 1;
2063
+ return next;
2064
+ });
2065
+ if (replaced === 0) fail$2(verb, "the chart has no cached values to update");
2066
+ zip.file(chartPath, chartXml);
2067
+ });
2068
+ };
2069
+ /** `slides.delete` — remove slides by 1-based index (cannot remove all). */
2070
+ var slidesDeleteStep = async (ctx) => {
2071
+ const verb = "slides delete";
2072
+ const slides = ctx.step.args.slides;
2073
+ return withPresentation(ctx, verb, async (zip, parsed) => {
2074
+ const unique = [...new Set(slides)];
2075
+ for (const index of unique) if (!Number.isInteger(index) || index < 1 || index > parsed.entries.length) fail$2(verb, `slide index ${index} is out of range (1-based; ${parsed.entries.length} slides)`);
2076
+ const toDelete = new Set(unique);
2077
+ const kept = parsed.entries.filter((_, idx) => !toDelete.has(idx + 1));
2078
+ if (kept.length === 0) fail$2(verb, "cannot delete every slide in the presentation");
2079
+ zip.file("ppt/presentation.xml", replaceSlideIdList(parsed.presentationXml, kept.map((e) => e.sldIdTag)));
2080
+ let relsXml = parsed.relsXml;
2081
+ parsed.entries.forEach((entry, idx) => {
2082
+ if (!toDelete.has(idx + 1)) return;
2083
+ const targetPath = resolveZipPath("ppt/presentation.xml", entry.target);
2084
+ zip.remove(targetPath);
2085
+ const slideNum = targetPath.match(/slide(\d+)\.xml$/)?.[1];
2086
+ if (slideNum) zip.remove(`ppt/slides/_rels/slide${slideNum}.xml.rels`);
2087
+ relsXml = relsXml.replace(new RegExp(`<Relationship\\b[^>]*Id="${entry.rid}"[^>]*/>`, "g"), "");
2088
+ });
2089
+ zip.file("ppt/_rels/presentation.xml.rels", relsXml);
2090
+ });
2091
+ };
2092
+ /** `slides.reorder` — every slide exactly once, by 1-based index. */
2093
+ var slidesReorderStep = async (ctx) => {
2094
+ const verb = "slides reorder";
2095
+ const order = ctx.step.args.order;
2096
+ return withPresentation(ctx, verb, async (zip, parsed) => {
2097
+ if (order.length !== parsed.entries.length) fail$2(verb, `order must list all ${parsed.entries.length} slides exactly once`);
2098
+ const seen = /* @__PURE__ */ new Set();
2099
+ const reordered = [];
2100
+ for (const index of order) {
2101
+ if (!Number.isInteger(index) || index < 1 || index > parsed.entries.length) fail$2(verb, `slide index ${index} is out of range (1-based)`);
2102
+ if (seen.has(index)) fail$2(verb, `duplicate slide index: ${index}`);
2103
+ seen.add(index);
2104
+ reordered.push(parsed.entries[index - 1]);
2105
+ }
2106
+ zip.file("ppt/presentation.xml", replaceSlideIdList(parsed.presentationXml, reordered.map((e) => e.sldIdTag)));
2107
+ });
2108
+ };
2109
+ /** `slides.duplicate` — copy a slide, optionally inserting at a position. */
2110
+ var slidesDuplicateStep = async (ctx) => {
2111
+ const verb = "slides duplicate";
2112
+ const slide = argOf(ctx.step, "slide");
2113
+ const at = argOf(ctx.step, "at");
2114
+ return withPresentation(ctx, verb, async (zip, parsed) => {
2115
+ if (!Number.isInteger(slide) || slide < 1 || slide > parsed.entries.length) fail$2(verb, `slide index ${slide} is out of range (1-based; ${parsed.entries.length} slides)`);
2116
+ const source = parsed.entries[slide - 1];
2117
+ const sourcePath = resolveZipPath("ppt/presentation.xml", source.target);
2118
+ const sourceXml = await readZipText(verb, zip, sourcePath);
2119
+ const newSlideNumber = getMaxSlideNumber(zip) + 1;
2120
+ zip.file(`ppt/slides/slide${newSlideNumber}.xml`, sourceXml);
2121
+ const sourceNum = sourcePath.match(/slide(\d+)\.xml$/)?.[1];
2122
+ if (sourceNum) {
2123
+ const sourceRels = zip.file(`ppt/slides/_rels/slide${sourceNum}.xml.rels`);
2124
+ if (sourceRels) {
2125
+ const relsXml = await sourceRels.async("text");
2126
+ zip.file(`ppt/slides/_rels/slide${newSlideNumber}.xml.rels`, relsXml.replace(/<Relationship\b[^>]*Type="[^"]*notesSlide"[^>]*\/>/g, ""));
2127
+ }
2128
+ }
2129
+ const newRid = `rId${getMaxRidNumber(parsed.relsXml) + 1}`;
2130
+ const newTag = `<p:sldId id="${getMaxSlideId(parsed.entries) + 1}" r:id="${newRid}"/>`;
2131
+ const relNode = `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newSlideNumber}.xml"/>`;
2132
+ zip.file("ppt/_rels/presentation.xml.rels", parsed.relsXml.replace("</Relationships>", `${relNode}</Relationships>`));
2133
+ const targetPos = at !== void 0 ? Math.min(Math.max(1, at), parsed.entries.length + 1) : slide + 1;
2134
+ const tags = parsed.entries.map((e) => e.sldIdTag);
2135
+ tags.splice(targetPos - 1, 0, newTag);
2136
+ zip.file("ppt/presentation.xml", replaceSlideIdList(parsed.presentationXml, tags));
2137
+ const contentTypes = await readZipText(verb, zip, "[Content_Types].xml");
2138
+ zip.file("[Content_Types].xml", ensureSlideContentType(contentTypes, newSlideNumber));
2139
+ });
2140
+ };
2141
+ /** The slides step registry fragment, keyed by canonical verb id. */
2142
+ var SLIDES_STEPS = [
2143
+ ["slides.add", slidesAddStep],
2144
+ ["slides.update_text", slidesUpdateTextStep],
2145
+ ["slides.update_table", slidesUpdateTableStep],
2146
+ ["slides.update_image", slidesUpdateImageStep],
2147
+ ["slides.update_chart", slidesUpdateChartStep],
2148
+ ["slides.delete", slidesDeleteStep],
2149
+ ["slides.reorder", slidesReorderStep],
2150
+ ["slides.duplicate", slidesDuplicateStep]
2151
+ ];
2152
+ //#endregion
2153
+ //#region src/batteries/media/steps/ingest.ts
2154
+ /**
2155
+ * Ingest-depth step extensions: the unified `extract.text` format dispatch (PDF text layer,
2156
+ * DOCX via mammoth, XLSX via ExcelJS, ODT/ODS via ODF XML, PPTX via slide XML, images via the
2157
+ * OCR engine) and format-aware `extract.metadata`.
2158
+ *
2159
+ * @remarks
2160
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. Ported from the source server's
2161
+ * four extractors, collapsed into one verb that routes on the resolved media's format family
2162
+ * (the single biggest order-out-of-chaos win — the model says `extract text` and doesn't care
2163
+ * what the container is). OCR runs through the configured `ocr` engine when the input is an
2164
+ * image, or when a PDF has no text layer with `ocr=auto`, or always with `ocr=force`.
2165
+ */
2166
+ var zipPromise;
2167
+ var jszip = () => {
2168
+ zipPromise ??= import("jszip").then((m) => "default" in m ? m.default : m);
2169
+ return zipPromise;
2170
+ };
2171
+ var fail$1 = (verb, message) => {
2172
+ throw new E_MEDIA_STEP_FAILED([verb, message]);
2173
+ };
2174
+ var VERB = "extract text";
2175
+ var extractPdfText = async (ctx) => {
2176
+ const { PDFParse } = await import("pdf-parse");
2177
+ try {
2178
+ return (await new PDFParse({ data: ctx.payload.bytes }).getText()).text ?? "";
2179
+ } catch (err) {
2180
+ fail$1(VERB, `could not read the PDF: ${isError(err) ? err.message : String(err)}`);
2181
+ throw err;
2182
+ }
2183
+ };
2184
+ var extractDocxText = async (ctx) => {
2185
+ const mod = await import("mammoth");
2186
+ const mammoth = "extractRawText" in mod ? mod : mod.default;
2187
+ const bytes = ctx.payload.bytes;
2188
+ const input = typeof globalThis.Buffer !== "undefined" ? { buffer: globalThis.Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength) } : { arrayBuffer: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) };
2189
+ return (await mammoth.extractRawText(input)).value;
2190
+ };
2191
+ var extractXlsxText = async (ctx) => {
2192
+ const mod = await import("exceljs");
2193
+ const wb = new ("default" in mod ? mod.default : mod).Workbook();
2194
+ await wb.xlsx.load(ctx.payload.bytes.buffer.slice(ctx.payload.bytes.byteOffset, ctx.payload.bytes.byteOffset + ctx.payload.bytes.byteLength));
2195
+ const lines = [];
2196
+ for (const ws of wb.worksheets) {
2197
+ lines.push(`# ${ws.name}`);
2198
+ ws.eachRow({ includeEmpty: false }, (row) => {
2199
+ const cells = [];
2200
+ row.eachCell({ includeEmpty: false }, (cell) => {
2201
+ cells.push(String(cell.value ?? ""));
2202
+ });
2203
+ lines.push(cells.join(" "));
2204
+ });
2205
+ }
2206
+ return lines.join("\n");
2207
+ };
2208
+ /** ODF content.xml → visible text (ported transformation order). */
2209
+ var extractOdfXmlText = (xml) => xml.replace(/<\/text:p>/g, "\n").replace(/<\/text:h>/g, "\n").replace(/<\/table:table-row>/g, "\n").replace(/<text:tab[^>]*\/>/g, " ").replace(/<text:line-break[^>]*\/>/g, "\n").replace(/<[^>]+>/g, "").replace(/&apos;/g, "'").replace(/&quot;/g, "\"").replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
2210
+ var extractOdfText = async (ctx) => {
2211
+ const content = (await (await jszip()).loadAsync(ctx.payload.bytes)).file("content.xml");
2212
+ if (!content) fail$1(VERB, "the ODF archive is missing content.xml (corrupt file?)");
2213
+ return extractOdfXmlText(await content.async("text"));
2214
+ };
2215
+ var extractPptxText = async (ctx) => {
2216
+ const zip = await (await jszip()).loadAsync(ctx.payload.bytes);
2217
+ const slideNames = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)).sort((a, b) => {
2218
+ return Number(a.match(/slide(\d+)/)?.[1] ?? 0) - Number(b.match(/slide(\d+)/)?.[1] ?? 0);
2219
+ });
2220
+ if (slideNames.length === 0) fail$1(VERB, "the presentation has no slides");
2221
+ const parts = [];
2222
+ for (const name of slideNames) {
2223
+ const texts = ((await zip.file(name).async("text")).match(/<a:t>([^<]*)<\/a:t>/g) ?? []).map((t) => t.replace(/<[^>]+>/g, "").replace(/&apos;/g, "'").replace(/&quot;/g, "\"").replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&"));
2224
+ parts.push(texts.join("\n"));
2225
+ }
2226
+ return parts.join("\n\n");
2227
+ };
2228
+ var runOcr = async (ctx) => {
2229
+ const out = argOf(ctx.step, "ocr_out") ?? "txt";
2230
+ if (!ctx.engines.hasConvert(ctx.payload.mimeType, out)) fail$1(VERB, `this input requires OCR, and no engine that converts ${ctx.payload.mimeType} to "${out}" is configured. Do not retry this verb on this media in this deployment.`);
2231
+ const langArg = ctx.step.args.lang;
2232
+ const languages = langArg === void 0 ? void 0 : Array.isArray(langArg) ? langArg : [langArg];
2233
+ const output = (await ctx.engines.convert({
2234
+ bytes: ctx.payload.bytes,
2235
+ mimeType: ctx.payload.mimeType,
2236
+ filename: ctx.payload.filename,
2237
+ to: out,
2238
+ options: { languages },
2239
+ signal: ctx.signal
2240
+ })).outputs[0];
2241
+ if (!output) fail$1(VERB, "OCR produced no output");
2242
+ return new TextDecoder().decode(output.bytes);
2243
+ };
2244
+ /**
2245
+ * `extract.text` — full format dispatch. Routes by MIME, with OCR engine fallback for images
2246
+ * and force-OCR for any input via `ocr=force`.
2247
+ */
2248
+ var extractTextDeepStep = async (ctx) => {
2249
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
2250
+ const ocrMode = argOf(ctx.step, "ocr") ?? "auto";
2251
+ const asData = (text) => ({
2252
+ kind: "data",
2253
+ data: text,
2254
+ asText: text
2255
+ });
2256
+ if (mime.startsWith("image/")) return asData(await runOcr(ctx));
2257
+ if (ocrMode === "force") return asData(await runOcr(ctx));
2258
+ if (mime === MIME.PDF) {
2259
+ const text = await extractPdfText(ctx);
2260
+ if (text.trim().length === 0 && ocrMode === "auto") {
2261
+ if (!ctx.engines.hasConvert(mime, "txt")) fail$1(VERB, "this PDF has no text layer (scanned?). An engine that converts application/pdf to text (OCR) is required to read it, and none is configured. Do not retry this verb on this media in this deployment.");
2262
+ return asData(await runOcr(ctx));
2263
+ }
2264
+ return asData(text);
2265
+ }
2266
+ if (mime === MIME.DOCX || mime === MIME.DOC) return asData(await extractDocxText(ctx));
2267
+ if (mime === MIME.XLSX) return asData(await extractXlsxText(ctx));
2268
+ if (mime === MIME.ODT || mime === MIME.ODS || mime === MIME.ODP) return asData(await extractOdfText(ctx));
2269
+ if (mime === MIME.PPTX) return asData(await extractPptxText(ctx));
2270
+ if (mime.startsWith("text/") || mime === MIME.JSON) return asData(decodeText(ctx.payload.bytes));
2271
+ fail$1(VERB, `text extraction for ${mime} is not supported in this build. For PPT/legacy formats: convert to=pptx | extract text (requires a convert engine).`);
2272
+ throw new Error("unreachable");
2273
+ };
2274
+ /** `extract.metadata` — format-aware metadata (page counts, sheet/slide inventories). */
2275
+ var extractMetadataDeepStep = async (ctx) => {
2276
+ const mime = ctx.payload.mimeType.toLowerCase().split(";")[0].trim();
2277
+ const meta = {
2278
+ filename: ctx.payload.filename,
2279
+ mime_type: ctx.payload.mimeType,
2280
+ size_bytes: ctx.payload.bytes.byteLength,
2281
+ family: familyOf(ctx.payload.mimeType)
2282
+ };
2283
+ try {
2284
+ if (mime === MIME.PDF) {
2285
+ const { PDFDocument } = await import("pdf-lib");
2286
+ const doc = await PDFDocument.load(ctx.payload.bytes, { ignoreEncryption: true });
2287
+ meta.page_count = doc.getPageCount();
2288
+ meta.title = doc.getTitle() ?? void 0;
2289
+ meta.author = doc.getAuthor() ?? void 0;
2290
+ } else if (mime === MIME.XLSX) {
2291
+ const mod = await import("exceljs");
2292
+ const wb = new ("default" in mod ? mod.default : mod).Workbook();
2293
+ await wb.xlsx.load(ctx.payload.bytes.buffer.slice(ctx.payload.bytes.byteOffset, ctx.payload.bytes.byteOffset + ctx.payload.bytes.byteLength));
2294
+ meta.sheets = wb.worksheets.map((ws) => ({
2295
+ name: ws.name,
2296
+ rows: ws.rowCount
2297
+ }));
2298
+ } else if (mime === MIME.PPTX) {
2299
+ const zip = await (await jszip()).loadAsync(ctx.payload.bytes);
2300
+ meta.slide_count = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)).length;
2301
+ }
2302
+ } catch {}
2303
+ return {
2304
+ kind: "data",
2305
+ data: meta,
2306
+ asText: JSON.stringify(meta)
2307
+ };
2308
+ };
2309
+ /** The ingest step registry fragment (overrides the Phase 0 native-text-only versions). */
2310
+ var INGEST_STEPS = [["extract.text", extractTextDeepStep], ["extract.metadata", extractMetadataDeepStep]];
2311
+ //#endregion
2312
+ //#region src/batteries/media/registry.ts
2313
+ /**
2314
+ * The engine registry: capability-filtered, middleware-arbitrated, ordered dispatch over the
2315
+ * pipeline's engine array.
2316
+ *
2317
+ * @remarks
2318
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry (re-exported through the barrel).
2319
+ * One selection rule everywhere: gather every engine whose declarations match the request
2320
+ * (capability filter), run the consumer's selection onion (stages may exclude or reorder
2321
+ * candidates, never add), then the first survivor in supply order wins.
2322
+ *
2323
+ * Convert dispatch additionally computes multi-hop paths: when no single engine declares a
2324
+ * direct edge for the requested (input, target) pair, a breadth-first search over the format
2325
+ * graph finds the shortest chain of hops (capped at {@link MAX_HOPS}), with supply order
2326
+ * breaking ties at equal length. Pathfinding explores capability declarations only — cheap
2327
+ * and synchronous; the selection onion runs once per executed hop with real bytes in hand.
2328
+ */
2329
+ /** The maximum number of conversion hops a computed path may take. */
2330
+ var MAX_HOPS = 3;
2331
+ /**
2332
+ * Format tokens that are valid conversion endpoints but never intermediate path nodes.
2333
+ * Without this rule the pathfinder would happily route `docx → txt → …` (lossy garbage in,
2334
+ * garbage out) or auto-chain `audio → pcm → txt` (skipping the resample the transcription
2335
+ * step performs between those legs).
2336
+ */
2337
+ var TERMINAL_TOKENS = new Set([
2338
+ "txt",
2339
+ "json",
2340
+ "hocr",
2341
+ "srt",
2342
+ "vtt",
2343
+ "md",
2344
+ "csv",
2345
+ "pcm",
2346
+ "images"
2347
+ ]);
2348
+ /** Normalize a MIME type for comparison (lowercase, parameters stripped). */
2349
+ var normalizeMime = (mimeType) => mimeType.toLowerCase().split(";")[0].trim();
2350
+ /** `true` when `mime` matches `pattern` (exact, or a `family/*` wildcard). */
2351
+ var matchesMime = (pattern, mime) => {
2352
+ const p = pattern.toLowerCase();
2353
+ if (p.endsWith("/*")) return mime.startsWith(p.slice(0, -1));
2354
+ return p === mime;
2355
+ };
2356
+ /** `true` when any pattern in `patterns` matches `mime`. */
2357
+ var matchesAny = (patterns, mime) => patterns.some((p) => matchesMime(p, mime));
2358
+ /** The mutate operations requested by a fused request. */
2359
+ var requestedOps = (request) => {
2360
+ const ops = [];
2361
+ if (request.resize) ops.push("resize");
2362
+ if (request.rotate !== void 0) ops.push("rotate");
2363
+ if (request.flip) ops.push("flip");
2364
+ if (request.stripMetadata) ops.push("strip_metadata");
2365
+ return ops;
2366
+ };
2367
+ /**
2368
+ * Build the registry over a resolved, validated engine array.
2369
+ *
2370
+ * @param engines - The engines, in supply (priority) order.
2371
+ * @param selection - Optional selection-middleware stages arbitrating multi-candidate dispatches.
2372
+ * @returns The registry.
2373
+ */
2374
+ var buildEngineRegistry = (engines, selection = []) => {
2375
+ const convertCandidates = (fromMime, to) => {
2376
+ const mime = normalizeMime(fromMime);
2377
+ const out = [];
2378
+ for (const engine of engines) for (const capability of engine.converts ?? []) if (capability.to.includes(to) && matchesAny(capability.from, mime)) {
2379
+ out.push({
2380
+ engine,
2381
+ capability
2382
+ });
2383
+ break;
2384
+ }
2385
+ return out;
2386
+ };
2387
+ const mutateCandidates = (request) => {
2388
+ const mime = normalizeMime(request.mimeType);
2389
+ const ops = requestedOps(request);
2390
+ const out = [];
2391
+ for (const engine of engines) for (const capability of engine.mutates ?? []) {
2392
+ if (!matchesAny(capability.over, mime)) continue;
2393
+ if (!ops.every((op) => capability.ops.includes(op))) continue;
2394
+ if (request.format && !capability.encodes.includes(request.format.to)) continue;
2395
+ out.push({
2396
+ engine,
2397
+ capability
2398
+ });
2399
+ break;
2400
+ }
2401
+ return out;
2402
+ };
2403
+ const arbitrate = async (kind, request, candidates) => {
2404
+ if (candidates.length === 0) return void 0;
2405
+ if (selection.length === 0 || candidates.length === 1) return candidates[0];
2406
+ const ctx = {
2407
+ kind,
2408
+ request,
2409
+ candidates: candidates.map((c) => c.engine)
2410
+ };
2411
+ const mw = new Middleware();
2412
+ for (const fn of selection) mw.add(fn);
2413
+ let failed;
2414
+ await mw.runner().errorHandler(async (error) => {
2415
+ failed = error;
2416
+ }).finalHandler(async () => {}).run((fn, next) => Promise.resolve(fn(ctx, next)));
2417
+ if (failed !== void 0) throw failed;
2418
+ for (const engine of ctx.candidates) {
2419
+ const survivor = candidates.find((c) => c.engine === engine);
2420
+ if (survivor) return survivor;
2421
+ }
2422
+ };
2423
+ const findPath = (fromMime, target) => {
2424
+ const start = normalizeMime(fromMime);
2425
+ const queue = [{
2426
+ mime: start,
2427
+ path: []
2428
+ }];
2429
+ const visited = new Set([start]);
2430
+ while (queue.length > 0) {
2431
+ const { mime, path } = queue.shift();
2432
+ if (path.length >= MAX_HOPS) continue;
2433
+ if (convertCandidates(mime, target).length > 0) return [...path, {
2434
+ fromMime: mime,
2435
+ to: target
2436
+ }];
2437
+ for (const engine of engines) for (const capability of engine.converts ?? []) {
2438
+ if (!matchesAny(capability.from, mime)) continue;
2439
+ for (const to of capability.to) {
2440
+ if (TERMINAL_TOKENS.has(to)) continue;
2441
+ const nextMime = EXT_TO_MIME[to];
2442
+ if (!nextMime || visited.has(nextMime)) continue;
2443
+ visited.add(nextMime);
2444
+ queue.push({
2445
+ mime: nextMime,
2446
+ path: [...path, {
2447
+ fromMime: mime,
2448
+ to
2449
+ }]
2450
+ });
2451
+ }
2452
+ }
2453
+ }
2454
+ };
2455
+ const hasConvert = (from, to) => {
2456
+ for (const engine of engines) for (const capability of engine.converts ?? []) {
2457
+ if (from !== void 0 && !matchesAny(capability.from, normalizeMime(from))) continue;
2458
+ if (to !== void 0 && !capability.to.includes(to)) continue;
2459
+ return true;
2460
+ }
2461
+ return false;
2462
+ };
2463
+ const convertTargets = (fromMime) => {
2464
+ const reachable = [];
2465
+ const seen = /* @__PURE__ */ new Set();
2466
+ let frontier = [normalizeMime(fromMime)];
2467
+ const visitedMimes = new Set(frontier);
2468
+ for (let hop = 0; hop < MAX_HOPS && frontier.length > 0; hop++) {
2469
+ const next = [];
2470
+ for (const mime of frontier) for (const engine of engines) for (const capability of engine.converts ?? []) {
2471
+ if (!matchesAny(capability.from, mime)) continue;
2472
+ for (const to of capability.to) {
2473
+ if (!seen.has(to)) {
2474
+ seen.add(to);
2475
+ reachable.push(to);
2476
+ }
2477
+ if (TERMINAL_TOKENS.has(to)) continue;
2478
+ const nextMime = EXT_TO_MIME[to];
2479
+ if (nextMime && !visitedMimes.has(nextMime)) {
2480
+ visitedMimes.add(nextMime);
2481
+ next.push(nextMime);
2482
+ }
2483
+ }
2484
+ }
2485
+ frontier = next;
2486
+ }
2487
+ return reachable;
2488
+ };
2489
+ const ids = () => engines.map((e) => e.id).join(", ") || "(none)";
2490
+ const convert = async (request) => {
2491
+ const path = findPath(request.mimeType, request.to);
2492
+ if (!path) {
2493
+ const supported = convertTargets(request.mimeType);
2494
+ throw new Error(`no configured engine (or computed path) can convert ${request.mimeType} to "${request.to}"; reachable targets: ${supported.join(", ") || "(none)"} (engines: ${ids()})`);
2495
+ }
2496
+ let bytes = request.bytes;
2497
+ let mimeType = request.mimeType;
2498
+ let result;
2499
+ for (const hop of path) {
2500
+ const candidates = convertCandidates(mimeType, hop.to);
2501
+ const chosen = await arbitrate("convert", {
2502
+ mimeType,
2503
+ filename: request.filename,
2504
+ bytes,
2505
+ to: hop.to
2506
+ }, candidates);
2507
+ if (!chosen) throw new Error(`all engines capable of converting ${mimeType} to "${hop.to}" (${candidates.map((c) => c.engine.id).join(", ")}) were excluded by selection middleware`);
2508
+ result = await chosen.capability.convert({
2509
+ bytes,
2510
+ mimeType,
2511
+ filename: request.filename,
2512
+ to: hop.to,
2513
+ options: request.options,
2514
+ signal: request.signal
2515
+ });
2516
+ const first = result.outputs[0];
2517
+ if (!first) throw new Error(`engine "${chosen.engine.id}" converted ${mimeType} to "${hop.to}" but produced no outputs`);
2518
+ bytes = first.bytes;
2519
+ mimeType = first.mimeType;
2520
+ }
2521
+ return result;
2522
+ };
2523
+ const mutate = async (request) => {
2524
+ const candidates = mutateCandidates(request);
2525
+ if (candidates.length === 0) {
2526
+ const allOps = /* @__PURE__ */ new Set();
2527
+ const allEncodes = /* @__PURE__ */ new Set();
2528
+ for (const engine of engines) for (const capability of engine.mutates ?? []) {
2529
+ for (const op of capability.ops) allOps.add(op);
2530
+ for (const enc of capability.encodes) allEncodes.add(enc);
2531
+ }
2532
+ const wanted = requestedOps(request).join(", ") || "(none)";
2533
+ const encodeNote = request.format ? `; requested encoding "${request.format.to}" — declared encodings: ${[...allEncodes].join(", ") || "(none)"}. A different engine (e.g. sharp) may support it` : "";
2534
+ throw new Error(`no configured engine can mutate ${request.mimeType} with ops [${wanted}]${encodeNote} (engines: ${ids()})`);
2535
+ }
2536
+ const chosen = await arbitrate("mutate", {
2537
+ mimeType: request.mimeType,
2538
+ filename: "",
2539
+ bytes: request.bytes,
2540
+ to: request.format?.to,
2541
+ ops: requestedOps(request)
2542
+ }, candidates);
2543
+ if (!chosen) throw new Error(`all engines capable of this mutate (${candidates.map((c) => c.engine.id).join(", ")}) were excluded by selection middleware`);
2544
+ return chosen.capability.mutate(request);
2545
+ };
2546
+ return {
2547
+ engines,
2548
+ hasConvert,
2549
+ hasMutate: () => engines.some((e) => (e.mutates ?? []).length > 0),
2550
+ convertTargets,
2551
+ convert,
2552
+ mutate
2553
+ };
2554
+ };
2555
+ //#endregion
2556
+ //#region src/batteries/media/steps/image_audio.ts
2557
+ /**
2558
+ * `image.*` and `audio.*` step implementations: fused raster transforms via the
2559
+ * imageTransform engine, and transcription via the audioDecode + asr engines.
2560
+ *
2561
+ * @remarks
2562
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. The split image verbs
2563
+ * (`resize`/`format`/`rotate`/`flip`/`strip_metadata`) read pipe-naturally, and the runtime
2564
+ * fuses **adjacent** image steps into a single engine invocation so a resize→format→rotate
2565
+ * chain costs one decode/encode (frozen design 0.13). Fusion happens here at execution time:
2566
+ * each image step folds its args into a shared fused request stored on the execution stash;
2567
+ * the LAST image step in a run performs the engine call.
2568
+ *
2569
+ * Audio transcription composes two engines: `audioDecode` (container → mono PCM) and `asr`
2570
+ * (PCM → text/subtitles). A small pure resampler bridges arbitrary sample rates to the
2571
+ * 16 kHz mono Float32Array ASR engines expect.
2572
+ */
2573
+ var fail = (verb, message) => {
2574
+ throw new E_MEDIA_STEP_FAILED([verb, message]);
2575
+ };
2576
+ var IMAGE_VERBS = new Set([
2577
+ "image.resize",
2578
+ "image.format",
2579
+ "image.rotate",
2580
+ "image.flip",
2581
+ "image.strip_metadata"
2582
+ ]);
2583
+ var requireImage = (ctx, verb) => {
2584
+ if (!ctx.payload.mimeType.toLowerCase().startsWith("image/")) fail(verb, `image operations expect an image; the media is ${ctx.payload.mimeType}`);
2585
+ };
2586
+ /** Fold this step's args into the fused request. */
2587
+ var foldImageStep = (ctx, fused) => {
2588
+ const args = ctx.step.args;
2589
+ switch (ctx.step.verb) {
2590
+ case "image.resize": return {
2591
+ ...fused,
2592
+ resize: {
2593
+ width: args.width,
2594
+ height: args.height,
2595
+ fit: args.fit
2596
+ }
2597
+ };
2598
+ case "image.format": return {
2599
+ ...fused,
2600
+ format: {
2601
+ to: args.to,
2602
+ quality: args.quality
2603
+ },
2604
+ ...args.strip_metadata !== void 0 ? { stripMetadata: args.strip_metadata } : {}
2605
+ };
2606
+ case "image.rotate": {
2607
+ const deg = Number(args.deg);
2608
+ const total = ((fused.rotate ?? 0) + deg) % 360;
2609
+ return {
2610
+ ...fused,
2611
+ rotate: total === 0 ? void 0 : total
2612
+ };
2613
+ }
2614
+ case "image.flip": {
2615
+ const axis = args.axis;
2616
+ const flip = { ...fused.flip ?? {} };
2617
+ if (axis === "horizontal" || axis === "both") flip.horizontal = !flip.horizontal;
2618
+ if (axis === "vertical" || axis === "both") flip.vertical = !flip.vertical;
2619
+ return {
2620
+ ...fused,
2621
+ flip
2622
+ };
2623
+ }
2624
+ case "image.strip_metadata": return {
2625
+ ...fused,
2626
+ stripMetadata: true
2627
+ };
2628
+ default: return fused;
2629
+ }
2630
+ };
2631
+ /** `true` when the NEXT step in the plan is also an image verb (so we defer the engine call). */
2632
+ var nextIsImageStep = (ctx) => {
2633
+ const next = ctx.plan.steps[ctx.stepIndex + 1];
2634
+ return next !== void 0 && IMAGE_VERBS.has(next.verb);
2635
+ };
2636
+ var FUSION_KEY = "media.image.fused";
2637
+ var imageStep = async (ctx) => {
2638
+ const verb = ctx.step.verb.replace(/\./g, " ");
2639
+ requireImage(ctx, verb);
2640
+ const fused = foldImageStep(ctx, ctx.stash.get(FUSION_KEY) ?? {});
2641
+ if (nextIsImageStep(ctx)) {
2642
+ ctx.stash.set(FUSION_KEY, fused);
2643
+ return {
2644
+ kind: "media",
2645
+ payload: ctx.payload
2646
+ };
2647
+ }
2648
+ ctx.stash.delete(FUSION_KEY);
2649
+ if (!ctx.engines.hasMutate()) fail(verb, "no image-mutating engine is configured. Do not retry image verbs in this deployment.");
2650
+ const result = await ctx.engines.mutate({
2651
+ ...fused,
2652
+ bytes: ctx.payload.bytes,
2653
+ mimeType: ctx.payload.mimeType,
2654
+ signal: ctx.signal
2655
+ });
2656
+ const ext = result.mimeType.split("/")[1]?.replace("jpeg", "jpg") ?? "bin";
2657
+ const dot = ctx.payload.filename.lastIndexOf(".");
2658
+ const base = dot > 0 ? ctx.payload.filename.slice(0, dot) : ctx.payload.filename;
2659
+ return {
2660
+ kind: "media",
2661
+ payload: {
2662
+ bytes: result.bytes,
2663
+ mimeType: result.mimeType,
2664
+ filename: `${base}.${ext}`
2665
+ }
2666
+ };
2667
+ };
2668
+ /** Downmix to mono and linearly resample to the target rate. Pure, cross-env. */
2669
+ var resampleTo = (pcm, fromRate, toRate) => {
2670
+ if (fromRate === toRate) return pcm;
2671
+ const ratio = fromRate / toRate;
2672
+ const outLength = Math.floor(pcm.length / ratio);
2673
+ const out = new Float32Array(outLength);
2674
+ for (let i = 0; i < outLength; i++) {
2675
+ const pos = i * ratio;
2676
+ const left = Math.floor(pos);
2677
+ const right = Math.min(left + 1, pcm.length - 1);
2678
+ const frac = pos - left;
2679
+ out[i] = pcm[left] * (1 - frac) + pcm[right] * frac;
2680
+ }
2681
+ return out;
2682
+ };
2683
+ var ASR_SAMPLE_RATE = 16e3;
2684
+ /**
2685
+ * `audio.transcribe` — an explicit two-stage composition: convert the container to PCM,
2686
+ * resample to 16 kHz mono, then convert the PCM to the requested text form. The registry
2687
+ * never auto-chains through PCM (it is a terminal token), precisely because this resample
2688
+ * leg must happen between the two converts.
2689
+ */
2690
+ var audioTranscribeStep = async (ctx) => {
2691
+ const verb = "audio transcribe";
2692
+ if (!ctx.payload.mimeType.toLowerCase().startsWith("audio/")) fail(verb, `transcription expects audio; the media is ${ctx.payload.mimeType}`);
2693
+ const out = argOf(ctx.step, "out") ?? "txt";
2694
+ if (!ctx.engines.hasConvert(ctx.payload.mimeType, "pcm")) fail(verb, `no engine that decodes ${ctx.payload.mimeType} to PCM is configured. Do not retry this verb in this deployment.`);
2695
+ if (!ctx.engines.hasConvert("audio/x-adk-pcm", out)) fail(verb, `no engine that transcribes PCM to "${out}" is configured. Do not retry this verb in this deployment.`);
2696
+ const pcmOut = (await ctx.engines.convert({
2697
+ bytes: ctx.payload.bytes,
2698
+ mimeType: ctx.payload.mimeType,
2699
+ filename: ctx.payload.filename,
2700
+ to: "pcm",
2701
+ signal: ctx.signal
2702
+ })).outputs[0];
2703
+ if (!pcmOut) fail(verb, "audio decoding produced no output");
2704
+ const sourceRate = Number(pcmOut.meta?.sampleRate);
2705
+ if (!Number.isFinite(sourceRate) || sourceRate <= 0) fail(verb, "the decode engine did not report meta.sampleRate on its PCM output");
2706
+ const pcm = resampleTo(bytesToPcm(pcmOut.bytes), sourceRate, ASR_SAMPLE_RATE);
2707
+ const final = (await ctx.engines.convert({
2708
+ bytes: pcmToBytes(pcm),
2709
+ mimeType: PCM_MIME,
2710
+ filename: ctx.payload.filename,
2711
+ to: out,
2712
+ options: {
2713
+ lang: argOf(ctx.step, "lang"),
2714
+ translate: argOf(ctx.step, "translate")
2715
+ },
2716
+ signal: ctx.signal
2717
+ })).outputs[0];
2718
+ if (!final) fail(verb, "transcription produced no output");
2719
+ const text = new TextDecoder().decode(final.bytes);
2720
+ return {
2721
+ kind: "data",
2722
+ data: text,
2723
+ asText: text
2724
+ };
2725
+ };
2726
+ /** The image + audio step registry fragment. */
2727
+ var IMAGE_AUDIO_STEPS = [
2728
+ ["image.resize", imageStep],
2729
+ ["image.format", imageStep],
2730
+ ["image.rotate", imageStep],
2731
+ ["image.flip", imageStep],
2732
+ ["image.strip_metadata", imageStep],
2733
+ ["audio.transcribe", audioTranscribeStep]
2734
+ ];
2735
+ //#endregion
2736
+ //#region src/batteries/media/steps/pages.ts
2737
+ /**
2738
+ * Pure page-operation step implementations for PDFs: select, split, merge, reorder.
2739
+ *
2740
+ * @remarks
2741
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. Implemented with `pdf-lib`
2742
+ * (optional peer, lazily imported) on in-memory bytes — no binaries, cross-environment.
2743
+ * Phase 0 covers PDF inputs; PPTX slide operations and DOCX section operations join in the
2744
+ * sheet/slides/doc phases, sharing these verb ids with format dispatch.
2745
+ */
2746
+ var pdfLibPromise;
2747
+ var pdfLib = () => {
2748
+ pdfLibPromise ??= import("pdf-lib");
2749
+ return pdfLibPromise;
2750
+ };
2751
+ var requirePdf = (verb, payload) => {
2752
+ if (payload.mimeType.toLowerCase().split(";")[0].trim() !== MIME.PDF) throw new E_MEDIA_STEP_FAILED([verb, `page operations for ${payload.mimeType} are not yet implemented in this build (PDF is supported)`]);
2753
+ };
2754
+ var loadPdf = async (verb, bytes) => {
2755
+ const { PDFDocument } = await pdfLib();
2756
+ try {
2757
+ return await PDFDocument.load(bytes, { ignoreEncryption: false });
2758
+ } catch (err) {
2759
+ throw new E_MEDIA_STEP_FAILED([verb, `could not read the PDF: ${isError(err) ? err.message : String(err)}`], { cause: err });
2760
+ }
2761
+ };
2762
+ var checkIndices = (verb, indices, pageCount) => {
2763
+ for (const i of indices) if (i < 1 || i > pageCount) throw new E_MEDIA_STEP_FAILED([verb, `page ${i} is out of range — the document has ${pageCount} pages (pages are 1-based)`]);
2764
+ };
2765
+ /** `select` — keep only the listed 1-based pages, producing one document. */
2766
+ var selectStep = async (ctx) => {
2767
+ requirePdf("select", ctx.payload);
2768
+ const pages = argOf(ctx.step, "pages");
2769
+ const source = await loadPdf("select", ctx.payload.bytes);
2770
+ checkIndices("select", pages, source.getPageCount());
2771
+ const { PDFDocument } = await pdfLib();
2772
+ const out = await PDFDocument.create();
2773
+ const copied = await out.copyPages(source, pages.map((p) => p - 1));
2774
+ for (const page of copied) out.addPage(page);
2775
+ const bytes = await out.save();
2776
+ return {
2777
+ kind: "media",
2778
+ payload: {
2779
+ ...ctx.payload,
2780
+ bytes: new Uint8Array(bytes)
2781
+ }
2782
+ };
2783
+ };
2784
+ /** `split` — split into multiple documents by page (optionally grouped by JSON ranges). */
2785
+ var splitStep = async (ctx) => {
2786
+ requirePdf("split", ctx.payload);
2787
+ const by = argOf(ctx.step, "by") ?? "page";
2788
+ if (by !== "page") throw new E_MEDIA_STEP_FAILED(["split", `split by=${by} is not yet implemented in this build (page is supported)`]);
2789
+ const source = await loadPdf("split", ctx.payload.bytes);
2790
+ const pageCount = source.getPageCount();
2791
+ const rawRanges = ctx.step.args.ranges;
2792
+ const groups = rawRanges !== void 0 ? rawRanges.map((pair) => {
2793
+ if (!Array.isArray(pair) || pair.length !== 2) throw new E_MEDIA_STEP_FAILED(["split", `ranges must be [start,end] pairs, e.g. ranges='[[1,3],[5,7]]'`]);
2794
+ const [start, end] = pair;
2795
+ const pages = [];
2796
+ for (let p = start; p <= end; p++) pages.push(p);
2797
+ return pages;
2798
+ }) : Array.from({ length: pageCount }, (_, i) => [i + 1]);
2799
+ const { PDFDocument } = await pdfLib();
2800
+ const payloads = [];
2801
+ let part = 1;
2802
+ for (const group of groups) {
2803
+ checkIndices("split", group, pageCount);
2804
+ const out = await PDFDocument.create();
2805
+ const copied = await out.copyPages(source, group.map((p) => p - 1));
2806
+ for (const page of copied) out.addPage(page);
2807
+ const bytes = new Uint8Array(await out.save());
2808
+ payloads.push({
2809
+ bytes,
2810
+ mimeType: MIME.PDF,
2811
+ filename: replaceExtension(ctx.payload.filename, "").replace(/\.$/, "") + `.part${part}.pdf`
2812
+ });
2813
+ part += 1;
2814
+ }
2815
+ return {
2816
+ kind: "media-list",
2817
+ payloads
2818
+ };
2819
+ };
2820
+ /** `merge` — append the referenced media's pages, in order. */
2821
+ var mergeStep = async (ctx) => {
2822
+ requirePdf("merge", ctx.payload);
2823
+ const raw = ctx.step.args.with;
2824
+ const refs = Array.isArray(raw) ? raw : [raw];
2825
+ const { PDFDocument } = await pdfLib();
2826
+ const out = await PDFDocument.create();
2827
+ const appendAll = async (doc) => {
2828
+ const copied = await out.copyPages(doc, doc.getPageIndices());
2829
+ for (const page of copied) out.addPage(page);
2830
+ };
2831
+ await appendAll(await loadPdf("merge", ctx.payload.bytes));
2832
+ for (const ref of refs) {
2833
+ if (ref.kind !== "id") throw new E_MEDIA_STEP_FAILED(["merge", "builder refs are not yet supported here"]);
2834
+ const other = await ctx.resolveRef(ref.id);
2835
+ requirePdf("merge", other);
2836
+ await appendAll(await loadPdf("merge", other.bytes));
2837
+ }
2838
+ const bytes = new Uint8Array(await out.save());
2839
+ return {
2840
+ kind: "media",
2841
+ payload: {
2842
+ ...ctx.payload,
2843
+ bytes
2844
+ }
2845
+ };
2846
+ };
2847
+ /** `reorder` — rebuild the document with pages in the given 1-based order. */
2848
+ var reorderStep = async (ctx) => {
2849
+ requirePdf("reorder", ctx.payload);
2850
+ const order = argOf(ctx.step, "order");
2851
+ const source = await loadPdf("reorder", ctx.payload.bytes);
2852
+ const pageCount = source.getPageCount();
2853
+ checkIndices("reorder", order, pageCount);
2854
+ if (order.length !== pageCount) throw new E_MEDIA_STEP_FAILED(["reorder", `order lists ${order.length} pages but the document has ${pageCount} — list every page exactly once`]);
2855
+ const { PDFDocument } = await pdfLib();
2856
+ const out = await PDFDocument.create();
2857
+ const copied = await out.copyPages(source, order.map((p) => p - 1));
2858
+ for (const page of copied) out.addPage(page);
2859
+ const bytes = new Uint8Array(await out.save());
2860
+ return {
2861
+ kind: "media",
2862
+ payload: {
2863
+ ...ctx.payload,
2864
+ bytes
2865
+ }
2866
+ };
2867
+ };
2868
+ //#endregion
2869
+ //#region src/batteries/media/index.ts
2870
+ /**
2871
+ * A knex-inspired local media pipeline: one declarative plan, three front-ends (chainable
2872
+ * builder, pipe string, JSON ops), engines as composable seams.
2873
+ *
2874
+ * @module @nhtio/adk/batteries/media
2875
+ *
2876
+ * @remarks
2877
+ * Most implementations give you two ways to process media — ship it to an external API, or
2878
+ * flood the model's context window with the bytes. This battery is the third option: **full
2879
+ * control of media — local processing, no external APIs by default, cross-environment where
2880
+ * possible.** Your data stays in your application and infrastructure unless you deliberately
2881
+ * wire an engine that sends it elsewhere.
2882
+ *
2883
+ * The entry point is {@link createMediaPipeline}:
2884
+ *
2885
+ * ```ts
2886
+ * import { createMediaPipeline } from '@nhtio/adk/batteries/media'
2887
+ *
2888
+ * const mp = await createMediaPipeline({
2889
+ * engines: [
2890
+ * // an ordered array of self-declaring engines; resolvers run eagerly at construction,
2891
+ * // but each engine's heavy peer dependency loads lazily on first actual use
2892
+ * () => import('@nhtio/adk/batteries/media/engines/jimp').then((m) => m.jimpEngine()),
2893
+ * ],
2894
+ * })
2895
+ *
2896
+ * // chainable builder (implementor DX)
2897
+ * const redacted = await mp(payload)
2898
+ * .select({ pages: [2, 3] })
2899
+ * .redact({ match: [/\d{3}-\d{2}-\d{4}/] })
2900
+ *
2901
+ * // pipe string (the LLM-facing DSL — same plan underneath)
2902
+ * const result = await mp.query(payload, 'select pages=2-3 | redact match=/\\d{3}-\\d{2}-\\d{4}/')
2903
+ * ```
2904
+ *
2905
+ * Media pipelines have no standard — every tool invented its own grammar. LLMs don't share one
2906
+ * mental model of that chaos, so the pipe DSL projects every operation onto the one
2907
+ * transformation idiom every model already groks: shell pipes with `key=value` args. The verbs
2908
+ * a deployment advertises narrow to the engines it configured; everything else fails loud with
2909
+ * a model-actionable message.
2910
+ */
2911
+ var configSchema = validator.object({
2912
+ engines: validator.array().items(validator.alternatives().try(validator.function(), validator.object().unknown(true))).optional(),
2913
+ selection: validator.array().items(validator.function()).optional(),
2914
+ use: validator.array().items(validator.function()).optional(),
2915
+ resolveRef: validator.function().optional()
2916
+ }).unknown(false);
2917
+ /** The Phase 0 step registry: pure steps available in every deployment. */
2918
+ var PURE_STEPS = [
2919
+ ["select", selectStep],
2920
+ ["split", splitStep],
2921
+ ["merge", mergeStep],
2922
+ ["reorder", reorderStep],
2923
+ ["redact", redactStep],
2924
+ ["update_text", updateTextStep],
2925
+ ["diff", diffStep],
2926
+ ["apply_patch", applyPatchStep],
2927
+ ["extract.text", extractTextStep],
2928
+ ["extract.metadata", extractMetadataStep],
2929
+ ["chunk", chunkStep],
2930
+ ...SHEET_STEPS,
2931
+ ...SLIDES_STEPS,
2932
+ ...DOC_STEPS,
2933
+ ...INGEST_STEPS,
2934
+ ...IMAGE_AUDIO_STEPS
2935
+ ];
2936
+ /** Resolve one engine entry: invoke a resolver, unwrap `{default}`, guard-check the result. */
2937
+ var resolveEngine = async (entry, index) => {
2938
+ let value = entry;
2939
+ if (typeof value === "function") {
2940
+ try {
2941
+ value = await value();
2942
+ } catch (err) {
2943
+ throw new E_INVALID_MEDIA_PIPELINE_CONFIG([`engines[${index}] resolver failed: ${isError(err) ? err.message : String(err)}`]);
2944
+ }
2945
+ if (isObject(value) && "default" in value && !implementsMediaEngine(value)) value = value.default;
2946
+ }
2947
+ if (!implementsMediaEngine(value)) {
2948
+ const candidate = value;
2949
+ throw new E_INVALID_MEDIA_PIPELINE_CONFIG([`engines[${index}]${isObject(value) && typeof candidate?.id === "string" ? ` ("${candidate.id}")` : ""} does not implement the MediaEngine contract (a string id plus at least one well-formed converts/mutates capability entry)`]);
2950
+ }
2951
+ return value;
2952
+ };
2953
+ /**
2954
+ * Create a media pipeline.
2955
+ *
2956
+ * @remarks
2957
+ * Config is validated eagerly, and so are the engines: every resolver in the array runs at
2958
+ * construction (capability declarations drive verb narrowing, so they must be known up
2959
+ * front), each result is contract-checked, and failures throw
2960
+ * `E_INVALID_MEDIA_PIPELINE_CONFIG` naming the offending index. Construction stays cheap —
2961
+ * bundled engine modules import their heavy peer dependencies lazily inside capability
2962
+ * methods, on first actual use.
2963
+ *
2964
+ * @param config - Engines, selection stages, step interceptors, and the media-ref resolver.
2965
+ * @returns The callable {@link MediaPipeline}.
2966
+ */
2967
+ var createMediaPipeline = async (config = {}) => {
2968
+ const { error } = configSchema.validate(config, { abortEarly: true });
2969
+ if (error) throw new E_INVALID_MEDIA_PIPELINE_CONFIG([error.message]);
2970
+ const use = config.use ?? [];
2971
+ const resolveRef = config.resolveRef ?? (() => {
2972
+ throw new E_INVALID_MEDIA_PIPELINE_CONFIG(["this chain references another media (@id) but no resolveRef was configured"]);
2973
+ });
2974
+ const registry = buildEngineRegistry(await Promise.all((config.engines ?? []).map(resolveEngine)), config.selection ?? []);
2975
+ const steps = new Map(PURE_STEPS);
2976
+ const run = async (input, plan, options) => {
2977
+ const refResolver = options?.resolveRef ?? resolveRef;
2978
+ return executePlan(plan, {
2979
+ input,
2980
+ steps,
2981
+ use,
2982
+ engines: registry,
2983
+ resolveRef: async (id) => refResolver(id),
2984
+ signal: options?.signal
2985
+ });
2986
+ };
2987
+ const compile = (statement) => typeof statement === "string" ? validateSegments(parsePipeRaw(statement), { capabilities: registry }) : validateOps(statement, { capabilities: registry });
2988
+ const callable = ((input) => {
2989
+ return new MediaChain(async (ops) => {
2990
+ return run(input, validateOps(ops, { capabilities: registry }));
2991
+ });
2992
+ });
2993
+ Object.defineProperties(callable, {
2994
+ query: { value: async (input, q, options) => {
2995
+ return run(input, validateSegments(parsePipeRaw(q), { capabilities: registry }), options);
2996
+ } },
2997
+ ops: { value: async (input, ops, options) => {
2998
+ return run(input, validateOps(ops, { capabilities: registry }), options);
2999
+ } },
3000
+ compile: { value: compile },
3001
+ capabilities: { value: registry },
3002
+ engines: { value: registry.engines }
3003
+ });
3004
+ return callable;
3005
+ };
3006
+ //#endregion
3007
+ export { EXT_TO_MIME, E_INVALID_MEDIA_PIPELINE_CONFIG, E_MEDIA_BAD_ARG, E_MEDIA_ENGINE_REQUIRED, E_MEDIA_MISSING_ARG, E_MEDIA_NOT_PIPE_EXPRESSIBLE, E_MEDIA_PIPE_SYNTAX, E_MEDIA_STEP_FAILED, E_MEDIA_STEP_UNAVAILABLE, E_MEDIA_UNKNOWN_ARG, E_MEDIA_UNKNOWN_VERB, E_MEDIA_UNSUPPORTED_OP, FOLDED_VERBS, MIME, MediaChain, PCM_MIME, VERBS, VERB_INDEX, availableVerbs, buildEngineRegistry, bytesToPcm, createMediaPipeline, familyOf, foldVerb, fromOps, implementsMediaEngine, isMediaRef, isRegExpRef, lowerSegments, parsePipeRaw, pcmToBytes, replaceExtension, suggestVerbs, toOps, toPipe, unsupportedForMutationReason, validateOps, validateSegments };
3008
+
3009
+ //# sourceMappingURL=media.mjs.map