@veryfront/ext-document-kreuzberg 0.1.988 → 0.1.989

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.
@@ -7,21 +7,26 @@
7
7
  * @module extensions/ext-document-kreuzberg
8
8
  */
9
9
  import "../../../_dnt.polyfills.js";
10
- import type { ExtensionFactory } from "veryfront/extensions";
11
- import type { DocumentExtractor, KreuzbergExtractor } from "veryfront/extensions/compat";
10
+ import type { ExtensionFactory, ExtensionLogger } from "veryfront/extensions";
11
+ import type { DocumentExtractionOptions, DocumentExtractor, KreuzbergExtractor } from "veryfront/extensions/compat";
12
12
  /** Maximum time to wait for fallback worker extraction before aborting. */
13
13
  export declare const EXTRACTION_TIMEOUT_MS = 120000;
14
+ export declare const NATIVE_PROGRESS_IDLE_TIMEOUT_MS = 120000;
15
+ export declare const NATIVE_PROGRESS_HARD_TIMEOUT_MS: number;
14
16
  declare function extractInWorkerDeno(buffer: ArrayBuffer, mimeType: string): Promise<string>;
15
17
  export interface KreuzbergDocumentExtractorDeps {
16
18
  isDenoRuntime?: boolean;
17
19
  loadNativeKreuzberg?: () => Promise<KreuzbergExtractor>;
18
20
  extractInWorkerDeno?: typeof extractInWorkerDeno;
21
+ extractWithNativeProgressDeno?: typeof extractWithNativeProgressDeno;
22
+ logger?: Pick<ExtensionLogger, "warn">;
19
23
  }
24
+ declare function extractWithNativeProgressDeno(buffer: ArrayBuffer, mimeType: string, options: DocumentExtractionOptions): Promise<string>;
20
25
  export declare class KreuzbergDocumentExtractor implements DocumentExtractor {
21
26
  private readonly deps;
22
27
  constructor(deps?: KreuzbergDocumentExtractorDeps);
23
28
  importKreuzberg(): Promise<KreuzbergExtractor>;
24
- extractInWorker(buffer: ArrayBuffer, mimeType: string): Promise<string>;
29
+ extractInWorker(buffer: ArrayBuffer, mimeType: string, options?: DocumentExtractionOptions): Promise<string>;
25
30
  }
26
31
  declare const extDocumentKreuzberg: ExtensionFactory;
27
32
  export default extDocumentKreuzberg;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/extensions/ext-document-kreuzberg/src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,4BAA4B,CAAC;AAKpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAIrG,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,SAAU,CAAC;AAE7C,iBAAS,mBAAmB,CAC1B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CA0CjB;AAED,MAAM,WAAW,8BAA8B;IAC7C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACxD,mBAAmB,CAAC,EAAE,OAAO,mBAAmB,CAAC;CAClD;AAgBD,qBAAa,0BAA2B,YAAW,iBAAiB;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,GAAE,8BAAmC;IAEtE,eAAe,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAIxC,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CA2B9E;AAED,QAAA,MAAM,oBAAoB,EAAE,gBAkB3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/extensions/ext-document-kreuzberg/src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,4BAA4B,CAAC;AAKpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAC1F,OAAO,KAAK,EACV,yBAAyB,EAEzB,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,yCAAyC,CAAC;AAIjD,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,SAAU,CAAC;AAC7C,eAAO,MAAM,+BAA+B,SAAU,CAAC;AACvD,eAAO,MAAM,+BAA+B,QAAc,CAAC;AAE3D,iBAAS,mBAAmB,CAC1B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CA0CjB;AAED,MAAM,WAAW,8BAA8B;IAC7C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACxD,mBAAmB,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACjD,6BAA6B,CAAC,EAAE,OAAO,6BAA6B,CAAC;IACrE,MAAM,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;CACxC;AAuCD,iBAAS,6BAA6B,CACpC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,CAgFjB;AAED,qBAAa,0BAA2B,YAAW,iBAAiB;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,GAAE,8BAAmC;IAEtE,eAAe,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAIxC,eAAe,CACnB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,MAAM,CAAC;CAgDnB;AAED,QAAA,MAAM,oBAAoB,EAAE,gBAiB3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -12,6 +12,8 @@ import { isMissingPackageError, loadKreuzberg, loadKreuzbergNative } from "./kre
