abb-opencode-local-rag 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.de.md +416 -0
  3. package/README.es.md +416 -0
  4. package/README.fr.md +416 -0
  5. package/README.md +491 -0
  6. package/README.pt-BR.md +416 -0
  7. package/README.zh-CN.md +416 -0
  8. package/dist/bin/install-skills.d.ts +20 -0
  9. package/dist/bin/install-skills.d.ts.map +1 -0
  10. package/dist/bin/install-skills.js +195 -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 +97 -0
  17. package/dist/chunker/semantic-chunker.d.ts.map +1 -0
  18. package/dist/chunker/semantic-chunker.js +294 -0
  19. package/dist/chunker/semantic-chunker.js.map +1 -0
  20. package/dist/chunker/sentence-splitter.d.ts +28 -0
  21. package/dist/chunker/sentence-splitter.d.ts.map +1 -0
  22. package/dist/chunker/sentence-splitter.js +219 -0
  23. package/dist/chunker/sentence-splitter.js.map +1 -0
  24. package/dist/cli/common.d.ts +65 -0
  25. package/dist/cli/common.d.ts.map +1 -0
  26. package/dist/cli/common.js +138 -0
  27. package/dist/cli/common.js.map +1 -0
  28. package/dist/cli/delete.d.ts +8 -0
  29. package/dist/cli/delete.d.ts.map +1 -0
  30. package/dist/cli/delete.js +173 -0
  31. package/dist/cli/delete.js.map +1 -0
  32. package/dist/cli/file-collection.d.ts +2 -0
  33. package/dist/cli/file-collection.d.ts.map +1 -0
  34. package/dist/cli/file-collection.js +53 -0
  35. package/dist/cli/file-collection.js.map +1 -0
  36. package/dist/cli/ingest.d.ts +100 -0
  37. package/dist/cli/ingest.d.ts.map +1 -0
  38. package/dist/cli/ingest.js +363 -0
  39. package/dist/cli/ingest.js.map +1 -0
  40. package/dist/cli/list.d.ts +35 -0
  41. package/dist/cli/list.d.ts.map +1 -0
  42. package/dist/cli/list.js +210 -0
  43. package/dist/cli/list.js.map +1 -0
  44. package/dist/cli/options.d.ts +100 -0
  45. package/dist/cli/options.d.ts.map +1 -0
  46. package/dist/cli/options.js +241 -0
  47. package/dist/cli/options.js.map +1 -0
  48. package/dist/cli/query.d.ts +24 -0
  49. package/dist/cli/query.d.ts.map +1 -0
  50. package/dist/cli/query.js +191 -0
  51. package/dist/cli/query.js.map +1 -0
  52. package/dist/cli/read-neighbors.d.ts +11 -0
  53. package/dist/cli/read-neighbors.d.ts.map +1 -0
  54. package/dist/cli/read-neighbors.js +224 -0
  55. package/dist/cli/read-neighbors.js.map +1 -0
  56. package/dist/cli/status.d.ts +8 -0
  57. package/dist/cli/status.d.ts.map +1 -0
  58. package/dist/cli/status.js +80 -0
  59. package/dist/cli/status.js.map +1 -0
  60. package/dist/cli/sync.d.ts +8 -0
  61. package/dist/cli/sync.d.ts.map +1 -0
  62. package/dist/cli/sync.js +244 -0
  63. package/dist/cli/sync.js.map +1 -0
  64. package/dist/cli-main.d.ts +12 -0
  65. package/dist/cli-main.d.ts.map +1 -0
  66. package/dist/cli-main.js +63 -0
  67. package/dist/cli-main.js.map +1 -0
  68. package/dist/embedder/index.d.ts +85 -0
  69. package/dist/embedder/index.d.ts.map +1 -0
  70. package/dist/embedder/index.js +284 -0
  71. package/dist/embedder/index.js.map +1 -0
  72. package/dist/features/list.d.ts +37 -0
  73. package/dist/features/list.d.ts.map +1 -0
  74. package/dist/features/list.js +40 -0
  75. package/dist/features/list.js.map +1 -0
  76. package/dist/features/sync.d.ts +207 -0
  77. package/dist/features/sync.d.ts.map +1 -0
  78. package/dist/features/sync.js +380 -0
  79. package/dist/features/sync.js.map +1 -0
  80. package/dist/index.d.ts +3 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +53 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/ingest/compute.d.ts +86 -0
  85. package/dist/ingest/compute.d.ts.map +1 -0
  86. package/dist/ingest/compute.js +177 -0
  87. package/dist/ingest/compute.js.map +1 -0
  88. package/dist/ingest/file.d.ts +27 -0
  89. package/dist/ingest/file.d.ts.map +1 -0
  90. package/dist/ingest/file.js +67 -0
  91. package/dist/ingest/file.js.map +1 -0
  92. package/dist/ingest/visual.d.ts +45 -0
  93. package/dist/ingest/visual.d.ts.map +1 -0
  94. package/dist/ingest/visual.js +234 -0
  95. package/dist/ingest/visual.js.map +1 -0
  96. package/dist/parser/docx-parser.d.ts +12 -0
  97. package/dist/parser/docx-parser.d.ts.map +1 -0
  98. package/dist/parser/docx-parser.js +328 -0
  99. package/dist/parser/docx-parser.js.map +1 -0
  100. package/dist/parser/html-parser.d.ts +18 -0
  101. package/dist/parser/html-parser.d.ts.map +1 -0
  102. package/dist/parser/html-parser.js +102 -0
  103. package/dist/parser/html-parser.js.map +1 -0
  104. package/dist/parser/index.d.ts +214 -0
  105. package/dist/parser/index.d.ts.map +1 -0
  106. package/dist/parser/index.js +454 -0
  107. package/dist/parser/index.js.map +1 -0
  108. package/dist/parser/pdf-extract.d.ts +81 -0
  109. package/dist/parser/pdf-extract.d.ts.map +1 -0
  110. package/dist/parser/pdf-extract.js +112 -0
  111. package/dist/parser/pdf-extract.js.map +1 -0
  112. package/dist/parser/pdf-filter.d.ts +117 -0
  113. package/dist/parser/pdf-filter.d.ts.map +1 -0
  114. package/dist/parser/pdf-filter.js +528 -0
  115. package/dist/parser/pdf-filter.js.map +1 -0
  116. package/dist/parser/title-extractor.d.ts +69 -0
  117. package/dist/parser/title-extractor.d.ts.map +1 -0
  118. package/dist/parser/title-extractor.js +145 -0
  119. package/dist/parser/title-extractor.js.map +1 -0
  120. package/dist/pdf-visual/captioner.d.ts +16 -0
  121. package/dist/pdf-visual/captioner.d.ts.map +1 -0
  122. package/dist/pdf-visual/captioner.js +63 -0
  123. package/dist/pdf-visual/captioner.js.map +1 -0
  124. package/dist/pdf-visual/captioners/fast.d.ts +7 -0
  125. package/dist/pdf-visual/captioners/fast.d.ts.map +1 -0
  126. package/dist/pdf-visual/captioners/fast.js +103 -0
  127. package/dist/pdf-visual/captioners/fast.js.map +1 -0
  128. package/dist/pdf-visual/captioners/quality.d.ts +7 -0
  129. package/dist/pdf-visual/captioners/quality.d.ts.map +1 -0
  130. package/dist/pdf-visual/captioners/quality.js +127 -0
  131. package/dist/pdf-visual/captioners/quality.js.map +1 -0
  132. package/dist/pdf-visual/captioners/shared.d.ts +44 -0
  133. package/dist/pdf-visual/captioners/shared.d.ts.map +1 -0
  134. package/dist/pdf-visual/captioners/shared.js +104 -0
  135. package/dist/pdf-visual/captioners/shared.js.map +1 -0
  136. package/dist/pdf-visual/detector.d.ts +9 -0
  137. package/dist/pdf-visual/detector.d.ts.map +1 -0
  138. package/dist/pdf-visual/detector.js +234 -0
  139. package/dist/pdf-visual/detector.js.map +1 -0
  140. package/dist/pdf-visual/index.d.ts +13 -0
  141. package/dist/pdf-visual/index.d.ts.map +1 -0
  142. package/dist/pdf-visual/index.js +45 -0
  143. package/dist/pdf-visual/index.js.map +1 -0
  144. package/dist/pdf-visual/renderer.d.ts +9 -0
  145. package/dist/pdf-visual/renderer.d.ts.map +1 -0
  146. package/dist/pdf-visual/renderer.js +177 -0
  147. package/dist/pdf-visual/renderer.js.map +1 -0
  148. package/dist/pdf-visual/types.d.ts +62 -0
  149. package/dist/pdf-visual/types.d.ts.map +1 -0
  150. package/dist/pdf-visual/types.js +32 -0
  151. package/dist/pdf-visual/types.js.map +1 -0
  152. package/dist/server/error-utils.d.ts +79 -0
  153. package/dist/server/error-utils.d.ts.map +1 -0
  154. package/dist/server/error-utils.js +148 -0
  155. package/dist/server/error-utils.js.map +1 -0
  156. package/dist/server/index.d.ts +258 -0
  157. package/dist/server/index.d.ts.map +1 -0
  158. package/dist/server/index.js +1104 -0
  159. package/dist/server/index.js.map +1 -0
  160. package/dist/server/list-scanner.d.ts +52 -0
  161. package/dist/server/list-scanner.d.ts.map +1 -0
  162. package/dist/server/list-scanner.js +72 -0
  163. package/dist/server/list-scanner.js.map +1 -0
  164. package/dist/server/tool-definitions.d.ts +8 -0
  165. package/dist/server/tool-definitions.d.ts.map +1 -0
  166. package/dist/server/tool-definitions.js +181 -0
  167. package/dist/server/tool-definitions.js.map +1 -0
  168. package/dist/server/tool-input.d.ts +37 -0
  169. package/dist/server/tool-input.d.ts.map +1 -0
  170. package/dist/server/tool-input.js +216 -0
  171. package/dist/server/tool-input.js.map +1 -0
  172. package/dist/server/types.d.ts +331 -0
  173. package/dist/server/types.d.ts.map +1 -0
  174. package/dist/server/types.js +3 -0
  175. package/dist/server/types.js.map +1 -0
  176. package/dist/server-main.d.ts +46 -0
  177. package/dist/server-main.d.ts.map +1 -0
  178. package/dist/server-main.js +242 -0
  179. package/dist/server-main.js.map +1 -0
  180. package/dist/utils/base-dirs.d.ts +212 -0
  181. package/dist/utils/base-dirs.d.ts.map +1 -0
  182. package/dist/utils/base-dirs.js +422 -0
  183. package/dist/utils/base-dirs.js.map +1 -0
  184. package/dist/utils/errors.d.ts +24 -0
  185. package/dist/utils/errors.d.ts.map +1 -0
  186. package/dist/utils/errors.js +53 -0
  187. package/dist/utils/errors.js.map +1 -0
  188. package/dist/utils/limits.d.ts +26 -0
  189. package/dist/utils/limits.d.ts.map +1 -0
  190. package/dist/utils/limits.js +28 -0
  191. package/dist/utils/limits.js.map +1 -0
  192. package/dist/utils/list-sources.d.ts +47 -0
  193. package/dist/utils/list-sources.d.ts.map +1 -0
  194. package/dist/utils/list-sources.js +50 -0
  195. package/dist/utils/list-sources.js.map +1 -0
  196. package/dist/utils/raw-data-utils.d.ts +131 -0
  197. package/dist/utils/raw-data-utils.d.ts.map +1 -0
  198. package/dist/utils/raw-data-utils.js +255 -0
  199. package/dist/utils/raw-data-utils.js.map +1 -0
  200. package/dist/utils/scan.d.ts +126 -0
  201. package/dist/utils/scan.d.ts.map +1 -0
  202. package/dist/utils/scan.js +221 -0
  203. package/dist/utils/scan.js.map +1 -0
  204. package/dist/utils/scope-match.d.ts +43 -0
  205. package/dist/utils/scope-match.d.ts.map +1 -0
  206. package/dist/utils/scope-match.js +87 -0
  207. package/dist/utils/scope-match.js.map +1 -0
  208. package/dist/utils/sensitive-path.d.ts +23 -0
  209. package/dist/utils/sensitive-path.d.ts.map +1 -0
  210. package/dist/utils/sensitive-path.js +91 -0
  211. package/dist/utils/sensitive-path.js.map +1 -0
  212. package/dist/utils/sync-path-key.d.ts +20 -0
  213. package/dist/utils/sync-path-key.d.ts.map +1 -0
  214. package/dist/utils/sync-path-key.js +33 -0
  215. package/dist/utils/sync-path-key.js.map +1 -0
  216. package/dist/vectordb/index.d.ts +168 -0
  217. package/dist/vectordb/index.d.ts.map +1 -0
  218. package/dist/vectordb/index.js +619 -0
  219. package/dist/vectordb/index.js.map +1 -0
  220. package/dist/vectordb/search-filters.d.ts +39 -0
  221. package/dist/vectordb/search-filters.d.ts.map +1 -0
  222. package/dist/vectordb/search-filters.js +136 -0
  223. package/dist/vectordb/search-filters.js.map +1 -0
  224. package/dist/vectordb/types.d.ts +196 -0
  225. package/dist/vectordb/types.d.ts.map +1 -0
  226. package/dist/vectordb/types.js +224 -0
  227. package/dist/vectordb/types.js.map +1 -0
  228. package/package.json +105 -0
  229. package/skills/mcp-local-rag/SKILL.md +308 -0
  230. package/skills/mcp-local-rag/references/cli-reference.md +175 -0
  231. package/skills/mcp-local-rag/references/html-ingestion.md +78 -0
  232. package/skills/mcp-local-rag/references/query-optimization.md +57 -0
  233. package/skills/mcp-local-rag/references/result-refinement.md +56 -0
