@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,105 @@
1
+ // Title Extractor - Per-format document title extraction
2
+ // Title is display-only metadata (NOT used for search scoring)
3
+ // ============================================
4
+ // Constants
5
+ // ============================================
6
+ /** Minimum font size (pt) for page-1 largest-font text to be treated as a title. */
7
+ const TITLE_MIN_FONT_SIZE = 14;
8
+ // ============================================
9
+ // Shared Helper
10
+ // ============================================
11
+ /** `2024-annual-report.pdf` -> `2024 annual report`. */
12
+ export function fileNameToTitle(fileName) {
13
+ // Strip extension (last dot and everything after)
14
+ const lastDotIndex = fileName.lastIndexOf('.');
15
+ const nameWithoutExt = lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
16
+ // Replace hyphens and underscores with spaces
17
+ return nameWithoutExt.replace(/[-_]/g, ' ');
18
+ }
19
+ // ============================================
20
+ // Per-Format Extractors
21
+ // ============================================
22
+ /** Priority: YAML frontmatter title -> first `#` H1 -> file name. */
23
+ export function extractMarkdownTitle(text, fileName) {
24
+ // 1. Try YAML frontmatter
25
+ const frontmatterMatch = text.match(/^---\n[\s\S]*?title:\s*['"]?(.+?)['"]?\s*\n[\s\S]*?---/);
26
+ if (frontmatterMatch?.[1]) {
27
+ return { title: frontmatterMatch[1].trim(), source: 'metadata' };
28
+ }
29
+ // 2. Try first H1 heading
30
+ const h1Match = text.match(/^# (.+)$/m);
31
+ if (h1Match?.[1]) {
32
+ return { title: h1Match[1].trim(), source: 'content' };
33
+ }
34
+ // 3. Fall back to file name
35
+ return { title: fileNameToTitle(fileName), source: 'filename' };
36
+ }
37
+ /** Priority: a first line followed by a blank line -> file name. */
38
+ export function extractTxtTitle(text, fileName) {
39
+ // Try first line followed by empty line
40
+ if (text.length > 0) {
41
+ const lines = text.split('\n');
42
+ const firstLine = lines[0];
43
+ const secondLine = lines[1];
44
+ if (firstLine !== undefined &&
45
+ secondLine !== undefined &&
46
+ firstLine.trim().length > 0 &&
47
+ secondLine.trim().length === 0) {
48
+ return { title: firstLine.trim(), source: 'content' };
49
+ }
50
+ }
51
+ // Fall back to file name
52
+ return { title: fileNameToTitle(fileName), source: 'filename' };
53
+ }
54
+ /** Priority: Readability title -> file name. */
55
+ export function extractHtmlTitle(readabilityTitle, fileName) {
56
+ if (readabilityTitle && readabilityTitle.trim().length > 0) {
57
+ return { title: readabilityTitle.trim(), source: 'content' };
58
+ }
59
+ // Fall back to file name
60
+ return { title: fileNameToTitle(fileName), source: 'filename' };
61
+ }
62
+ /**
63
+ * Priority: PDF metadata `/Title` -> page-1 chunk 0 -> file name.
64
+ *
65
+ * A metadata title that looks like a file path (contains `/` or `\`) is
66
+ * rejected, since some producers write the source path there.
67
+ */
68
+ export function extractPdfTitle(metadataTitle, firstPageChunkText, fileName, firstPageFontHint) {
69
+ // 1. Try PDF metadata title (reject file paths and empty values)
70
+ if (metadataTitle && metadataTitle.trim().length > 0) {
71
+ const trimmed = metadataTitle.trim();
72
+ const looksLikeFilePath = trimmed.includes('/') || trimmed.includes('\\');
73
+ if (!looksLikeFilePath) {
74
+ return { title: trimmed, source: 'metadata' };
75
+ }
76
+ }
77
+ // 2. Try largest-font text from page 1 (font size > threshold indicates title)
78
+ if (firstPageFontHint &&
79
+ firstPageFontHint.fontSize > TITLE_MIN_FONT_SIZE &&
80
+ firstPageFontHint.text.trim().length > 0) {
81
+ return { title: firstPageFontHint.text.trim(), source: 'content' };
82
+ }
83
+ // 3. Try first chunk from page 1 semantic chunking
84
+ if (firstPageChunkText && firstPageChunkText.trim().length > 0) {
85
+ return { title: firstPageChunkText.trim(), source: 'content' };
86
+ }
87
+ // 4. Fall back to file name
88
+ return { title: fileNameToTitle(fileName), source: 'filename' };
89
+ }
90
+ /** Priority: DOCX core title -> first non-empty `<h1>` -> file name. */
91
+ export function extractDocxTitle(document, fileName, metadataTitle) {
92
+ const normalizedMetadataTitle = metadataTitle?.replace(/\s+/g, ' ').trim();
93
+ if (normalizedMetadataTitle) {
94
+ return { title: normalizedMetadataTitle, source: 'metadata' };
95
+ }
96
+ for (const heading of document.querySelectorAll('h1')) {
97
+ const title = (heading.textContent ?? '').replace(/\s+/g, ' ').trim();
98
+ if (title) {
99
+ return { title, source: 'content' };
100
+ }
101
+ }
102
+ // Fall back to file name
103
+ return { title: fileNameToTitle(fileName), source: 'filename' };
104
+ }
105
+ //# sourceMappingURL=title-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"title-extractor.js","sourceRoot":"","sources":["../../src/parser/title-extractor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Captioner, CaptionerConfig } from './types.js';
2
+ /**
3
+ * Create a captioner for the requested profile, setting `env.cacheDir`
4
+ * immediately so the global is right even if no embedder has initialized.
5
+ *
6
+ * `env.cacheDir` is a transformers.js process global, so constructing two
7
+ * captioners with DIFFERENT cacheDirs in parallel would let the last writer
8
+ * win. Safe for the current one-captioner-per-run usage.
9
+ */
10
+ export declare function createCaptioner(config: CaptionerConfig): Captioner;
11
+ //# sourceMappingURL=captioner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captioner.d.ts","sourceRoot":"","sources":["../../src/pdf-visual/captioner.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE5D;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CA6BlE"}
@@ -0,0 +1,48 @@
1
+ // Captioner dispatcher for the visual ingest path. Each profile is a
2
+ // self-contained module under `./captioners/`, so prompt, chat template,
3
+ // processor signature, generation options and model class stay coherent per
4
+ // profile.
5
+ //
6
+ // `env.cacheDir` is set once here rather than in the per-profile modules, so
7
+ // the shared global is configured before either `from_pretrained` runs and the
8
+ // profiles stay free of the side effect.
9
+ import { env } from '@huggingface/transformers';
10
+ import { createFastCaptioner } from './captioners/fast.js';
11
+ import { createQualityCaptioner } from './captioners/quality.js';
12
+ /**
13
+ * Create a captioner for the requested profile, setting `env.cacheDir`
14
+ * immediately so the global is right even if no embedder has initialized.
15
+ *
16
+ * `env.cacheDir` is a transformers.js process global, so constructing two
17
+ * captioners with DIFFERENT cacheDirs in parallel would let the last writer
18
+ * win. Safe for the current one-captioner-per-run usage.
19
+ */
20
+ export function createCaptioner(config) {
21
+ // Defensive ordering: set the global cacheDir at construction so the very
22
+ // first `from_pretrained` call sees the right value. Setting the same
23
+ // global twice with the same value is idempotent (shared with the
24
+ // embedder).
25
+ env.cacheDir = config.cacheDir;
26
+ if (process.env['HF_ENDPOINT']) {
27
+ env.remoteHost = process.env['HF_ENDPOINT'];
28
+ }
29
+ const resolvedDevice = config.device || 'cpu';
30
+ switch (config.profile) {
31
+ case 'fast':
32
+ return createFastCaptioner(resolvedDevice);
33
+ case 'quality':
34
+ // No silent fallback to `fast`. A load failure surfaces as a `VlmError`
35
+ // per page and the file still ingests text-only, so a misconfigured
36
+ // install degrades visibly instead of being masked by a quieter model.
37
+ return createQualityCaptioner(resolvedDevice);
38
+ default: {
39
+ // Exhaustiveness guard. `QualityProfile` is statically narrow at the
40
+ // call sites (CLI + MCP both validate before reaching here) so this
41
+ // branch is unreachable today; the throw is defensive for future
42
+ // ProfileType additions that forget to extend this switch.
43
+ const _exhaustive = config.profile;
44
+ throw new Error(`Unknown QualityProfile: ${String(_exhaustive)}`);
45
+ }
46
+ }
47
+ }
48
+ //# sourceMappingURL=captioner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captioner.js","sourceRoot":"","sources":["../../src/pdf-visual/captioner.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,yEAAyE;AACzE,4EAA4E;AAC5E,WAAW;AACX,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,yCAAyC;AAEzC,OAAO,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAA;AAE/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAGhE;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,MAAuB;IACrD,0EAA0E;IAC1E,sEAAsE;IACtE,kEAAkE;IAClE,aAAa;IACb,GAAG,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IAC9B,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,IAAI,KAAK,CAAA;IAE7C,QAAQ,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,KAAK,MAAM;YACT,OAAO,mBAAmB,CAAC,cAAc,CAAC,CAAA;QAC5C,KAAK,SAAS;YACZ,wEAAwE;YACxE,oEAAoE;YACpE,uEAAuE;YACvE,OAAO,sBAAsB,CAAC,cAAc,CAAC,CAAA;QAC/C,SAAS,CAAC;YACR,qEAAqE;YACrE,oEAAoE;YACpE,iEAAiE;YACjE,2DAA2D;YAC3D,MAAM,WAAW,GAAU,MAAM,CAAC,OAAO,CAAA;YACzC,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACnE,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Captioner } from '../types.js';
2
+ /**
3
+ * Create a `fast` profile captioner. The dispatcher has already configured
4
+ * `env.cacheDir`; this profile only owns lazy model loading and inference.
5
+ */
6
+ export declare function createFastCaptioner(resolvedDevice: string): Captioner;
7
+ //# sourceMappingURL=fast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fast.d.ts","sourceRoot":"","sources":["../../../src/pdf-visual/captioners/fast.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAwB5C;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAiErE"}
@@ -0,0 +1,108 @@
1
+ // `fast` visual-quality profile — SmolVLM-256M-Instruct / IDEFICS3.
2
+ //
3
+ // Shares the profile-agnostic load/decode mechanics with `quality` via
4
+ // `shared.ts` (`createModelLoader`, `decodePngToRawImage`, `VLM_DTYPE`,
5
+ // post-processing). The profile-specific parts — model class, prompt,
6
+ // processor call shape, and generation options — stay here so `fast` and
7
+ // `quality` can diverge on exactly the parts that genuinely differ.
8
+ //
9
+ // Implementation contract (matches HEAD captioner contract steps 2–8):
10
+ // 1. Lazy-load processor + model on first `caption()` call with the pinned
11
+ // `VLM_DTYPE` and the resolved device. `env.cacheDir` is set by the
12
+ // dispatcher (`captioner.ts`) before this profile is constructed.
13
+ // 2. Decode PNG bytes via `RawImage.fromBlob(new Blob([pngBytes], { type:
14
+ // 'image/png' }))`. SmolVLM-256M is NOT resized client-side — IDEFICS3's
15
+ // processor handles its own preprocessing.
16
+ // 3. Build chat-style input via `processor.apply_chat_template(messages,
17
+ // { add_generation_prompt: true })` with the IDEFICS3 conversation shape
18
+ // `[{role:'user', content:[{type:'image'},{type:'text',text:...}]}]`.
19
+ // Probe-verified.
20
+ // 4. Call `model.generate({ ...inputs, max_new_tokens: 128,
21
+ // repetition_penalty: 1.15, no_repeat_ngram_size: 3 })` — these
22
+ // generation options are tuned for SmolVLM-256M and MUST stay on the
23
+ // `fast` profile (they cause forced variant generation on Qwen2.5-VL,
24
+ // which is why the `quality` profile drops them).
25
+ // 5. Decode via `processor.batch_decode(newTokens, { skip_special_tokens: true })`
26
+ // where `newTokens = outputs.slice(null, [inputs.input_ids.dims[1], null])`.
27
+ // 6. Post-processing via `shared.postProcess` (control-char strip, trim,
28
+ // empty → `null`, length > 1000 → truncate + `…`).
29
+ // 7. On model load / image decode / generation failure throw `VlmError`
30
+ // with `pageNum` + `cause`.
31
+ import { AutoModelForImageTextToText, AutoProcessor } from '@huggingface/transformers';
32
+ import { VlmError } from '../types.js';
33
+ import { asVlmModel, asVlmProcessor, createModelLoader, decodePngToRawImage, postProcess, } from './shared.js';
34
+ const MODEL_NAME = 'HuggingFaceTB/SmolVLM-256M-Instruct';
35
+ /**
36
+ * Static prompt — tuned for "describe for search retrieval" not "describe for
37
+ * a blind reader". It asks for retrieval-relevant detail rather than a short
38
+ * summary, while keeping claims grounded in visible evidence.
39
+ */
40
+ const PROMPT = 'Write search text for this PDF page image. Include visible section names, visual titles, ' +
41
+ 'headings, labels, legends, axes, row or column names, UI text, metric names, identifiers, ' +
42
+ 'proper nouns, and flow or diagram step names. Prefer exact readable words from the image. ' +
43
+ 'Cover the main visual regions across the page. Use short searchable phrases separated by ' +
44
+ 'commas or semicolons. Use only readable or visually evident details. Use each phrase once.';
45
+ /**
46
+ * Create a `fast` profile captioner. The dispatcher has already configured
47
+ * `env.cacheDir`; this profile only owns lazy model loading and inference.
48
+ */
49
+ export function createFastCaptioner(resolvedDevice) {
50
+ // Both classes accept `{ dtype }` (probe-verified). They load in sequence
51
+ // because the second resolves the runtime class
52
+ // (`Idefics3ForConditionalGeneration` for the default model) via the
53
+ // architecture-agnostic `AutoModelForImageTextToText` entry point.
54
+ const loader = createModelLoader(MODEL_NAME, resolvedDevice, async ({ dtypeOpt, modelOpt }) => {
55
+ const processor = await AutoProcessor.from_pretrained(MODEL_NAME, dtypeOpt);
56
+ const model = await AutoModelForImageTextToText.from_pretrained(MODEL_NAME, modelOpt);
57
+ return { processor, model };
58
+ });
59
+ return {
60
+ dispose: loader.dispose,
61
+ async caption(pngBytes, pageNum) {
62
+ try {
63
+ const { processor, model } = await loader.ensureLoaded();
64
+ // Decode PNG → RawImage. SmolVLM-256M is NOT resized client-side —
65
+ // IDEFICS3's processor handles its own preprocessing.
66
+ const rawImage = await decodePngToRawImage(pngBytes);
67
+ // Build chat-style input. The IDEFICS3 conversation shape is
68
+ // probe-verified for `Idefics3Processor.apply_chat_template`.
69
+ const messages = [
70
+ {
71
+ role: 'user',
72
+ content: [{ type: 'image' }, { type: 'text', text: PROMPT }],
73
+ },
74
+ ];
75
+ // IDEFICS3 takes an image array.
76
+ const proc = asVlmProcessor(processor);
77
+ const mdl = asVlmModel(model);
78
+ const chatPrompt = proc.apply_chat_template(messages, { add_generation_prompt: true });
79
+ const inputs = await proc(chatPrompt, [rawImage]);
80
+ const outputs = await mdl.generate({
81
+ ...inputs,
82
+ max_new_tokens: 128,
83
+ repetition_penalty: 1.15,
84
+ no_repeat_ngram_size: 3,
85
+ });
86
+ // `outputs.slice(null, [inputLen, null])` strips the prompt tokens.
87
+ const inputLen = inputs.input_ids.dims[1];
88
+ if (typeof inputLen !== 'number') {
89
+ throw new VlmError('Captioner returned an input tensor without a token dimension', {
90
+ pageNum,
91
+ });
92
+ }
93
+ const newTokens = outputs.slice(null, [inputLen, null]);
94
+ const decoded = proc.batch_decode(newTokens, { skip_special_tokens: true });
95
+ const text = decoded[0] ?? '';
96
+ return postProcess(text);
97
+ }
98
+ catch (err) {
99
+ if (err instanceof VlmError) {
100
+ throw err;
101
+ }
102
+ const cause = err instanceof Error ? err : new Error(String(err));
103
+ throw new VlmError(`Captioning failed for page ${pageNum}`, { cause, pageNum });
104
+ }
105
+ },
106
+ };
107
+ }
108
+ //# sourceMappingURL=fast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fast.js","sourceRoot":"","sources":["../../../src/pdf-visual/captioners/fast.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { Captioner } from '../types.js';
2
+ /**
3
+ * Create a `quality` profile captioner. The dispatcher has already configured
4
+ * `env.cacheDir`; this profile only owns lazy model loading and inference.
5
+ */
6
+ export declare function createQualityCaptioner(resolvedDevice: string): Captioner;
7
+ //# sourceMappingURL=quality.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality.d.ts","sourceRoot":"","sources":["../../../src/pdf-visual/captioners/quality.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA0C5C;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CA2DxE"}
@@ -0,0 +1,101 @@
1
+ // `quality` visual-quality profile — Qwen3.5-2B.
2
+ //
3
+ // Non-obvious constraints:
4
+ // - The `-ONNX-OPT` export of this model needs an ORT contrib op
5
+ // (`com.microsoft:CausalConvWithState`) that onnxruntime-node does not
6
+ // register, so the plain `-ONNX` repo is the only loadable one here.
7
+ // - Generation options were picked by measurement over a figure fixture set:
8
+ // a stronger `repetition_penalty` (1.15, as `fast` uses) makes this family
9
+ // invent label variants, and `no_repeat_ngram_size: 3` costs recall,
10
+ // because a figure legitimately repeats short phrases.
11
+ // - Keywords precede the summary because `postProcess` truncates the tail,
12
+ // and the tail is where an exhausted run degenerates.
13
+ import { AutoProcessor, Qwen3_5ForConditionalGeneration } from '@huggingface/transformers';
14
+ import { VlmError } from '../types.js';
15
+ import { asVlmModel, asVlmProcessor, capLongEdge, createModelLoader, decodePngToRawImage, postProcess, } from './shared.js';
16
+ const MODEL_NAME = 'onnx-community/Qwen3.5-2B-ONNX';
17
+ /**
18
+ * Input long-edge cap (px). The image processor does not downscale on its own,
19
+ * so this is what bounds per-page CPU time. Measured: 768 loses small in-figure
20
+ * text, 1280 costs ~20% more time for no recall gain.
21
+ */
22
+ const INPUT_LONG_EDGE = 1024;
23
+ const PROMPT = `This image is a figure region cropped from a PDF page. Write search text for it.
24
+
25
+ Output two lines, in this order:
26
+
27
+ Line 1 starts with "Keywords:" and then lists the phrases, separated by semicolons.
28
+ Line 2 starts with "Summary:" and then holds one sentence.
29
+
30
+ Keywords rules:
31
+ - Copy the text that is legible in the image: titles, axis names, legend entries, row and column names, panel labels, annotations, diagram node and step names, and numbers shown as labels.
32
+ - Use the exact wording from the image.
33
+ - Keep a number with the label it belongs to, printed as shown, including decimal points and units.
34
+ - Every part of the image that carries legible text contributes at least one phrase.
35
+ - List each phrase once.
36
+ - For partly legible text, keep the part you can read.
37
+ - For a table, keep its title, row names and column names; leave the cell-by-cell values out.
38
+ - Once the legible labels are listed, write the Summary line and stop.
39
+
40
+ Summary rule:
41
+ - One sentence naming the figure type and its subject.
42
+
43
+ Ground every phrase and the summary in what is visible. Where a region carries no legible text, describe it in the summary instead of supplying words for it.`;
44
+ /**
45
+ * Create a `quality` profile captioner. The dispatcher has already configured
46
+ * `env.cacheDir`; this profile only owns lazy model loading and inference.
47
+ */
48
+ export function createQualityCaptioner(resolvedDevice) {
49
+ const loader = createModelLoader(MODEL_NAME, resolvedDevice, async ({ dtypeOpt, modelOpt }) => {
50
+ const processor = await AutoProcessor.from_pretrained(MODEL_NAME, dtypeOpt);
51
+ const model = await Qwen3_5ForConditionalGeneration.from_pretrained(MODEL_NAME, modelOpt);
52
+ return { processor, model };
53
+ });
54
+ return {
55
+ dispose: loader.dispose,
56
+ async caption(pngBytes, pageNum) {
57
+ try {
58
+ const { processor, model } = await loader.ensureLoaded();
59
+ const rawImage = await capLongEdge(await decodePngToRawImage(pngBytes), INPUT_LONG_EDGE);
60
+ const messages = [
61
+ {
62
+ role: 'user',
63
+ content: [{ type: 'image' }, { type: 'text', text: PROMPT }],
64
+ },
65
+ ];
66
+ // Qwen3.5 takes a single image, not an array.
67
+ const proc = asVlmProcessor(processor);
68
+ const mdl = asVlmModel(model);
69
+ const chatPrompt = proc.apply_chat_template(messages, { add_generation_prompt: true });
70
+ const inputs = await proc(chatPrompt, rawImage);
71
+ const outputs = await mdl.generate({
72
+ ...inputs,
73
+ max_new_tokens: 160,
74
+ no_repeat_ngram_size: 6,
75
+ repetition_penalty: 1.05,
76
+ // The model ships `do_sample: true`, which would make captions for
77
+ // the same page differ between ingests.
78
+ do_sample: false,
79
+ });
80
+ const inputLen = inputs.input_ids.dims.at(-1);
81
+ if (typeof inputLen !== 'number') {
82
+ throw new VlmError('Captioner returned an input tensor without a token dimension', {
83
+ pageNum,
84
+ });
85
+ }
86
+ const newTokens = outputs.slice(null, [inputLen, null]);
87
+ const decoded = proc.batch_decode(newTokens, { skip_special_tokens: true });
88
+ const text = decoded[0] ?? '';
89
+ return postProcess(text);
90
+ }
91
+ catch (err) {
92
+ if (err instanceof VlmError) {
93
+ throw err;
94
+ }
95
+ const cause = err instanceof Error ? err : new Error(String(err));
96
+ throw new VlmError(`Captioning failed for page ${pageNum}`, { cause, pageNum });
97
+ }
98
+ },
99
+ };
100
+ }
101
+ //# sourceMappingURL=quality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality.js","sourceRoot":"","sources":["../../../src/pdf-visual/captioners/quality.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { type DeviceType, RawImage } from '@huggingface/transformers';
2
+ /**
3
+ * Build the `from_pretrained` option objects (processor + model) with the
4
+ * pinned dtype and resolved device. `RAG_DEVICE` passes through with no
5
+ * allowlist (see `resolveDevice`) into a closed literal union.
6
+ */
7
+ export declare function buildModelLoadOptions(resolvedDevice: string): {
8
+ dtypeOpt: {
9
+ dtype: 'q4';
10
+ };
11
+ modelOpt: {
12
+ dtype: 'q4';
13
+ device: DeviceType;
14
+ };
15
+ };
16
+ /**
17
+ * `apply_chat_template` is declared to return TOKENIZED output by default, not
18
+ * the string the profiles pass on.
19
+ */
20
+ export type VlmProcessor = {
21
+ apply_chat_template: (messages: unknown, options: {
22
+ add_generation_prompt: boolean;
23
+ }) => string;
24
+ batch_decode: (tokens: unknown, options: {
25
+ skip_special_tokens: boolean;
26
+ }) => string[];
27
+ } & ((prompt: string, images: unknown) => Promise<{
28
+ input_ids: {
29
+ dims: unknown[];
30
+ };
31
+ }>);
32
+ /** Picks the tensor side of `generate`'s declared model-output-or-tensor union. */
33
+ export interface VlmModel {
34
+ generate: (inputs: unknown) => Promise<{
35
+ slice: (axis: null, range: [number, number | null]) => unknown;
36
+ }>;
37
+ }
38
+ /**
39
+ * Not a guard: member existence says nothing about return values. The captioner
40
+ * tests replace the library, so they prove these call sites are right GIVEN
41
+ * this contract, not that the library still honors it — only `fast` runs
42
+ * against the real one, in `visual-ingest-e2e.test.ts`.
43
+ */
44
+ export declare function asVlmProcessor(processor: unknown): VlmProcessor;
45
+ /** Present a loaded model as {@link VlmModel}. See {@link asVlmProcessor}. */
46
+ export declare function asVlmModel(model: unknown): VlmModel;
47
+ /** The processor + model pair produced by a profile's load callback. */
48
+ export interface LoadedModel {
49
+ processor: unknown;
50
+ model: unknown;
51
+ }
52
+ /**
53
+ * Lazy model loader shared by both profiles: owns the pending→ok/failed state
54
+ * machine and the identical load-failure wrapping. A prior failure re-throws
55
+ * the same wrapped error rather than retrying. The per-profile `load` callback
56
+ * owns the model-class choice.
57
+ */
58
+ export declare function createModelLoader(modelName: string, resolvedDevice: string, load: (opts: ReturnType<typeof buildModelLoadOptions>) => Promise<LoadedModel>): {
59
+ ensureLoaded: () => Promise<LoadedModel>;
60
+ dispose: () => Promise<void>;
61
+ };
62
+ /**
63
+ * Decode PNG bytes to a `RawImage`. `BlobPart` omits `Uint8Array<ArrayBufferLike>`
64
+ * (SharedArrayBuffer subtyping), so the bytes are copied into a view that is
65
+ * definitely backed by a plain `ArrayBuffer`. Profiles needing a fixed input
66
+ * size resize the result.
67
+ */
68
+ export declare function decodePngToRawImage(pngBytes: Uint8Array): Promise<RawImage>;
69
+ /**
70
+ * Downscale so the long edge is at most `longEdge`, preserving the aspect
71
+ * ratio, and widen a hairline crop until the processor accepts it. Alignment to
72
+ * the model's patch grid is left to the image processor. Returns the image
73
+ * untouched when it already satisfies both.
74
+ */
75
+ export declare function capLongEdge(image: RawImage, longEdge: number): Promise<RawImage>;
76
+ /**
77
+ * Apply the post-generation processing rules. Returns the final caption or
78
+ * `null` when the result is empty after stripping.
79
+ */
80
+ export declare function postProcess(decoded: string): string | null;
81
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/pdf-visual/captioners/shared.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AAarE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG;IAC7D,QAAQ,EAAE;QAAE,KAAK,EAAE,IAAI,CAAA;KAAE,CAAA;IACzB,QAAQ,EAAE;QAAE,KAAK,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,UAAU,CAAA;KAAE,CAAA;CAC9C,CAQA;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,mBAAmB,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE;QAAE,qBAAqB,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,CAAA;IAC/F,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE;QAAE,mBAAmB,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,EAAE,CAAA;CACvF,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAAE,SAAS,EAAE;QAAE,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAA;CAAE,CAAC,CAAC,CAAA;AAEtF,mFAAmF;AACnF,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;QACrC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,KAAK,OAAO,CAAA;KAC/D,CAAC,CAAA;CACH;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,OAAO,GAAG,YAAY,CAG/D;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAGnD;AAED,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;CACf;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,GAC7E;IAAE,YAAY,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAuC5E;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAIjF;AAKD;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYtF;AA4BD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS1D"}