@ssmeo/mcp-local-rag 0.18.6

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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.de.md +441 -0
  3. package/README.es.md +441 -0
  4. package/README.fr.md +441 -0
  5. package/README.md +530 -0
  6. package/README.pt-BR.md +441 -0
  7. package/README.zh-CN.md +441 -0
  8. package/dist/bin/install-skills.d.ts +19 -0
  9. package/dist/bin/install-skills.d.ts.map +1 -0
  10. package/dist/bin/install-skills.js +218 -0
  11. package/dist/bin/install-skills.js.map +1 -0
  12. package/dist/chunker/index.d.ts +24 -0
  13. package/dist/chunker/index.d.ts.map +1 -0
  14. package/dist/chunker/index.js +2 -0
  15. package/dist/chunker/index.js.map +1 -0
  16. package/dist/chunker/semantic-chunker.d.ts +96 -0
  17. package/dist/chunker/semantic-chunker.d.ts.map +1 -0
  18. package/dist/chunker/semantic-chunker.js +362 -0
  19. package/dist/chunker/semantic-chunker.js.map +1 -0
  20. package/dist/chunker/sentence-splitter.d.ts +32 -0
  21. package/dist/chunker/sentence-splitter.d.ts.map +1 -0
  22. package/dist/chunker/sentence-splitter.js +231 -0
  23. package/dist/chunker/sentence-splitter.js.map +1 -0
  24. package/dist/chunker/token-containment.d.ts +39 -0
  25. package/dist/chunker/token-containment.d.ts.map +1 -0
  26. package/dist/chunker/token-containment.js +83 -0
  27. package/dist/chunker/token-containment.js.map +1 -0
  28. package/dist/cli/common.d.ts +34 -0
  29. package/dist/cli/common.d.ts.map +1 -0
  30. package/dist/cli/common.js +121 -0
  31. package/dist/cli/common.js.map +1 -0
  32. package/dist/cli/delete.d.ts +3 -0
  33. package/dist/cli/delete.d.ts.map +1 -0
  34. package/dist/cli/delete.js +169 -0
  35. package/dist/cli/delete.js.map +1 -0
  36. package/dist/cli/file-collection.d.ts +2 -0
  37. package/dist/cli/file-collection.d.ts.map +1 -0
  38. package/dist/cli/file-collection.js +53 -0
  39. package/dist/cli/file-collection.js.map +1 -0
  40. package/dist/cli/ingest.d.ts +69 -0
  41. package/dist/cli/ingest.d.ts.map +1 -0
  42. package/dist/cli/ingest.js +323 -0
  43. package/dist/cli/ingest.js.map +1 -0
  44. package/dist/cli/list.d.ts +27 -0
  45. package/dist/cli/list.d.ts.map +1 -0
  46. package/dist/cli/list.js +207 -0
  47. package/dist/cli/list.js.map +1 -0
  48. package/dist/cli/options.d.ts +89 -0
  49. package/dist/cli/options.d.ts.map +1 -0
  50. package/dist/cli/options.js +244 -0
  51. package/dist/cli/options.js.map +1 -0
  52. package/dist/cli/query.d.ts +17 -0
  53. package/dist/cli/query.d.ts.map +1 -0
  54. package/dist/cli/query.js +194 -0
  55. package/dist/cli/query.js.map +1 -0
  56. package/dist/cli/read-neighbors.d.ts +3 -0
  57. package/dist/cli/read-neighbors.d.ts.map +1 -0
  58. package/dist/cli/read-neighbors.js +192 -0
  59. package/dist/cli/read-neighbors.js.map +1 -0
  60. package/dist/cli/status.d.ts +6 -0
  61. package/dist/cli/status.d.ts.map +1 -0
  62. package/dist/cli/status.js +78 -0
  63. package/dist/cli/status.js.map +1 -0
  64. package/dist/cli/sync.d.ts +6 -0
  65. package/dist/cli/sync.d.ts.map +1 -0
  66. package/dist/cli/sync.js +271 -0
  67. package/dist/cli/sync.js.map +1 -0
  68. package/dist/cli-main.d.ts +9 -0
  69. package/dist/cli-main.d.ts.map +1 -0
  70. package/dist/cli-main.js +60 -0
  71. package/dist/cli-main.js.map +1 -0
  72. package/dist/embedder/index.d.ts +143 -0
  73. package/dist/embedder/index.d.ts.map +1 -0
  74. package/dist/embedder/index.js +429 -0
  75. package/dist/embedder/index.js.map +1 -0
  76. package/dist/embedder/remote.d.ts +51 -0
  77. package/dist/embedder/remote.d.ts.map +1 -0
  78. package/dist/embedder/remote.js +182 -0
  79. package/dist/embedder/remote.js.map +1 -0
  80. package/dist/features/list.d.ts +37 -0
  81. package/dist/features/list.d.ts.map +1 -0
  82. package/dist/features/list.js +41 -0
  83. package/dist/features/list.js.map +1 -0
  84. package/dist/features/sync.d.ts +219 -0
  85. package/dist/features/sync.d.ts.map +1 -0
  86. package/dist/features/sync.js +461 -0
  87. package/dist/features/sync.js.map +1 -0
  88. package/dist/index.d.ts +3 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +56 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/ingest/compute.d.ts +63 -0
  93. package/dist/ingest/compute.d.ts.map +1 -0
  94. package/dist/ingest/compute.js +161 -0
  95. package/dist/ingest/compute.js.map +1 -0
  96. package/dist/ingest/file.d.ts +40 -0
  97. package/dist/ingest/file.d.ts.map +1 -0
  98. package/dist/ingest/file.js +74 -0
  99. package/dist/ingest/file.js.map +1 -0
  100. package/dist/ingest/visual.d.ts +52 -0
  101. package/dist/ingest/visual.d.ts.map +1 -0
  102. package/dist/ingest/visual.js +294 -0
  103. package/dist/ingest/visual.js.map +1 -0
  104. package/dist/parser/docx-parser.d.ts +12 -0
  105. package/dist/parser/docx-parser.d.ts.map +1 -0
  106. package/dist/parser/docx-parser.js +375 -0
  107. package/dist/parser/docx-parser.js.map +1 -0
  108. package/dist/parser/html-parser.d.ts +16 -0
  109. package/dist/parser/html-parser.d.ts.map +1 -0
  110. package/dist/parser/html-parser.js +119 -0
  111. package/dist/parser/html-parser.js.map +1 -0
  112. package/dist/parser/index.d.ts +125 -0
  113. package/dist/parser/index.d.ts.map +1 -0
  114. package/dist/parser/index.js +364 -0
  115. package/dist/parser/index.js.map +1 -0
  116. package/dist/parser/pdf-extract.d.ts +49 -0
  117. package/dist/parser/pdf-extract.d.ts.map +1 -0
  118. package/dist/parser/pdf-extract.js +114 -0
  119. package/dist/parser/pdf-extract.js.map +1 -0
  120. package/dist/parser/pdf-filter.d.ts +91 -0
  121. package/dist/parser/pdf-filter.d.ts.map +1 -0
  122. package/dist/parser/pdf-filter.js +522 -0
  123. package/dist/parser/pdf-filter.js.map +1 -0
  124. package/dist/parser/title-extractor.d.ts +28 -0
  125. package/dist/parser/title-extractor.d.ts.map +1 -0
  126. package/dist/parser/title-extractor.js +105 -0
  127. package/dist/parser/title-extractor.js.map +1 -0
  128. package/dist/pdf-visual/captioner.d.ts +11 -0
  129. package/dist/pdf-visual/captioner.d.ts.map +1 -0
  130. package/dist/pdf-visual/captioner.js +48 -0
  131. package/dist/pdf-visual/captioner.js.map +1 -0
  132. package/dist/pdf-visual/captioners/fast.d.ts +7 -0
  133. package/dist/pdf-visual/captioners/fast.d.ts.map +1 -0
  134. package/dist/pdf-visual/captioners/fast.js +108 -0
  135. package/dist/pdf-visual/captioners/fast.js.map +1 -0
  136. package/dist/pdf-visual/captioners/quality.d.ts +7 -0
  137. package/dist/pdf-visual/captioners/quality.d.ts.map +1 -0
  138. package/dist/pdf-visual/captioners/quality.js +101 -0
  139. package/dist/pdf-visual/captioners/quality.js.map +1 -0
  140. package/dist/pdf-visual/captioners/shared.d.ts +81 -0
  141. package/dist/pdf-visual/captioners/shared.d.ts.map +1 -0
  142. package/dist/pdf-visual/captioners/shared.js +156 -0
  143. package/dist/pdf-visual/captioners/shared.js.map +1 -0
  144. package/dist/pdf-visual/detector.d.ts +9 -0
  145. package/dist/pdf-visual/detector.d.ts.map +1 -0
  146. package/dist/pdf-visual/detector.js +269 -0
  147. package/dist/pdf-visual/detector.js.map +1 -0
  148. package/dist/pdf-visual/index.d.ts +13 -0
  149. package/dist/pdf-visual/index.d.ts.map +1 -0
  150. package/dist/pdf-visual/index.js +53 -0
  151. package/dist/pdf-visual/index.js.map +1 -0
  152. package/dist/pdf-visual/renderer.d.ts +9 -0
  153. package/dist/pdf-visual/renderer.d.ts.map +1 -0
  154. package/dist/pdf-visual/renderer.js +186 -0
  155. package/dist/pdf-visual/renderer.js.map +1 -0
  156. package/dist/pdf-visual/types.d.ts +53 -0
  157. package/dist/pdf-visual/types.d.ts.map +1 -0
  158. package/dist/pdf-visual/types.js +17 -0
  159. package/dist/pdf-visual/types.js.map +1 -0
  160. package/dist/server/error-utils.d.ts +63 -0
  161. package/dist/server/error-utils.d.ts.map +1 -0
  162. package/dist/server/error-utils.js +124 -0
  163. package/dist/server/error-utils.js.map +1 -0
  164. package/dist/server/index.d.ts +216 -0
  165. package/dist/server/index.d.ts.map +1 -0
  166. package/dist/server/index.js +1068 -0
  167. package/dist/server/index.js.map +1 -0
  168. package/dist/server/list-scanner.d.ts +30 -0
  169. package/dist/server/list-scanner.d.ts.map +1 -0
  170. package/dist/server/list-scanner.js +50 -0
  171. package/dist/server/list-scanner.js.map +1 -0
  172. package/dist/server/tool-definitions.d.ts +8 -0
  173. package/dist/server/tool-definitions.d.ts.map +1 -0
  174. package/dist/server/tool-definitions.js +181 -0
  175. package/dist/server/tool-definitions.js.map +1 -0
  176. package/dist/server/tool-input.d.ts +37 -0
  177. package/dist/server/tool-input.d.ts.map +1 -0
  178. package/dist/server/tool-input.js +211 -0
  179. package/dist/server/tool-input.js.map +1 -0
  180. package/dist/server/types.d.ts +307 -0
  181. package/dist/server/types.d.ts.map +1 -0
  182. package/dist/server/types.js +3 -0
  183. package/dist/server/types.js.map +1 -0
  184. package/dist/server-main.d.ts +40 -0
  185. package/dist/server-main.d.ts.map +1 -0
  186. package/dist/server-main.js +254 -0
  187. package/dist/server-main.js.map +1 -0
  188. package/dist/utils/base-dirs.d.ts +145 -0
  189. package/dist/utils/base-dirs.d.ts.map +1 -0
  190. package/dist/utils/base-dirs.js +344 -0
  191. package/dist/utils/base-dirs.js.map +1 -0
  192. package/dist/utils/errors.d.ts +32 -0
  193. package/dist/utils/errors.d.ts.map +1 -0
  194. package/dist/utils/errors.js +61 -0
  195. package/dist/utils/errors.js.map +1 -0
  196. package/dist/utils/limits.d.ts +26 -0
  197. package/dist/utils/limits.d.ts.map +1 -0
  198. package/dist/utils/limits.js +28 -0
  199. package/dist/utils/limits.js.map +1 -0
  200. package/dist/utils/list-sources.d.ts +38 -0
  201. package/dist/utils/list-sources.d.ts.map +1 -0
  202. package/dist/utils/list-sources.js +40 -0
  203. package/dist/utils/list-sources.js.map +1 -0
  204. package/dist/utils/raw-data-utils.d.ts +76 -0
  205. package/dist/utils/raw-data-utils.d.ts.map +1 -0
  206. package/dist/utils/raw-data-utils.js +202 -0
  207. package/dist/utils/raw-data-utils.js.map +1 -0
  208. package/dist/utils/scan.d.ts +114 -0
  209. package/dist/utils/scan.d.ts.map +1 -0
  210. package/dist/utils/scan.js +209 -0
  211. package/dist/utils/scan.js.map +1 -0
  212. package/dist/utils/scope-match.d.ts +38 -0
  213. package/dist/utils/scope-match.d.ts.map +1 -0
  214. package/dist/utils/scope-match.js +86 -0
  215. package/dist/utils/scope-match.js.map +1 -0
  216. package/dist/utils/sensitive-path.d.ts +9 -0
  217. package/dist/utils/sensitive-path.d.ts.map +1 -0
  218. package/dist/utils/sensitive-path.js +78 -0
  219. package/dist/utils/sensitive-path.js.map +1 -0
  220. package/dist/utils/sync-path-key.d.ts +14 -0
  221. package/dist/utils/sync-path-key.d.ts.map +1 -0
  222. package/dist/utils/sync-path-key.js +27 -0
  223. package/dist/utils/sync-path-key.js.map +1 -0
  224. package/dist/utils/type-guards.d.ts +18 -0
  225. package/dist/utils/type-guards.d.ts.map +1 -0
  226. package/dist/utils/type-guards.js +34 -0
  227. package/dist/utils/type-guards.js.map +1 -0
  228. package/dist/utils/visual-profile.d.ts +10 -0
  229. package/dist/utils/visual-profile.d.ts.map +1 -0
  230. package/dist/utils/visual-profile.js +15 -0
  231. package/dist/utils/visual-profile.js.map +1 -0
  232. package/dist/vectordb/index.d.ts +132 -0
  233. package/dist/vectordb/index.d.ts.map +1 -0
  234. package/dist/vectordb/index.js +617 -0
  235. package/dist/vectordb/index.js.map +1 -0
  236. package/dist/vectordb/search-filters.d.ts +15 -0
  237. package/dist/vectordb/search-filters.d.ts.map +1 -0
  238. package/dist/vectordb/search-filters.js +129 -0
  239. package/dist/vectordb/search-filters.js.map +1 -0
  240. package/dist/vectordb/types.d.ts +194 -0
  241. package/dist/vectordb/types.d.ts.map +1 -0
  242. package/dist/vectordb/types.js +237 -0
  243. package/dist/vectordb/types.js.map +1 -0
  244. package/package.json +108 -0
  245. package/skills/mcp-local-rag/SKILL.md +234 -0
  246. package/skills/mcp-local-rag/references/cli-reference.md +190 -0
  247. package/skills/mcp-local-rag/references/html-ingestion.md +78 -0