@@ -0,0 +1,145 @@
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
+ /**
12
+ * Convert a file name to a human-readable title
13
+ * Strips the extension and replaces hyphens/underscores with spaces
14
+ *
15
+ * @param fileName - File name (e.g., "2024-annual-report.pdf")
16
+ * @returns Human-readable title (e.g., "2024 annual report")
17
+ */
18
+ export function fileNameToTitle(fileName) {
19
+ // Strip extension (last dot and everything after)
20
+ const lastDotIndex = fileName.lastIndexOf('.');
21
+ const nameWithoutExt = lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
22
+ // Replace hyphens and underscores with spaces
23
+ return nameWithoutExt.replace(/[-_]/g, ' ');
24
+ }
25
+ // ============================================
26
+ // Per-Format Extractors
27
+ // ============================================
28
+ /**
29
+ * Extract title from Markdown content
30
+ * Priority: YAML frontmatter title -> first # H1 -> file name
31
+ *
32
+ * @param text - Markdown content
33
+ * @param fileName - File name for fallback
34
+ * @returns Title extraction result
35
+ */
36
+ export function extractMarkdownTitle(text, fileName) {
37
+ // 1. Try YAML frontmatter
38
+ const frontmatterMatch = text.match(/^---\n[\s\S]*?title:\s*['"]?(.+?)['"]?\s*\n[\s\S]*?---/);
39
+ if (frontmatterMatch?.[1]) {
40
+ return { title: frontmatterMatch[1].trim(), source: 'metadata' };
41
+ }
42
+ // 2. Try first H1 heading
43
+ const h1Match = text.match(/^# (.+)$/m);
44
+ if (h1Match?.[1]) {
45
+ return { title: h1Match[1].trim(), source: 'content' };
46
+ }
47
+ // 3. Fall back to file name
48
+ return { title: fileNameToTitle(fileName), source: 'filename' };
49
+ }
50
+ /**
51
+ * Extract title from plain text content
52
+ * Priority: first line followed by empty line -> file name
53
+ *
54
+ * @param text - Plain text content
55
+ * @param fileName - File name for fallback
56
+ * @returns Title extraction result
57
+ */
58
+ export function extractTxtTitle(text, fileName) {
59
+ // Try first line followed by empty line
60
+ if (text.length > 0) {
61
+ const lines = text.split('\n');
62
+ const firstLine = lines[0];
63
+ const secondLine = lines[1];
64
+ if (firstLine !== undefined &&
65
+ secondLine !== undefined &&
66
+ firstLine.trim().length > 0 &&
67
+ secondLine.trim().length === 0) {
68
+ return { title: firstLine.trim(), source: 'content' };
69
+ }
70
+ }
71
+ // Fall back to file name
72
+ return { title: fileNameToTitle(fileName), source: 'filename' };
73
+ }
74
+ /**
75
+ * Extract title from HTML content (using Readability title)
76
+ * Priority: readability title -> file name
77
+ *
78
+ * @param readabilityTitle - Title extracted by Readability
79
+ * @param fileName - File name for fallback
80
+ * @returns Title extraction result
81
+ */
82
+ export function extractHtmlTitle(readabilityTitle, fileName) {
83
+ if (readabilityTitle && readabilityTitle.trim().length > 0) {
84
+ return { title: readabilityTitle.trim(), source: 'content' };
85
+ }
86
+ // Fall back to file name
87
+ return { title: fileNameToTitle(fileName), source: 'filename' };
88
+ }
89
+ /**
90
+ * Extract title from PDF metadata or first page chunk text
91
+ * Priority: PDF metadata /Title -> first page chunk 0 text -> file name
92
+ *
93
+ * Rejects metadata titles that look like file paths (contain / or \) or are empty/whitespace-only.
94
+ *
95
+ * @param metadataTitle - PDF metadata /Title value (may be undefined)
96
+ * @param firstPageChunkText - Text of chunk 0 from semantic chunking of page 1 (may be undefined)
97
+ * @param fileName - File name for fallback
98
+ * @param firstPageFontHint - Largest-font text item from page 1 (optional, used for title detection)
99
+ * @returns Title extraction result
100
+ */
101
+ export function extractPdfTitle(metadataTitle, firstPageChunkText, fileName, firstPageFontHint) {
102
+ // 1. Try PDF metadata title (reject file paths and empty values)
103
+ if (metadataTitle && metadataTitle.trim().length > 0) {
104
+ const trimmed = metadataTitle.trim();
105
+ const looksLikeFilePath = trimmed.includes('/') || trimmed.includes('\\');
106
+ if (!looksLikeFilePath) {
107
+ return { title: trimmed, source: 'metadata' };
108
+ }
109
+ }
110
+ // 2. Try largest-font text from page 1 (font size > threshold indicates title)
111
+ if (firstPageFontHint &&
112
+ firstPageFontHint.fontSize > TITLE_MIN_FONT_SIZE &&
113
+ firstPageFontHint.text.trim().length > 0) {
114
+ return { title: firstPageFontHint.text.trim(), source: 'content' };
115
+ }
116
+ // 3. Try first chunk from page 1 semantic chunking
117
+ if (firstPageChunkText && firstPageChunkText.trim().length > 0) {
118
+ return { title: firstPageChunkText.trim(), source: 'content' };
119
+ }
120
+ // 4. Fall back to file name
121
+ return { title: fileNameToTitle(fileName), source: 'filename' };
122
+ }
123
+ /**
124
+ * Extract title from a parsed DOCX mammoth HTML document
125
+ * Priority: DOCX core title -> first non-empty <h1> from mammoth HTML -> file name
126
+ *
127
+ * @param document - Parsed HTML document generated from mammoth.convertToHtml()
128
+ * @param fileName - File name for fallback
129
+ * @param metadataTitle - Optional title extracted from docProps/core.xml
130
+ * @returns Title extraction result
131
+ */
132
+ export function extractDocxTitle(document, fileName, metadataTitle) {
133
+ const normalizedMetadataTitle = metadataTitle?.replace(/\s+/g, ' ').trim();
134
+ if (normalizedMetadataTitle) {
135
+ return { title: normalizedMetadataTitle, source: 'metadata' };
136
+ }
137
+ for (const heading of document.querySelectorAll('h1')) {
138
+ const title = (heading.textContent ?? '').replace(/\s+/g, ' ').trim();
139
+ if (title)
140
+ return { title, source: 'content' };
141
+ }
142
+ // Fall back to file name
143
+ return { title: fileNameToTitle(fileName), source: 'filename' };
144
+ }
145
+ //# sourceMappingURL=title-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"title-extractor.js","sourceRoot":"","sources":["../../src/parser/title-extractor.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,+DAA+D;AAE/D,+CAA+C;AAC/C,YAAY;AACZ,+CAA+C;AAE/C,oFAAoF;AACpF,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAc9B,+CAA+C;AAC/C,gBAAgB;AAChB,+CAA+C;AAE/C;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,kDAAkD;IAClD,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IAC9C,MAAM,cAAc,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IACxF,8CAA8C;IAC9C,OAAO,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAC7C,CAAC;AAED,+CAA+C;AAC/C,wBAAwB;AACxB,+CAA+C;AAE/C;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,QAAgB;IACjE,0BAA0B;IAC1B,MAAM,gBAAgB,GAAG,IAAI,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC7F,IAAI,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;IAClE,CAAC;IAED,0BAA0B;IAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IACvC,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IACxD,CAAC;IAED,4BAA4B;IAC5B,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AACjE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAgB;IAC5D,wCAAwC;IACxC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC9B,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QAC1B,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QAC3B,IACE,SAAS,KAAK,SAAS;YACvB,UAAU,KAAK,SAAS;YACxB,SAAS,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAC3B,UAAU,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAC9B,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;QACvD,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AACjE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,gBAAwB,EACxB,QAAgB;IAEhB,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3D,OAAO,EAAE,KAAK,EAAE,gBAAgB,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAC9D,CAAC;IAED,yBAAyB;IACzB,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AACjE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,aAAiC,EACjC,kBAAsC,EACtC,QAAgB,EAChB,iBAAsD;IAEtD,iEAAiE;IACjE,IAAI,aAAa,IAAI,aAAa,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrD,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,EAAE,CAAA;QACpC,MAAM,iBAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QACzE,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACvB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;QAC/C,CAAC;IACH,CAAC;IAED,+EAA+E;IAC/E,IACE,iBAAiB;QACjB,iBAAiB,CAAC,QAAQ,GAAG,mBAAmB;QAChD,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EACxC,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IACpE,CAAC;IAED,mDAAmD;IACnD,IAAI,kBAAkB,IAAI,kBAAkB,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/D,OAAO,EAAE,KAAK,EAAE,kBAAkB,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAChE,CAAC;IAED,4BAA4B;IAC5B,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AACjE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAkB,EAClB,QAAgB,EAChB,aAAsB;IAEtB,MAAM,uBAAuB,GAAG,aAAa,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;IAC1E,IAAI,uBAAuB,EAAE,CAAC;QAC5B,OAAO,EAAE,KAAK,EAAE,uBAAuB,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;IAC/D,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;QACrE,IAAI,KAAK;YAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAChD,CAAC;IAED,yBAAyB;IACzB,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AACjE,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { Captioner, CaptionerConfig } from './types.js';
2
+ /**
3
+ * Create a captioner for the requested visual-quality profile. Sets
4
+ * `env.cacheDir` immediately so the global is correct even if the captioner
5
+ * is constructed before any embedder initializes.
6
+ *
7
+ * Concurrency assumption: `env.cacheDir` is a process-global from
8
+ * `@huggingface/transformers`. Setting it here at construction time is safe
9
+ * for the current single-instance usage (one captioner per ingest run). If
10
+ * the codebase ever constructs multiple captioners with DIFFERENT `cacheDir`
11
+ * values in parallel, the last writer wins and the first captioner's
12
+ * `from_pretrained` may resolve against the wrong cache. Avoid concurrent
13
+ * construction with differing cacheDirs.
14
+ */
15
+ export declare function createCaptioner(config: CaptionerConfig): Captioner;
16
+ //# sourceMappingURL=captioner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captioner.d.ts","sourceRoot":"","sources":["../../src/pdf-visual/captioner.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CA6BlE"}
@@ -0,0 +1,63 @@
1
+ // Captioner dispatcher for the visual ingest path.
2
+ //
3
+ // `createCaptioner(config)` selects the underlying VLM family based on the
4
+ // `QualityProfile` and returns a `Captioner`. Each profile is implemented as a
5
+ // self-contained module under `./captioners/` so that prompt, chat template,
6
+ // processor signature, generation options, and model class stay coherent per
7
+ // profile.
8
+ //
9
+ // Profiles:
10
+ // - `fast` → `captioners/fast.ts` (SmolVLM-256M-Instruct, IDEFICS3).
11
+ // Lightweight default; ~250 MB cache.
12
+ // - `quality` → `captioners/quality.ts` (Qwen2.5-VL-3B-Instruct-ONNX).
13
+ // Higher fidelity on figures with in-image text; ~2.9 GB cache,
14
+ // ~2× per-page inference relative to `fast`.
15
+ //
16
+ // `env.cacheDir` is set once here (not inside the per-profile modules) so the
17
+ // shared global is configured before either profile's `from_pretrained` runs
18
+ // and the per-profile modules stay free of the global side effect.
19
+ import { env } from '@huggingface/transformers';
20
+ import { createFastCaptioner } from './captioners/fast.js';
21
+ import { createQualityCaptioner } from './captioners/quality.js';
22
+ /**
23
+ * Create a captioner for the requested visual-quality profile. Sets
24
+ * `env.cacheDir` immediately so the global is correct even if the captioner
25
+ * is constructed before any embedder initializes.
26
+ *
27
+ * Concurrency assumption: `env.cacheDir` is a process-global from
28
+ * `@huggingface/transformers`. Setting it here at construction time is safe
29
+ * for the current single-instance usage (one captioner per ingest run). If
30
+ * the codebase ever constructs multiple captioners with DIFFERENT `cacheDir`
31
+ * values in parallel, the last writer wins and the first captioner's
32
+ * `from_pretrained` may resolve against the wrong cache. Avoid concurrent
33
+ * construction with differing cacheDirs.
34
+ */
35
+ export function createCaptioner(config) {
36
+ // Defensive ordering: set the global cacheDir at construction so the very
37
+ // first `from_pretrained` call sees the right value. Setting the same
38
+ // global twice with the same value is idempotent (shared with the
39
+ // embedder).
40
+ env.cacheDir = config.cacheDir;
41
+ const resolvedDevice = config.device || 'cpu';
42
+ switch (config.profile) {
43
+ case 'fast':
44
+ return createFastCaptioner(resolvedDevice);
45
+ case 'quality':
46
+ // No silent fallback to `fast` on load failure. The heavier Qwen2.5-VL
47
+ // model surfaces its load error as a wrapped `VlmError` per page (see
48
+ // `processVisualRegions` in `./index.ts`); per FR-3 the file ingest
49
+ // as a whole still completes text-only, so a misconfigured install
50
+ // degrades each candidate page rather than masking the misconfig by
51
+ // switching to `fast`. Operators see one warn line per candidate page.
52
+ return createQualityCaptioner(resolvedDevice);
53
+ default: {
54
+ // Exhaustiveness guard. `QualityProfile` is statically narrow at the
55
+ // call sites (CLI + MCP both validate before reaching here) so this
56
+ // branch is unreachable today; the throw is defensive for future
57
+ // ProfileType additions that forget to extend this switch.
58
+ const _exhaustive = config.profile;
59
+ throw new Error(`Unknown QualityProfile: ${_exhaustive}`);
60
+ }
61
+ }
62
+ }
63
+ //# sourceMappingURL=captioner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captioner.js","sourceRoot":"","sources":["../../src/pdf-visual/captioner.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,EAAE;AACF,2EAA2E;AAC3E,+EAA+E;AAC/E,6EAA6E;AAC7E,6EAA6E;AAC7E,WAAW;AACX,EAAE;AACF,YAAY;AACZ,6EAA6E;AAC7E,sDAAsD;AACtD,yEAAyE;AACzE,gFAAgF;AAChF,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,mEAAmE;AAEnE,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;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,MAAuB;IACrD,0EAA0E;IAC1E,sEAAsE;IACtE,kEAAkE;IAClE,aAAa;IACb,GAAG,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IAE9B,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,uEAAuE;YACvE,sEAAsE;YACtE,oEAAoE;YACpE,mEAAmE;YACnE,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,WAAqB,EAAE,CAAC,CAAA;QACrE,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;AAkB5C;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAkErE"}
@@ -0,0 +1,103 @@
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 { 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
+ async caption(pngBytes, pageNum) {
61
+ try {
62
+ const { processor, model } = await loader.ensureLoaded();
63
+ // Decode PNG → RawImage. SmolVLM-256M is NOT resized client-side —
64
+ // IDEFICS3's processor handles its own preprocessing.
65
+ const rawImage = await decodePngToRawImage(pngBytes);
66
+ // Build chat-style input. The IDEFICS3 conversation shape is
67
+ // probe-verified for `Idefics3Processor.apply_chat_template`.
68
+ const messages = [
69
+ {
70
+ role: 'user',
71
+ content: [{ type: 'image' }, { type: 'text', text: PROMPT }],
72
+ },
73
+ ];
74
+ // The processor and model are dynamic in type at the boundary;
75
+ // narrow to a minimal callable / generate-able shape here. IDEFICS3
76
+ // processor takes an array of images.
77
+ const proc = processor;
78
+ const mdl = model;
79
+ const chatPrompt = proc.apply_chat_template(messages, { add_generation_prompt: true });
80
+ const inputs = await proc(chatPrompt, [rawImage]);
81
+ const outputs = await mdl.generate({
82
+ ...inputs,
83
+ max_new_tokens: 128,
84
+ repetition_penalty: 1.15,
85
+ no_repeat_ngram_size: 3,
86
+ });
87
+ // `outputs.slice(null, [inputLen, null])` strips the prompt tokens.
88
+ const inputLen = inputs.input_ids.dims[1];
89
+ const newTokens = outputs.slice(null, [inputLen, null]);
90
+ const decoded = proc.batch_decode(newTokens, { skip_special_tokens: true });
91
+ const text = decoded[0] ?? '';
92
+ return postProcess(text);
93
+ }
94
+ catch (err) {
95
+ if (err instanceof VlmError)
96
+ throw err;
97
+ const cause = err instanceof Error ? err : new Error(String(err));
98
+ throw new VlmError(`Captioning failed for page ${pageNum}`, { cause, pageNum });
99
+ }
100
+ },
101
+ };
102
+ }
103
+ //# 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":"AAsCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAkC5C;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAwExE"}
@@ -0,0 +1,127 @@
1
+ // `quality` visual-quality profile — Qwen2.5-VL-3B-Instruct-ONNX.
2
+ //
3
+ // Verbatim port of the working-tree captioner validated during the visual-
4
+ // quality design discussion. Higher fidelity than `fast` on figures with
5
+ // in-image text (axis labels, panel sub-labels, annotations), at the cost of
6
+ // a materially larger model cache (~10× `fast`) and ~2× per-page inference
7
+ // time on CPU. Shares the profile-agnostic load/decode mechanics with `fast`
8
+ // via `shared.ts`; keeps its own model class, prompt, resize, processor call
9
+ // shape, and generation options.
10
+ //
11
+ // Implementation contract:
12
+ // 1. Lazy-load processor + model on first `caption()` call with the pinned
13
+ // DTYPE and the resolved device. `env.cacheDir` is set by the dispatcher
14
+ // (`captioner.ts`) before this profile is constructed.
15
+ // 2. Decode PNG bytes via `RawImage.fromBlob(...)` and resize to 448x448 —
16
+ // matches the onnx-community Qwen2-VL reference example. Qwen2.5-VL
17
+ // supports dynamic resolution natively, but the reference example uses
18
+ // a fixed resize for stable behavior.
19
+ // 3. Build chat-style input via `processor.apply_chat_template(messages,
20
+ // { add_generation_prompt: true })` with the Qwen2.5-VL conversation
21
+ // shape `[{role:'user', content:[{type:'image'},{type:'text',text:...}]}]`.
22
+ // The shape is hard-coded against the Qwen2.5-VL family; swapping in a
23
+ // non-Qwen-VL model requires a new profile.
24
+ // 4. Call `model.generate({ ...inputs, max_new_tokens: 128 })`. The
25
+ // `repetition_penalty` / `no_repeat_ngram_size` options used by `fast`
26
+ // are intentionally absent — on Qwen2.5-VL they cause forced variant
27
+ // generation (e.g. "Cycles per cycle" → "Cpu cycles/cycle" → "Cnt
28
+ // cyles/clk") whenever a figure naturally repeats a phrase.
29
+ // 5. Decode via `processor.batch_decode(newTokens, { skip_special_tokens: true })`
30
+ // where `newTokens = outputs.slice(null, [inputs.input_ids.dims.at(-1), null])`.
31
+ // `dims.at(-1)` reads the last dimension defensively — matches the
32
+ // onnx-community reference example.
33
+ // 6. Post-processing via `shared.postProcess` (same pipeline as `fast`).
34
+ // 7. On model load / image decode / generation failure throw `VlmError`
35
+ // with `pageNum` + `cause`. No silent fallback to `fast`.
36
+ import { AutoProcessor, Qwen2_5_VLForConditionalGeneration } from '@huggingface/transformers';
37
+ import { VlmError } from '../types.js';
38
+ import { createModelLoader, decodePngToRawImage, postProcess } from './shared.js';
39
+ const MODEL_NAME = 'onnx-community/Qwen2.5-VL-3B-Instruct-ONNX';
40
+ /**
41
+ * Fixed input resolution (px) for the Qwen2.5-VL reference resize. Matches the
42
+ * onnx-community Qwen2-VL example's stable-behavior fixed resize.
43
+ */
44
+ const QWEN_INPUT_SIZE = 448;
45
+ /**
46
+ * Static prompt — tuned for retrieval search indexing. Asks the VLM to scan
47
+ * the whole image before composing output so coverage spans every region,
48
+ * then produces a two-part response (Summary + Keywords) suitable for
49
+ * embedding + downstream semantic / lexical search. No length specifiers —
50
+ * output length is controlled by `max_new_tokens` because length specs
51
+ * narrow coverage.
52
+ */
53
+ const PROMPT = `Describe this PDF page image for retrieval search indexing.
54
+
55
+ Procedure:
56
+ 1. Scan the whole image and identify every distinct region.
57
+ 2. Compose the output from across all regions identified.
58
+
59
+ Output exactly two parts:
60
+
61
+ Summary: Describe the page's content, including its type and subject when identifiable.
62
+
63
+ Keywords: Phrases separated by semicolons. Capture readable text and visible labels from across the page — including section titles, sub-labels inside figures, tables, panels, or annotations. Use exact wording from the image when readable. Cover the visible regions of the page. List each phrase once.
64
+
65
+ Use only details visible in the image. If a region is unreadable, skip it.`;
66
+ /**
67
+ * Create a `quality` profile captioner. The dispatcher has already configured
68
+ * `env.cacheDir`; this profile only owns lazy model loading and inference.
69
+ */
70
+ export function createQualityCaptioner(resolvedDevice) {
71
+ // The explicit `Qwen2_5_VLForConditionalGeneration` class matches the
72
+ // onnx-community reference example (rather than the architecture-agnostic
73
+ // AutoModelForImageTextToText entry point used by `fast`).
74
+ const loader = createModelLoader(MODEL_NAME, resolvedDevice, async ({ dtypeOpt, modelOpt }) => {
75
+ const processor = await AutoProcessor.from_pretrained(MODEL_NAME, dtypeOpt);
76
+ const model = await Qwen2_5_VLForConditionalGeneration.from_pretrained(MODEL_NAME, modelOpt);
77
+ return { processor, model };
78
+ });
79
+ return {
80
+ async caption(pngBytes, pageNum) {
81
+ try {
82
+ const { processor, model } = await loader.ensureLoaded();
83
+ // Decode PNG → RawImage, then resize to 448x448 to match the
84
+ // onnx-community Qwen2-VL reference example. Qwen2.5-VL supports dynamic
85
+ // resolution natively, but the reference example uses a fixed resize for
86
+ // stable behavior; revisit if small in-figure text is lost.
87
+ const rawImage = await (await decodePngToRawImage(pngBytes)).resize(QWEN_INPUT_SIZE, QWEN_INPUT_SIZE);
88
+ // Build chat-style input. The Qwen2.5-VL conversation shape mirrors
89
+ // the onnx-community Qwen2-VL reference: a single user turn with an
90
+ // image placeholder followed by the text prompt.
91
+ const messages = [
92
+ {
93
+ role: 'user',
94
+ content: [{ type: 'image' }, { type: 'text', text: PROMPT }],
95
+ },
96
+ ];
97
+ // The processor and model are dynamic in type at the boundary;
98
+ // narrow to a minimal callable / generate-able shape here. Qwen2.5-VL
99
+ // processor takes a single image (not an array), per the
100
+ // onnx-community reference example signature `processor(text, image)`.
101
+ const proc = processor;
102
+ const mdl = model;
103
+ const chatPrompt = proc.apply_chat_template(messages, { add_generation_prompt: true });
104
+ const inputs = await proc(chatPrompt, rawImage);
105
+ const outputs = await mdl.generate({
106
+ ...inputs,
107
+ max_new_tokens: 128,
108
+ });
109
+ // `outputs.slice(null, [inputLen, null])` strips the prompt tokens.
110
+ // `dims.at(-1)` reads the last dimension defensively — matches the
111
+ // onnx-community reference example.
112
+ const inputLen = inputs.input_ids.dims.at(-1);
113
+ const newTokens = outputs.slice(null, [inputLen, null]);
114
+ const decoded = proc.batch_decode(newTokens, { skip_special_tokens: true });
115
+ const text = decoded[0] ?? '';
116
+ return postProcess(text);
117
+ }
118
+ catch (err) {
119
+ if (err instanceof VlmError)
120
+ throw err;
121
+ const cause = err instanceof Error ? err : new Error(String(err));
122
+ throw new VlmError(`Captioning failed for page ${pageNum}`, { cause, pageNum });
123
+ }
124
+ },
125
+ };
126
+ }
127
+ //# 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,44 @@
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. transformers.js declares `dtype` as a
5
+ * literal union; cast through `unknown` to widen-to-string-then-back.
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
+ /** The processor + model pair produced by a profile's load callback. */
17
+ export interface LoadedModel {
18
+ processor: unknown;
19
+ model: unknown;
20
+ }
21
+ /**
22
+ * Lazy model loader shared by both profiles. Encapsulates the
23
+ * pending→ok/failed state machine and the identical load-failure wrapping
24
+ * (`Captioner load failed (modelName=..., device=...)`). The per-profile
25
+ * `load` callback owns the model-class choice and receives the shared option
26
+ * objects. On first `ensureLoaded()` the model loads; subsequent calls return
27
+ * the cached pair; a prior failure re-throws the same wrapped error.
28
+ */
29
+ export declare function createModelLoader(modelName: string, resolvedDevice: string, load: (opts: ReturnType<typeof buildModelLoadOptions>) => Promise<LoadedModel>): {
30
+ ensureLoaded: () => Promise<LoadedModel>;
31
+ };
32
+ /**
33
+ * Decode PNG bytes to a `RawImage`. `Blob` accepts `Uint8Array` directly (the
34
+ * renderer returns `Uint8Array` from `Pixmap.asPNG()`), but the `BlobPart`
35
+ * type omits `Uint8Array<ArrayBufferLike>` due to SharedArrayBuffer subtyping;
36
+ * cast through `unknown`. Profiles needing a fixed input size resize the result.
37
+ */
38
+ export declare function decodePngToRawImage(pngBytes: Uint8Array): Promise<RawImage>;
39
+ /**
40
+ * Apply the post-generation processing rules. Returns the final caption or
41
+ * `null` when the result is empty after stripping.
42
+ */
43
+ export declare function postProcess(decoded: string): string | null;
44
+ //# 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":"AAaA,OAAO,EAAE,KAAK,UAAU,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AAWrE;;;;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,CAOA;AAED,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;CACf;AAED;;;;;;;GAOG;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,CAAA;CAAE,CAuB9C;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAGjF;AAwBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK1D"}