12
12
  import { isDeno } from "./runtime.js";
13
13
  /** Maximum time to wait for fallback worker extraction before aborting. */
14
14
  export const EXTRACTION_TIMEOUT_MS = 120_000;
15
+ export const NATIVE_PROGRESS_IDLE_TIMEOUT_MS = 120_000;
16
+ export const NATIVE_PROGRESS_HARD_TIMEOUT_MS = 10 * 60_000;
15
17
  function extractInWorkerDeno(buffer, mimeType) {
16
18
  return new Promise((resolve, reject) => {
17
19
  // The worker ships as raw TypeScript in the compiled binary and from source
@@ -49,11 +51,98 @@ function extractInWorkerDeno(buffer, mimeType) {
49
51
  function isPdfMimeType(mimeType) {
50
52
  return mimeType.toLowerCase().split(";")[0]?.trim() === "application/pdf";
51
53
  }
54
+ function normalizeMimeType(mimeType) {
55
+ return mimeType.toLowerCase().split(";")[0]?.trim() ?? "";
56
+ }
57
+ function isNativeProgressMimeType(mimeType) {
58
+ const normalized = normalizeMimeType(mimeType);
59
+ return normalized === "application/pdf" ||
60
+ normalized === "application/vnd.ms-powerpoint" ||
61
+ normalized === "application/vnd.openxmlformats-officedocument.presentationml.presentation";
62
+ }
52
63
  async function extractWithNativeKreuzberg(buffer, mimeType, loadNative) {
53
64
  const { extractBytes } = await loadNative();
54
65
  const result = await extractBytes(new Uint8Array(buffer), mimeType);
55
66
  return result.content;
56
67
  }
68
+ function warningDetails(mimeType, error) {
69
+ return {
70
+ mimeType,
71
+ error: error instanceof Error ? error.message : String(error),
72
+ };
73
+ }
74
+ function extractWithNativeProgressDeno(buffer, mimeType, options) {
75
+ return new Promise((resolve, reject) => {
76
+ const workerFile = globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url.endsWith(".ts")
77
+ ? "./native-progress-extraction-worker.ts"
78
+ : "./native-progress-extraction-worker.js";
79
+ const workerUrl = new URL(workerFile, globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url);
80
+ const worker = new Worker(workerUrl, { type: "module" });
81
+ const idleTimeoutMs = options.idleTimeoutMs ?? NATIVE_PROGRESS_IDLE_TIMEOUT_MS;
82
+ const hardTimeoutMs = options.hardTimeoutMs ?? NATIVE_PROGRESS_HARD_TIMEOUT_MS;
83
+ let settled = false;
84
+ let idleTimer;
85
+ const clearIdleTimer = () => {
86
+ if (!idleTimer)
87
+ return;
88
+ clearTimeout(idleTimer);
89
+ idleTimer = undefined;
90
+ };
91
+ const cleanup = () => {
92
+ settled = true;
93
+ clearIdleTimer();
94
+ clearTimeout(hardTimer);
95
+ worker.terminate();
96
+ };
97
+ const fail = (error) => {
98
+ if (settled)
99
+ return;
100
+ cleanup();
101
+ reject(error);
102
+ };
103
+ const resetIdleTimer = () => {
104
+ clearIdleTimer();
105
+ idleTimer = dntShim.setTimeout(() => {
106
+ fail(new Error(`Text extraction made no progress for ${idleTimeoutMs / 1000}s. ` +
107
+ "The file may be corrupted or unsupported"));
108
+ }, idleTimeoutMs);
109
+ };
110
+ const hardTimer = dntShim.setTimeout(() => {
111
+ fail(new Error(`Text extraction exceeded the hard timeout after ${hardTimeoutMs / 1000}s. ` +
112
+ "The file may be corrupted or unsupported"));
113
+ }, hardTimeoutMs);
114
+ resetIdleTimer();
115
+ worker.onmessage = async (event) => {
116
+ if (settled)
117
+ return;
118
+ const message = event.data;
119
+ if (message.type === "progress") {
120
+ clearIdleTimer();
121
+ try {
122
+ await options.onProgress?.(message.event);
123
+ }
124
+ catch (error) {
125
+ fail(error instanceof Error ? error : new Error(String(error)));
126
+ return;
127
+ }
128
+ if (!settled)
129
+ resetIdleTimer();
130
+ return;
131
+ }
132
+ if (message.type === "error") {
133
+ fail(new Error(message.error));
134
+ return;
135
+ }
136
+ cleanup();
137
+ resolve(message.content);
138
+ };
139
+ worker.onerror = (event) => {
140
+ fail(new Error(`Text extraction worker failed: ${event.message ?? "unknown"}`));
141
+ };
142
+ const requestBuffer = buffer.slice(0);
143
+ worker.postMessage({ buffer: requestBuffer, mimeType }, [requestBuffer]);
144
+ });
145
+ }
57
146
  export class KreuzbergDocumentExtractor {
58
147
  deps;
59
148
  constructor(deps = {}) {
@@ -62,7 +151,7 @@ export class KreuzbergDocumentExtractor {
62
151
  importKreuzberg() {
63
152
  return loadKreuzberg();
64
153
  }
65
- async extractInWorker(buffer, mimeType) {
154
+ async extractInWorker(buffer, mimeType, options = {}) {
66
155
  const isDenoRuntime = this.deps.isDenoRuntime ?? isDeno;
67
156
  const extractWithWorker = this.deps.extractInWorkerDeno ?? extractInWorkerDeno;
68
157
  // Node/Bun extract in-process via @kreuzberg/node. Deno keeps the isolated
@@ -73,6 +162,23 @@ export class KreuzbergDocumentExtractor {
73
162
  const result = await extractBytes(new Uint8Array(buffer), mimeType);
74
163
  return result.content;
75
164
  }
165
+ if (options.onProgress && isNativeProgressMimeType(mimeType)) {
166
+ try {
167
+ return await (this.deps.extractWithNativeProgressDeno ?? extractWithNativeProgressDeno)(buffer, mimeType, options);
168
+ }
169
+ catch (error) {
170
+ // Keep progress opportunistic: if page/slide extraction cannot handle a
171
+ // document, fall back to the previous opaque extraction path.
172
+ const message = "[ext-document-kreuzberg] native progress extraction failed; falling back to opaque extraction";
173
+ const details = warningDetails(mimeType, error);
174
+ if (this.deps.logger) {
175
+ this.deps.logger.warn(message, details);
176
+ }
177
+ else {
178
+ console.warn(message, details);
179
+ }
180
+ }
181
+ }
76
182
  if (isPdfMimeType(mimeType)) {
77
183
  try {
78
184
  return await extractWithNativeKreuzberg(buffer, mimeType, this.deps.loadNativeKreuzberg ?? loadKreuzbergNative);
@@ -86,7 +192,6 @@ export class KreuzbergDocumentExtractor {
86
192
  }
87
193
  }
88
194
  const extDocumentKreuzberg = () => {
89
- const extractor = new KreuzbergDocumentExtractor();
90
195
  return {
91
196
  name: "ext-document-kreuzberg",
92
197
  version: "0.1.0",
@@ -97,6 +202,7 @@ const extDocumentKreuzberg = () => {
97
202
  { type: "fs:read" },
98
203
  ],
99
204
  setup(ctx) {
205
+ const extractor = new KreuzbergDocumentExtractor({ logger: ctx.logger });
100
206
  ctx.provide("DocumentExtractor", extractor);
101
207
  ctx.logger.info("[ext-document-kreuzberg] document extraction registered");
102
208
  },
@@ -0,0 +1,161 @@
1
+ import { PDFDocument } from "pdf-lib";
2
+ import JSZip from "jszip";
3
+ import { loadKreuzbergNative } from "./kreuzberg.js";
4
+ function postProgress(event) {
5
+ self.postMessage({ type: "progress", event });
6
+ }
7
+ function normalizeMimeType(mimeType) {
8
+ return mimeType.toLowerCase().split(";")[0]?.trim() ?? "";
9
+ }
10
+ function isPdfMimeType(mimeType) {
11
+ return normalizeMimeType(mimeType) === "application/pdf";
12
+ }
13
+ function isPptxMimeType(mimeType) {
14
+ return normalizeMimeType(mimeType) === "application/vnd.openxmlformats-officedocument.presentationml.presentation";
15
+ }
16
+ function decodeXmlText(value) {
17
+ return value.replace(
18
+ /&#x([0-9a-f]+);/gi,
19
+ (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))
20
+ ).replace(/&#(\d+);/g, (_, decimal) => String.fromCodePoint(Number.parseInt(decimal, 10))).replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
21
+ }
22
+ function extractPptxSlideText(xml) {
23
+ return Array.from(xml.matchAll(/<a:t(?:\s[^>]*)?>([\s\S]*?)<\/a:t>/g)).map((match) => decodeXmlText(match[1] ?? "").trim()).filter(Boolean).join(" ").trim();
24
+ }
25
+ function slideNumber(path) {
26
+ return Number(path.match(/\/slide(\d+)\.xml$/)?.[1] ?? 0);
27
+ }
28
+ function getXmlAttribute(tag, name) {
29
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+ return tag.match(new RegExp(`\\s${escapedName}=(["'])(.*?)\\1`))?.[2];
31
+ }
32
+ function normalizeZipPath(path) {
33
+ const parts = [];
34
+ for (const part of path.split("/")) {
35
+ if (!part || part === ".") continue;
36
+ if (part === "..") {
37
+ parts.pop();
38
+ continue;
39
+ }
40
+ parts.push(part);
41
+ }
42
+ return parts.join("/");
43
+ }
44
+ function normalizePresentationTarget(target) {
45
+ const path = target.split(/[?#]/)[0] ?? target;
46
+ if (path.startsWith("/")) return normalizeZipPath(path.slice(1));
47
+ if (path.startsWith("ppt/")) return normalizeZipPath(path);
48
+ return normalizeZipPath(`ppt/${path}`);
49
+ }
50
+ function presentationSlideRelationshipIds(xml) {
51
+ return Array.from(xml.matchAll(/<(?:\w+:)?sldId\b[^>]*>/g)).map((match) => getXmlAttribute(match[0], "r:id")).filter((id) => typeof id === "string" && id.length > 0);
52
+ }
53
+ function presentationRelationships(xml) {
54
+ const relationships = /* @__PURE__ */ new Map();
55
+ for (const match of xml.matchAll(/<Relationship\b[^>]*>/g)) {
56
+ const tag = match[0];
57
+ const id = getXmlAttribute(tag, "Id");
58
+ const target = getXmlAttribute(tag, "Target");
59
+ if (!id || !target) continue;
60
+ const path = normalizePresentationTarget(target);
61
+ if (/^ppt\/slides\/slide\d+\.xml$/.test(path)) {
62
+ relationships.set(id, path);
63
+ }
64
+ }
65
+ return relationships;
66
+ }
67
+ async function pptxSlidePaths(zip) {
68
+ const fallback = Object.keys(zip.files).filter((path) => /^ppt\/slides\/slide\d+\.xml$/.test(path)).sort((left, right) => slideNumber(left) - slideNumber(right));
69
+ const presentation = zip.file("ppt/presentation.xml");
70
+ const rels = zip.file("ppt/_rels/presentation.xml.rels");
71
+ if (!presentation || !rels) return fallback;
72
+ const [presentationXml, relsXml] = await Promise.all([
73
+ presentation.async("text"),
74
+ rels.async("text")
75
+ ]);
76
+ const relationships = presentationRelationships(relsXml);
77
+ const fallbackSet = new Set(fallback);
78
+ const ordered = [];
79
+ for (const id of presentationSlideRelationshipIds(presentationXml)) {
80
+ const path = relationships.get(id);
81
+ if (path && fallbackSet.has(path) && !ordered.includes(path)) {
82
+ ordered.push(path);
83
+ }
84
+ }
85
+ if (!ordered.length) return fallback;
86
+ return [
87
+ ...ordered,
88
+ ...fallback.filter((path) => !ordered.includes(path))
89
+ ];
90
+ }
91
+ async function extractPdfByPage(buffer) {
92
+ const { extractBytes } = await loadKreuzbergNative();
93
+ const source = await PDFDocument.load(buffer, { ignoreEncryption: true });
94
+ const total = source.getPageCount();
95
+ const pages = [];
96
+ for (let index = 0; index < total; index += 1) {
97
+ const singlePage = await PDFDocument.create();
98
+ const [page] = await singlePage.copyPages(source, [index]);
99
+ singlePage.addPage(page);
100
+ const bytes = await singlePage.save({ useObjectStreams: false });
101
+ const result = await extractBytes(new Uint8Array(bytes), "application/pdf");
102
+ const content = result.content.trim();
103
+ pages.push(content);
104
+ postProgress({
105
+ unit: "page",
106
+ current: index + 1,
107
+ total,
108
+ characters: content.length
109
+ });
110
+ }
111
+ return pages.filter(Boolean).join("\n\n");
112
+ }
113
+ async function extractPptxBySlide(buffer, mimeType) {
114
+ const zip = await JSZip.loadAsync(buffer);
115
+ const slidePaths = await pptxSlidePaths(zip);
116
+ if (!slidePaths.length) {
117
+ const { extractBytes } = await loadKreuzbergNative();
118
+ const result = await extractBytes(new Uint8Array(buffer), mimeType);
119
+ postProgress({ unit: "file", current: 1, total: 1, characters: result.content.length });
120
+ return result.content;
121
+ }
122
+ const slides = [];
123
+ for (const [index, path] of slidePaths.entries()) {
124
+ const file = zip.file(path);
125
+ const xml = file ? await file.async("text") : "";
126
+ const content = extractPptxSlideText(xml);
127
+ slides.push(content);
128
+ postProgress({
129
+ unit: "slide",
130
+ current: index + 1,
131
+ total: slidePaths.length,
132
+ characters: content.length
133
+ });
134
+ }
135
+ return slides.filter(Boolean).join("\n\n");
136
+ }
137
+ async function extractWholeFile(buffer, mimeType) {
138
+ const { extractBytes } = await loadKreuzbergNative();
139
+ const result = await extractBytes(new Uint8Array(buffer), mimeType);
140
+ postProgress({ unit: "file", current: 1, total: 1, characters: result.content.length });
141
+ return result.content;
142
+ }
143
+ self.onmessage = async (event) => {
144
+ if (event.origin && event.origin !== self.location.origin) {
145
+ self.postMessage(
146
+ {
147
+ type: "error",
148
+ error: "Rejected document extraction request from invalid origin"
149
+ }
150
+ );
151
+ return;
152
+ }
153
+ try {
154
+ const { buffer, mimeType } = event.data;
155
+ const content = isPdfMimeType(mimeType) ? await extractPdfByPage(buffer) : isPptxMimeType(mimeType) ? await extractPptxBySlide(buffer, mimeType) : await extractWholeFile(buffer, mimeType);
156
+ self.postMessage({ type: "done", content });
157
+ } catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ self.postMessage({ type: "error", error: message });
160
+ }
161
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veryfront/ext-document-kreuzberg",
3
- "version": "0.1.988",
3
+ "version": "0.1.989",
4
4
  "description": "Veryfront first-party extension package for ext-document-kreuzberg",
5
5
  "keywords": [
6
6
  "veryfront",
@@ -50,10 +50,12 @@
50
50
  "@deno/shim-crypto": "~0.3.1",
51
51
  "@deno/shim-timers": "~0.1.0",
52
52
  "@kreuzberg/node": "4.4.2",
53
- "@kreuzberg/wasm": "4.5.2"
53
+ "@kreuzberg/wasm": "4.5.2",
54
+ "jszip": "3.10.1",
55
+ "pdf-lib": "1.17.1"
54
56
  },
55
57
  "peerDependencies": {
56
- "veryfront": "^0.1.988"
58
+ "veryfront": "^0.1.989"
57
59
  },
58
60
  "type": "module",
59
61
  "types": "./esm/extensions/ext-document-kreuzberg/src/index.d.ts",