@@ -0,0 +1,161 @@
1
+ // Shared chunk + embed computation for the ingest pipeline: the single
2
+ // `chunkText -> embedBatch` call site for any ingest path.
3
+ //
4
+ // Persistence (delete + insert + rollback + optimize) stays in each caller,
5
+ // because the rollback semantics differ between the MCP and CLI paths.
6
+ import { createHash, randomUUID } from 'node:crypto';
7
+ import { basename, extname } from 'node:path';
8
+ import { MAX_VISUAL_RENDITION_BYTES } from '../utils/limits.js';
9
+ /** Distance from `offset` to a chunk's source span; 0 when it falls inside. */
10
+ function distanceToSpan(offset, chunk) {
11
+ if (offset < chunk.sourceStart) {
12
+ return chunk.sourceStart - offset;
13
+ }
14
+ if (offset > chunk.sourceEnd) {
15
+ return offset - chunk.sourceEnd;
16
+ }
17
+ return 0;
18
+ }
19
+ export function findNearestChunk(chunks, sourceOffset) {
20
+ let owner;
21
+ let bestDistance = Number.POSITIVE_INFINITY;
22
+ for (const chunk of chunks) {
23
+ const distance = distanceToSpan(sourceOffset, chunk);
24
+ if (!owner) {
25
+ owner = chunk;
26
+ bestDistance = distance;
27
+ continue;
28
+ }
29
+ const isPreceding = chunk.sourceEnd <= sourceOffset;
30
+ const ownerIsPreceding = owner.sourceEnd <= sourceOffset;
31
+ if (distance < bestDistance ||
32
+ (distance === bestDistance && isPreceding && !ownerIsPreceding) ||
33
+ (distance === bestDistance && isPreceding === ownerIsPreceding && chunk.index < owner.index)) {
34
+ owner = chunk;
35
+ bestDistance = distance;
36
+ }
37
+ }
38
+ return owner;
39
+ }
40
+ export function createVisualAttachment(imageIndex, rendition) {
41
+ if (!Number.isInteger(imageIndex) ||
42
+ imageIndex < 0 ||
43
+ rendition.bytes.byteLength === 0 ||
44
+ rendition.bytes.byteLength > MAX_VISUAL_RENDITION_BYTES) {
45
+ throw new Error('Invalid bounded image rendition');
46
+ }
47
+ return {
48
+ imageIndex,
49
+ mimeType: rendition.mimeType,
50
+ data: Buffer.from(rendition.bytes).toString('base64'),
51
+ };
52
+ }
53
+ /**
54
+ * Compute semantic chunks and their embeddings for already-extracted text.
55
+ *
56
+ * Does not fail fast on zero chunks — the MCP handler throws, the CLI warns
57
+ * and returns 0. Chunker and embedder errors propagate verbatim.
58
+ */
59
+ export async function buildChunksAndEmbeddings(text, chunker, embedder, atomicRanges) {
60
+ const chunks = await chunker.chunkText(text, embedder, atomicRanges);
61
+ // F5: Skip `embedBatch` entirely on zero chunks. `embedBatch` runs
62
+ // `ensureInitialized()` (which triggers the ~90MB MiniLM download on a
63
+ // cold cache) BEFORE checking for the empty-array short-circuit, so an
64
+ // empty file would otherwise pay the model-load cost for no work.
65
+ if (chunks.length === 0) {
66
+ return { chunks: [], embeddings: [] };
67
+ }
68
+ const embeddings = await embedder.embedBatch(chunks.map((chunk) => chunk.text));
69
+ return { chunks, embeddings };
70
+ }
71
+ /**
72
+ * Preserve the parser content/range mapping at one shared boundary. Display
73
+ * title handling stays in each dispatch root because it does not affect chunks.
74
+ */
75
+ export async function buildChunksFromParseResult(result, chunker, embedder) {
76
+ const computed = await buildChunksAndEmbeddings(result.content, chunker, embedder, result.atomicRanges);
77
+ const visualAttachments = new Map();
78
+ if (!result.imageAnchors?.length || computed.chunks.length === 0) {
79
+ return { ...computed, visualAttachments, omittedImageCount: 0 };
80
+ }
81
+ const { renderImageRendition } = await import('../pdf-visual/renderer.js');
82
+ let omittedCount = 0;
83
+ for (const anchor of [...result.imageAnchors].sort((left, right) => left.imageIndex - right.imageIndex)) {
84
+ const owner = findNearestChunk(computed.chunks, anchor.offset);
85
+ if (!owner) {
86
+ throw new Error(`Image ${anchor.imageIndex} has no owning chunk`);
87
+ }
88
+ try {
89
+ const rendition = renderImageRendition(anchor.bytes, anchor.mimeType);
90
+ const attachment = createVisualAttachment(anchor.imageIndex, rendition);
91
+ const owned = visualAttachments.get(owner.index) ?? [];
92
+ owned.push(attachment);
93
+ visualAttachments.set(owner.index, owned);
94
+ }
95
+ catch {
96
+ omittedCount += 1;
97
+ }
98
+ }
99
+ return { ...computed, visualAttachments, omittedImageCount: omittedCount };
100
+ }
101
+ /**
102
+ * Content identity of a source file: SHA-256 of its raw BYTES, not of the
103
+ * parsed text, so any caller that can read the file reproduces it — which is
104
+ * what lets a later sync decide "unchanged" without re-parsing.
105
+ */
106
+ export function computeContentHash(bytes) {
107
+ return createHash('sha256').update(bytes).digest('hex');
108
+ }
109
+ /**
110
+ * Build persistable `VectorChunk`s from computed chunks + embeddings — the
111
+ * single chunk→row mapping for both the MCP handler and both CLI paths. One
112
+ * shared `timestamp` per call, a fresh `id`, and `fileName`/`fileType` derived
113
+ * with `node:path` so they hold on Windows.
114
+ *
115
+ * Throws when a chunk has no embedding; the two arrays must align 1:1.
116
+ *
117
+ * `contentHash` is `null` for a chunk set with no source file, and the key is
118
+ * then omitted rather than stored empty, so a hashless row is never mistaken
119
+ * for a real hash. Required, not optional, so a new call site cannot silently
120
+ * write hashless rows.
121
+ *
122
+ * `visualProfile` is the requested visual intent of this ingestion, copied to
123
+ * every row. It accepts `null`/`undefined` and then omits the optional property,
124
+ * so a caller holding a nullable value can pass it directly under
125
+ * `exactOptionalPropertyTypes` and a caller with no visual concept can omit it.
126
+ */
127
+ export function buildVectorChunks(params) {
128
+ const { filePath, chunks, embeddings, fileSize, fileTitle, contentHash, visualAttachments = new Map(), visualProfile = null, } = params;
129
+ const timestamp = new Date().toISOString();
130
+ return chunks.map((chunk, index) => {
131
+ const embedding = embeddings[index];
132
+ if (!embedding) {
133
+ throw new Error(`Missing embedding for chunk ${index}`);
134
+ }
135
+ const attachments = [...(visualAttachments.get(chunk.index) ?? [])]
136
+ .sort((left, right) => left.imageIndex - right.imageIndex)
137
+ .map(({ imageIndex, mimeType, data }) => ({
138
+ imageIndex,
139
+ mimeType,
140
+ data,
141
+ }));
142
+ return {
143
+ id: randomUUID(),
144
+ filePath,
145
+ chunkIndex: chunk.index,
146
+ text: chunk.text,
147
+ vector: embedding,
148
+ metadata: {
149
+ fileName: basename(filePath),
150
+ fileSize,
151
+ fileType: extname(filePath).slice(1),
152
+ },
153
+ fileTitle,
154
+ ...(contentHash === null ? {} : { contentHash }),
155
+ ...(visualProfile === null || visualProfile === undefined ? {} : { visualProfile }),
156
+ visualAttachments: JSON.stringify(attachments),
157
+ timestamp,
158
+ };
159
+ });
160
+ }
161
+ //# sourceMappingURL=compute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compute.js","sourceRoot":"","sources":["../../src/ingest/compute.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { SemanticChunker, TextChunk } from '../chunker/index.js';
2
+ import type { EmbedderInterface } from '../chunker/semantic-chunker.js';
3
+ import type { DocumentParser } from '../parser/index.js';
4
+ import type { QualityProfile } from '../utils/visual-profile.js';
5
+ import type { VectorChunk, VisualAttachment } from '../vectordb/index.js';
6
+ import { type CaptionerConfig } from './visual.js';
7
+ /** Collaborators one file ingest run needs, injected as a unit. */
8
+ export interface FileIngestCollaborators {
9
+ parser: DocumentParser;
10
+ chunker: SemanticChunker;
11
+ embedder: EmbedderInterface;
12
+ }
13
+ export interface PrepareFileForIngestOptions {
14
+ images: boolean;
15
+ captioner?: CaptionerConfig;
16
+ }
17
+ export interface PreparedFileIngest {
18
+ filePath: string;
19
+ chunks: TextChunk[];
20
+ embeddings: number[][];
21
+ textLength: number;
22
+ title: string | null;
23
+ contentHash: string;
24
+ /** Requested visual intent of this run: a profile for a captioned PDF, `null` otherwise. */
25
+ visualProfile: QualityProfile | null;
26
+ visualAttachments: Map<number, VisualAttachment[]>;
27
+ omittedImageCount: number;
28
+ }
29
+ export declare function buildPreparedFileVectorChunks(prepared: PreparedFileIngest): VectorChunk[];
30
+ /**
31
+ * Prepare one validated source file for persistence without touching storage or
32
+ * process output. The source hash is read before parsing so a concurrent rewrite
33
+ * remains visible to the next sync instead of being paired with stale chunks.
34
+ *
35
+ * The visual profile is resolved from the request before any caption outcome is
36
+ * known, so it records what was asked for: a run with no qualifying region or a
37
+ * tolerated caption failure still carries the requested profile.
38
+ */
39
+ export declare function prepareFileForIngest(filePath: string, collaborators: FileIngestCollaborators, options: PrepareFileForIngestOptions): Promise<PreparedFileIngest>;
40
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/ingest/file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAEzE,OAAO,EACL,KAAK,eAAe,EAGrB,MAAM,aAAa,CAAA;AAEpB,mEAAmE;AACnE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,eAAe,CAAA;IACxB,QAAQ,EAAE,iBAAiB,CAAA;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,UAAU,EAAE,MAAM,EAAE,EAAE,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,4FAA4F;IAC5F,aAAa,EAAE,cAAc,GAAG,IAAI,CAAA;IACpC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAA;IAClD,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,kBAAkB,GAAG,WAAW,EAAE,CAWzF;AAQD;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,uBAAuB,EACtC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,kBAAkB,CAAC,CA6C7B"}
@@ -0,0 +1,74 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { buildChunksFromParseResult, buildVectorChunks, computeContentHash } from './compute.js';
3
+ import { prepareVisualPdfChunks, } from './visual.js';
4
+ export function buildPreparedFileVectorChunks(prepared) {
5
+ return buildVectorChunks({
6
+ filePath: prepared.filePath,
7
+ chunks: prepared.chunks,
8
+ embeddings: prepared.embeddings,
9
+ fileSize: prepared.textLength,
10
+ fileTitle: prepared.title,
11
+ contentHash: prepared.contentHash,
12
+ visualProfile: prepared.visualProfile,
13
+ visualAttachments: prepared.visualAttachments,
14
+ });
15
+ }
16
+ async function readPreParseContentHash(filePath, parser) {
17
+ await parser.validateFilePath(filePath);
18
+ parser.validateFileSize(filePath);
19
+ return computeContentHash(await readFile(filePath));
20
+ }
21
+ /**
22
+ * Prepare one validated source file for persistence without touching storage or
23
+ * process output. The source hash is read before parsing so a concurrent rewrite
24
+ * remains visible to the next sync instead of being paired with stale chunks.
25
+ *
26
+ * The visual profile is resolved from the request before any caption outcome is
27
+ * known, so it records what was asked for: a run with no qualifying region or a
28
+ * tolerated caption failure still carries the requested profile.
29
+ */
30
+ export async function prepareFileForIngest(filePath, collaborators, options) {
31
+ const { parser, chunker, embedder } = collaborators;
32
+ const contentHash = await readPreParseContentHash(filePath, parser);
33
+ const isPdf = filePath.toLowerCase().endsWith('.pdf');
34
+ let text;
35
+ let title;
36
+ let chunks;
37
+ let embeddings;
38
+ let visualAttachments;
39
+ let omittedImageCount;
40
+ if (isPdf && (options.captioner !== undefined || options.images)) {
41
+ const visualOptions = {
42
+ images: options.images,
43
+ ...(options.captioner === undefined ? {} : { captioner: options.captioner }),
44
+ };
45
+ const result = await prepareVisualPdfChunks(filePath, collaborators, visualOptions);
46
+ text = result.text;
47
+ title = result.title;
48
+ chunks = result.chunks;
49
+ embeddings = result.embeddings;
50
+ visualAttachments = result.visualAttachments;
51
+ omittedImageCount = result.omittedImageCount;
52
+ }
53
+ else {
54
+ const result = isPdf
55
+ ? await parser.parsePdf(filePath, embedder)
56
+ : await parser.parseFile(filePath, { images: options.images });
57
+ text = result.content;
58
+ title = result.title || null;
59
+ ({ chunks, embeddings, visualAttachments, omittedImageCount } =
60
+ await buildChunksFromParseResult(result, chunker, embedder));
61
+ }
62
+ return {
63
+ filePath,
64
+ chunks,
65
+ embeddings,
66
+ textLength: text.length,
67
+ title,
68
+ contentHash,
69
+ visualProfile: isPdf ? (options.captioner?.profile ?? null) : null,
70
+ visualAttachments,
71
+ omittedImageCount,
72
+ };
73
+ }
74
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/ingest/file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { AtomicTextRange, SemanticChunker, TextChunk } from '../chunker/index.js';
2
+ import type { EmbedderInterface } from '../chunker/semantic-chunker.js';
3
+ import type { DocumentParser } from '../parser/index.js';
4
+ import type { FilteredTextFragment } from '../parser/pdf-filter.js';
5
+ import type { ProcessedVisualRegion } from '../pdf-visual/types.js';
6
+ import type { QualityProfile } from '../utils/visual-profile.js';
7
+ import type { VisualAttachment } from '../vectordb/types.js';
8
+ export interface VisualPdfParser {
9
+ parsePdfPages: DocumentParser['parsePdfPages'];
10
+ }
11
+ export interface CaptionerConfig {
12
+ profile: QualityProfile;
13
+ cacheDir: string;
14
+ device?: string | undefined;
15
+ }
16
+ /** Collaborators one visual PDF ingest run needs, injected as a unit. */
17
+ export interface VisualIngestCollaborators {
18
+ parser: VisualPdfParser;
19
+ chunker: SemanticChunker;
20
+ embedder: EmbedderInterface;
21
+ }
22
+ export interface PrepareVisualPdfChunksOptions {
23
+ images: boolean;
24
+ captioner?: CaptionerConfig;
25
+ }
26
+ export interface PrepareVisualPdfChunksResult {
27
+ chunks: TextChunk[];
28
+ embeddings: number[][];
29
+ title: string | null;
30
+ text: string;
31
+ atomicRanges: AtomicTextRange[];
32
+ visualAttachments: Map<number, VisualAttachment[]>;
33
+ omittedImageCount: number;
34
+ }
35
+ export interface OrderedVisualPage {
36
+ pageNum: number;
37
+ text: string;
38
+ textFragments: readonly FilteredTextFragment[];
39
+ }
40
+ export interface OrderedVisualRegion extends ProcessedVisualRegion {
41
+ visualIndex: number;
42
+ anchorOffset: number;
43
+ captionRange?: AtomicTextRange;
44
+ }
45
+ export interface OrderedVisualDocument {
46
+ text: string;
47
+ atomicRanges: AtomicTextRange[];
48
+ regions: OrderedVisualRegion[];
49
+ }
50
+ export declare function buildOrderedVisualDocument(pages: readonly OrderedVisualPage[], processedRegions: readonly ProcessedVisualRegion[]): OrderedVisualDocument;
51
+ export declare function prepareVisualPdfChunks(filePath: string, collaborators: VisualIngestCollaborators, options: PrepareVisualPdfChunksOptions): Promise<PrepareVisualPdfChunksResult>;
52
+ //# sourceMappingURL=visual.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../src/ingest/visual.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AACtF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AACnE,OAAO,KAAK,EAAwB,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACzF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAG5D,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,cAAc,CAAC,eAAe,CAAC,CAAA;CAC/C;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,yEAAyE;AACzE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,eAAe,CAAA;IACvB,OAAO,EAAE,eAAe,CAAA;IACxB,QAAQ,EAAE,iBAAiB,CAAA;CAC5B;AAED,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,UAAU,EAAE,MAAM,EAAE,EAAE,CAAA;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,eAAe,EAAE,CAAA;IAC/B,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAA;IAClD,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,SAAS,oBAAoB,EAAE,CAAA;CAC/C;AAED,MAAM,WAAW,mBAAoB,SAAQ,qBAAqB;IAChE,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,eAAe,EAAE,CAAA;IAC/B,OAAO,EAAE,mBAAmB,EAAE,CAAA;CAC/B;AAiND,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,gBAAgB,EAAE,SAAS,qBAAqB,EAAE,GACjD,qBAAqB,CAmCvB;AAoHD,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,yBAAyB,EACxC,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,4BAA4B,CAAC,CAyCvC"}
@@ -0,0 +1,294 @@
1
+ import { buildChunksAndEmbeddings, createVisualAttachment, findNearestChunk } from './compute.js';
2
+ function horizontalOverlap(left, right) {
3
+ return Math.min(left[2] ?? 0, right[2] ?? 0) >= Math.max(left[0] ?? 0, right[0] ?? 0);
4
+ }
5
+ function xCenter(bbox) {
6
+ return ((bbox[0] ?? 0) + (bbox[2] ?? 0)) / 2;
7
+ }
8
+ function yCenter(bbox) {
9
+ return ((bbox[1] ?? 0) + (bbox[3] ?? 0)) / 2;
10
+ }
11
+ function findInsertionIndex(fragments, region) {
12
+ if (fragments.length === 0) {
13
+ return 0;
14
+ }
15
+ let candidates = fragments
16
+ .map((fragment, index) => ({ fragment, index }))
17
+ .filter(({ fragment }) => horizontalOverlap(fragment.bbox, region.bbox));
18
+ if (candidates.length === 0) {
19
+ const regionCenter = xCenter(region.bbox);
20
+ const minimumDistance = Math.min(...fragments.map((fragment) => Math.abs(xCenter(fragment.bbox) - regionCenter)));
21
+ candidates = fragments
22
+ .map((fragment, index) => ({ fragment, index }))
23
+ .filter(({ fragment }) => Math.abs(xCenter(fragment.bbox) - regionCenter) === minimumDistance);
24
+ }
25
+ const regionMidpoint = yCenter(region.bbox);
26
+ const preceding = candidates.filter(({ fragment }) => yCenter(fragment.bbox) <= regionMidpoint);
27
+ if (preceding.length > 0) {
28
+ return Math.max(...preceding.map(({ index }) => index + 1));
29
+ }
30
+ return Math.min(...candidates.map(({ index }) => index));
31
+ }
32
+ function fallbackFragments(page) {
33
+ if (page.textFragments.length > 0 || page.text.length === 0) {
34
+ return page.textFragments;
35
+ }
36
+ return [
37
+ {
38
+ pageNum: page.pageNum,
39
+ blockOrdinal: 0,
40
+ lineOrdinal: 0,
41
+ fragmentOrdinal: 0,
42
+ bbox: [0, 0, 0, 0],
43
+ text: page.text,
44
+ pageTextStart: 0,
45
+ pageTextEnd: page.text.length,
46
+ },
47
+ ];
48
+ }
49
+ /** Regions on one page, ordered by insertion slot then detection order. */
50
+ function positionRegions(page, pageFragments, processedRegions) {
51
+ return processedRegions
52
+ .filter((region) => region.pageNum === page.pageNum)
53
+ .map((region) => ({ region, insertionIndex: findInsertionIndex(pageFragments, region) }))
54
+ .sort((left, right) => left.insertionIndex - right.insertionIndex ||
55
+ left.region.detectionIndex - right.region.detectionIndex);
56
+ }
57
+ /**
58
+ * Group regions by the character offset in the page text they are inserted at,
59
+ * so regions landing in the same gap are emitted together.
60
+ */
61
+ function groupByInsertionOffset(page, pageFragments, positioned) {
62
+ const groups = new Map();
63
+ for (const item of positioned) {
64
+ const preceding = pageFragments[item.insertionIndex - 1];
65
+ const following = pageFragments[item.insertionIndex];
66
+ const offset = Math.max(0, Math.min(page.text.length, preceding?.pageTextEnd ?? following?.pageTextStart ?? 0));
67
+ const group = groups.get(offset) ?? [];
68
+ group.push(item.region);
69
+ groups.set(offset, group);
70
+ }
71
+ return groups;
72
+ }
73
+ /** Newlines needed so `adjacent` is separated by a blank line. */
74
+ function blankLinePadding(adjacent) {
75
+ const present = adjacent.match(/^\n*/)?.[0].length ?? 0;
76
+ return '\n'.repeat(Math.max(0, 2 - present));
77
+ }
78
+ /**
79
+ * Append one insertion group's captions to `pageText`, recording each region's
80
+ * caption range and anchor offset. Uncaptioned regions only get an anchor.
81
+ */
82
+ function appendGroupCaptions(pageText, group, context) {
83
+ const { page, pageVisualIndices, ...into } = context;
84
+ let text = pageText;
85
+ let captionIndex = 0;
86
+ for (const region of group) {
87
+ if (region.caption === null) {
88
+ into.anchorOffsets.set(region, text.length);
89
+ continue;
90
+ }
91
+ if (captionIndex > 0) {
92
+ text += '\n\n';
93
+ }
94
+ const start = text.length;
95
+ text += `[Visual content on page ${page.pageNum}, visual ${pageVisualIndices.get(region)}: ${region.caption}]`;
96
+ into.captionRanges.set(region, { start, end: text.length });
97
+ into.anchorOffsets.set(region, start);
98
+ captionIndex += 1;
99
+ }
100
+ return text;
101
+ }
102
+ function renderPageWithCaptions(page, insertionGroups, pageVisualIndices) {
103
+ let pageText = '';
104
+ let pageCursor = 0;
105
+ const captionRanges = new Map();
106
+ const anchorOffsets = new Map();
107
+ for (const [offset, group] of [...insertionGroups].sort((left, right) => left[0] - right[0])) {
108
+ pageText += page.text.slice(pageCursor, offset);
109
+ const hasCaption = group.some((region) => region.caption !== null);
110
+ if (hasCaption && pageText.length > 0) {
111
+ pageText += blankLinePadding(pageText.match(/\n*$/)?.[0] ?? '');
112
+ }
113
+ pageText = appendGroupCaptions(pageText, group, {
114
+ page,
115
+ pageVisualIndices,
116
+ captionRanges,
117
+ anchorOffsets,
118
+ });
119
+ if (hasCaption && offset < page.text.length) {
120
+ pageText += blankLinePadding(page.text.slice(offset));
121
+ }
122
+ pageCursor = offset;
123
+ }
124
+ pageText += page.text.slice(pageCursor);
125
+ return { pageText, captionRanges, anchorOffsets };
126
+ }
127
+ /** Rebase one page-relative region onto the whole-document text. */
128
+ function toOrderedRegion(region, rendered, pageStart, visualIndex) {
129
+ const relative = rendered.captionRanges.get(region);
130
+ const captionRange = relative
131
+ ? { start: pageStart + relative.start, end: pageStart + relative.end }
132
+ : undefined;
133
+ const anchorOffset = rendered.anchorOffsets.get(region);
134
+ if (anchorOffset === undefined) {
135
+ throw new Error('Visual region has no anchor offset');
136
+ }
137
+ return {
138
+ ...region,
139
+ visualIndex,
140
+ anchorOffset: pageStart + anchorOffset,
141
+ ...(captionRange ? { captionRange } : {}),
142
+ };
143
+ }
144
+ export function buildOrderedVisualDocument(pages, processedRegions) {
145
+ let text = '';
146
+ let visualIndex = 0;
147
+ const atomicRanges = [];
148
+ const regions = [];
149
+ const consumedRegions = new Set();
150
+ for (const page of pages) {
151
+ const pageFragments = fallbackFragments(page);
152
+ const positioned = positionRegions(page, pageFragments, processedRegions);
153
+ const pageVisualIndices = new Map(positioned.map((item, index) => [item.region, visualIndex + index]));
154
+ const insertionGroups = groupByInsertionOffset(page, pageFragments, positioned);
155
+ const rendered = renderPageWithCaptions(page, insertionGroups, pageVisualIndices);
156
+ const pageSeparator = text.length > 0 && rendered.pageText.length > 0 ? '\n\n' : '';
157
+ const pageStart = text.length + pageSeparator.length;
158
+ text += pageSeparator + rendered.pageText;
159
+ for (const positionedRegion of positioned) {
160
+ const record = toOrderedRegion(positionedRegion.region, rendered, pageStart, visualIndex++);
161
+ if (record.captionRange) {
162
+ atomicRanges.push(record.captionRange);
163
+ }
164
+ regions.push(record);
165
+ consumedRegions.add(positionedRegion.region);
166
+ }
167
+ }
168
+ if (consumedRegions.size !== processedRegions.length) {
169
+ throw new Error('A processed visual region has no matching PDF page');
170
+ }
171
+ return { text, atomicRanges, regions };
172
+ }
173
+ function assignVisualAttachments(document, chunks, attachments) {
174
+ const attachmentsByChunkIndex = new Map();
175
+ if (chunks.length === 0) {
176
+ return attachmentsByChunkIndex;
177
+ }
178
+ const regionByVisualIndex = new Map(document.regions.map((region) => [region.visualIndex, region]));
179
+ const seenVisualIndices = new Set();
180
+ for (const attachment of [...attachments].sort((left, right) => left.imageIndex - right.imageIndex)) {
181
+ if (seenVisualIndices.has(attachment.imageIndex)) {
182
+ throw new Error(`Duplicate visual attachment index: ${attachment.imageIndex}`);
183
+ }
184
+ seenVisualIndices.add(attachment.imageIndex);
185
+ const region = regionByVisualIndex.get(attachment.imageIndex);
186
+ if (!region) {
187
+ throw new Error(`Visual attachment has no ordered region: ${attachment.imageIndex}`);
188
+ }
189
+ const owner = findNearestChunk(chunks, region.anchorOffset);
190
+ if (!owner) {
191
+ throw new Error(`Visual ${attachment.imageIndex} has no owning chunk`);
192
+ }
193
+ const current = attachmentsByChunkIndex.get(owner.index) ?? [];
194
+ current.push(attachment);
195
+ attachmentsByChunkIndex.set(owner.index, current);
196
+ }
197
+ return attachmentsByChunkIndex;
198
+ }
199
+ async function processImageOnlyRegions(regions, doc) {
200
+ const renderer = await import('../pdf-visual/renderer.js');
201
+ const processed = [];
202
+ let omittedImageCount = 0;
203
+ for (const region of regions) {
204
+ try {
205
+ const image = await renderer.renderPdfRendition(doc, region.pageNum, region.bbox, region.evidence);
206
+ processed.push({ ...region, caption: null, rendition: image });
207
+ }
208
+ catch {
209
+ omittedImageCount += 1;
210
+ processed.push({ ...region, caption: null });
211
+ }
212
+ }
213
+ return { processed, omittedImageCount };
214
+ }
215
+ /**
216
+ * Detect visual regions and turn them into processed ones. With a captioner
217
+ * configured the VLM barrel is loaded and every region is captioned; without
218
+ * one only the lighter detector is loaded and regions carry images alone.
219
+ */
220
+ async function detectAndProcessRegions(pages, doc, captionerConfig, includeImages) {
221
+ const pageRefs = pages.map((page) => ({ pageNum: page.pageNum, stextJson: page.stextJson }));
222
+ if (captionerConfig === undefined) {
223
+ const detector = await import('../pdf-visual/detector.js');
224
+ return processImageOnlyRegions(detector.detectVisualRegions(pageRefs, doc), doc);
225
+ }
226
+ const pdfVisual = await import('../pdf-visual/index.js');
227
+ const regions = pdfVisual.detectVisualRegions(pageRefs, doc);
228
+ const captioner = pdfVisual.createCaptioner(captionerConfig);
229
+ try {
230
+ const processed = await pdfVisual.processVisualRegions(regions, doc, {
231
+ captioner,
232
+ includeImages,
233
+ });
234
+ return { processed, omittedImageCount: 0 };
235
+ }
236
+ finally {
237
+ await captioner.dispose();
238
+ }
239
+ }
240
+ /** Encode every rendered region; one that cannot be encoded is omitted, not fatal. */
241
+ function collectAttachments(regions) {
242
+ const attachments = [];
243
+ let omittedImageCount = 0;
244
+ for (const region of regions) {
245
+ if (!region.rendition) {
246
+ continue;
247
+ }
248
+ try {
249
+ attachments.push(createVisualAttachment(region.visualIndex, region.rendition));
250
+ }
251
+ catch {
252
+ omittedImageCount += 1;
253
+ }
254
+ }
255
+ return { attachments, omittedImageCount };
256
+ }
257
+ export async function prepareVisualPdfChunks(filePath, collaborators, options) {
258
+ const { parser, chunker, embedder } = collaborators;
259
+ const captionerConfig = options.captioner;
260
+ const { doc, title, pages } = await parser.parsePdfPages(filePath, embedder);
261
+ try {
262
+ let omittedImageCount = 0;
263
+ const detected = await detectAndProcessRegions(pages, doc, captionerConfig, options.images);
264
+ const processed = detected.processed;
265
+ omittedImageCount += detected.omittedImageCount;
266
+ const ordered = buildOrderedVisualDocument(pages, processed);
267
+ const { chunks, embeddings } = await buildChunksAndEmbeddings(ordered.text, chunker, embedder, ordered.atomicRanges);
268
+ const collected = options.images
269
+ ? collectAttachments(ordered.regions)
270
+ : { attachments: [], omittedImageCount: 0 };
271
+ const attachments = collected.attachments;
272
+ omittedImageCount += collected.omittedImageCount;
273
+ const visualAttachments = assignVisualAttachments(ordered, chunks, attachments);
274
+ return {
275
+ chunks,
276
+ embeddings,
277
+ title,
278
+ text: ordered.text,
279
+ atomicRanges: ordered.atomicRanges,
280
+ visualAttachments,
281
+ omittedImageCount,
282
+ };
283
+ }
284
+ finally {
285
+ try {
286
+ doc.destroy();
287
+ }
288
+ catch (destroyError) {
289
+ const message = destroyError instanceof Error ? destroyError.message : String(destroyError);
290
+ console.warn(`prepareVisualPdfChunks: doc.destroy() failed: ${message}`);
291
+ }
292
+ }
293
+ }
294
+ //# sourceMappingURL=visual.js.map