docling.rs 0.32.0 → 0.37.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.
package/README.md CHANGED
@@ -93,6 +93,62 @@ for await (const chunk of streamFileMarkdown('paper.pdf')) {
93
93
  }
94
94
  ```
95
95
 
96
+ ### Chunking (docling's chunkers, for RAG)
97
+
98
+ `chunkFile` / `chunk` / `chunkDocument` (each with an `…Async` variant) run
99
+ docling's chunkers over a converted document and return embedding-ready
100
+ records. The default is the structure-driven **hierarchical** chunker (one
101
+ chunk per document item — whole lists, triplet-serialized tables — with its
102
+ heading path); pass `chunker: 'hybrid'` to refine against a token budget
103
+ (split oversized chunks, merge undersized same-heading neighbours), matching
104
+ docling's `HybridChunker`. The hybrid token counts come from a HuggingFace
105
+ `tokenizer.json`: pass a path via `tokenizer`, or omit it to use
106
+ `models/chunk/tokenizer.json` (all-MiniLM-L6-v2's — fetched by
107
+ `scripts/download_dependencies.sh` alongside the ML models, resolved through
108
+ the same install-home logic).
109
+
110
+ ```js
111
+ import { chunkFileAsync, Pipeline, chunkDocumentAsync } from 'docling.rs'
112
+
113
+ const chunks = await chunkFileAsync('report.docx', {
114
+ chunker: 'hybrid',
115
+ tokenizer: 'tokenizer.json', // e.g. all-MiniLM-L6-v2's
116
+ maxTokens: 256,
117
+ })
118
+ for (const c of chunks) {
119
+ await embed(c.contextualized) // heading path + text, ready for the embedder
120
+ }
121
+
122
+ // Chunk something you already converted (no re-conversion), e.g. a PDF
123
+ // that went through the warm Pipeline:
124
+ const { content } = new Pipeline().convertFile('paper.pdf', { to: 'json' })
125
+ const pdfChunks = await chunkDocumentAsync(content)
126
+ ```
127
+
128
+ Each `Chunk` is `{ text, headings?, docItems, contextualized }` — `docItems`
129
+ holds the source items' JSON-pointer refs (`"#/texts/12"`), `contextualized`
130
+ is docling's `contextualize()` rendering to feed the embedding model.
131
+
132
+ #### Streaming chunks
133
+
134
+ `streamFileChunks` / `streamChunks` / `streamDocumentChunks` are the streaming
135
+ counterparts: async generators that yield each chunk **as the chunkers produce
136
+ it** — the first chunk is ready for embedding while the rest of the document
137
+ is still being chunked, and no all-chunks array is materialized. Abandoning
138
+ the generator early (`break`) cancels the background chunking.
139
+
140
+ ```js
141
+ import { streamFileChunks } from 'docling.rs'
142
+
143
+ for await (const c of streamFileChunks('report.docx', {
144
+ chunker: 'hybrid',
145
+ tokenizer: 'tokenizer.json',
146
+ maxTokens: 256,
147
+ })) {
148
+ await embed(c.contextualized) // embedding overlaps the remaining chunking
149
+ }
150
+ ```
151
+
96
152
  ### PDF / images: getting the ML models
97
153
 
98
154
  Declarative formats (Markdown, HTML, DOCX, XLSX, …) are pure Rust and need
@@ -159,12 +215,22 @@ import { Pipeline } from 'docling.rs'
159
215
 
160
216
  const pipeline = new Pipeline({ strict: true })
161
217
  for (const path of pdfPaths) {
162
- const { content } = pipeline.convertFile(path, { to: 'markdown' }) // warm models
218
+ const { content } = await pipeline.convertFileAsync(path, { to: 'json' }) // warm models, off the event loop
219
+ }
220
+
221
+ // Or stream a PDF's Markdown as pages finish converting:
222
+ for await (const chunk of pipeline.streamFileMarkdown('paper.pdf')) {
223
+ process.stdout.write(chunk)
163
224
  }
164
225
  ```
165
226
 
166
- `Pipeline` handles `pdf` and `image` inputs (the ML pipeline) and is synchronous
167
- reuse one instance behind a job queue.
227
+ `Pipeline` handles `pdf` and `image` inputs (the ML pipeline). The sync
228
+ `convertFile` / `convert` block the event loop; the `*Async` variants run on the
229
+ libuv thread pool, and `streamFileMarkdown` yields Markdown chunks in document
230
+ order as pages finish. Conversions on one instance run one at a time (the
231
+ models are mutable sessions) — overlapping `*Async` calls queue in submission
232
+ order, so batch throughput comes from keeping the models warm, not from
233
+ parallel calls.
168
234
 
169
235
  ### Images
170
236
 
@@ -194,12 +260,18 @@ JSON output always embeds extracted images as data URIs.
194
260
  | `convertFileAsync(path, options?)` | `Promise<ConvertResult>` | Off the event loop. |
195
261
  | `convertAsync(input, options?)` | `Promise<ConvertResult>` | Off the event loop. |
196
262
  | `streamFileMarkdown(path, options?)` | `AsyncGenerator<string>` | Markdown chunks in document order. |
263
+ | `chunkFile(path, options?)` | `Chunk[]` | Convert + run docling's hierarchical/hybrid chunker. |
264
+ | `chunk(input, options?)` | `Chunk[]` | Same, over in-memory bytes. |
265
+ | `chunkDocument(documentJson, options?)` | `Chunk[]` | Chunk an already-converted docling JSON document. |
266
+ | `chunkFileAsync` / `chunkAsync` / `chunkDocumentAsync` | `Promise<Chunk[]>` | Off the event loop. |
267
+ | `streamFileChunks` / `streamChunks` / `streamDocumentChunks` | `AsyncGenerator<Chunk>` | Chunks yielded as produced; `break` cancels. |
197
268
  | `supportedFormats()` | `string[]` | Supported input format ids. |
198
269
  | `formatFromName(name)` | `string \| null` | Detect a format id from a filename/extension. |
199
270
  | `checkDependencies(options?)` | `DependencyStatus` | Report which PDF/image deps are present. |
200
271
 
201
272
  `Pipeline` is the reusable warm PDF/image converter: `new Pipeline(converterOptions)`
202
- then `convertFile` / `convert`.
273
+ then `convertFile` / `convert` / `convertFileAsync` / `convertAsync` /
274
+ `convertFileStreaming` / `streamFileMarkdown`.
203
275
 
204
276
  `DocumentConverter` is the reusable form: `new DocumentConverter(converterOptions)`
205
277
  then `convert` / `convertFile` / `convertFileAsync` / `convertAsync` /
package/deps.js CHANGED
@@ -92,9 +92,22 @@ function resolvePaths(dir) {
92
92
  tfDecoder:
93
93
  process.env.DOCLING_TABLEFORMER_DECODER || path.join(models, 'tableformer', 'decoder.onnx'),
94
94
  tfBbox: process.env.DOCLING_TABLEFORMER_BBOX || path.join(models, 'tableformer', 'bbox.onnx'),
95
+ chunkTokenizer:
96
+ process.env.DOCLING_CHUNK_TOKENIZER || path.join(models, 'chunk', 'tokenizer.json'),
95
97
  }
96
98
  }
97
99
 
100
+ /**
101
+ * The hybrid chunker's default tokenizer (all-MiniLM-L6-v2's tokenizer.json,
102
+ * fetched by `scripts/download_dependencies.sh` into `models/chunk/`), resolved
103
+ * through the same install-home logic as the ML models. Returns `null` when not
104
+ * installed — the native side then reports a clear error with the download hint.
105
+ */
106
+ function defaultChunkTokenizer(dir) {
107
+ const p = resolvePaths(dir)
108
+ return fs.existsSync(p.chunkTokenizer) ? p.chunkTokenizer : null
109
+ }
110
+
98
111
  /**
99
112
  * Report which dependencies are present on disk. `ready` is true when the
100
113
  * minimum for PDF (pdfium + layout) is present.
@@ -108,6 +121,7 @@ function checkDependencies(options = {}) {
108
121
  layout: has(p.layout),
109
122
  ocr: has(p.ocrRec) && has(p.ocrDict),
110
123
  tableformer: has(p.tfEncoder) && has(p.tfDecoder) && has(p.tfBbox),
124
+ chunkTokenizer: has(p.chunkTokenizer),
111
125
  }
112
126
  status.ready = status.pdfium && status.layout
113
127
  status.missing = [
@@ -185,4 +199,5 @@ module.exports = {
185
199
  assertMlReady,
186
200
  resolvePaths,
187
201
  exportEnv,
202
+ defaultChunkTokenizer,
188
203
  }
package/index.d.ts CHANGED
@@ -8,9 +8,19 @@ import type {
8
8
  ConvertOptions,
9
9
  ConvertInput,
10
10
  ConvertResult,
11
+ ChunkOptions,
12
+ Chunk,
11
13
  } from './native'
12
14
 
13
- export type { ConverterOptions, OutputOptions, ConvertOptions, ConvertInput, ConvertResult }
15
+ export type {
16
+ ConverterOptions,
17
+ OutputOptions,
18
+ ConvertOptions,
19
+ ConvertInput,
20
+ ConvertResult,
21
+ ChunkOptions,
22
+ Chunk,
23
+ }
14
24
 
15
25
  // Format helpers pass straight through from the native binding.
16
26
  export { supportedFormats, formatFromName } from './native'
@@ -27,6 +37,27 @@ export declare function convertFileAsync(path: string, options?: ConvertOptions
27
37
  /** Async (Promise) bytes conversion, off the event loop. */
28
38
  export declare function convertAsync(input: ConvertInput, options?: ConvertOptions | null): Promise<ConvertResult>
29
39
 
40
+ /**
41
+ * Chunk a file with docling's chunkers: convert it, then run the hierarchical
42
+ * (default) or hybrid (`chunker: 'hybrid'` + `tokenizer`) chunker over the
43
+ * document. Throws for PDF/image/METS if deps aren't installed.
44
+ */
45
+ export declare function chunkFile(path: string, options?: ChunkOptions | null): Array<Chunk>
46
+ /** Async (Promise) {@link chunkFile}; conversion + chunking run off the event loop. */
47
+ export declare function chunkFileAsync(path: string, options?: ChunkOptions | null): Promise<Array<Chunk>>
48
+ /** Chunk in-memory bytes (same input contract as {@link convert}). */
49
+ export declare function chunk(input: ConvertInput, options?: ChunkOptions | null): Array<Chunk>
50
+ /** Async (Promise) {@link chunk}. */
51
+ export declare function chunkAsync(input: ConvertInput, options?: ChunkOptions | null): Promise<Array<Chunk>>
52
+ /**
53
+ * Chunk an already-converted document, passed as docling-core JSON (the
54
+ * `content` of a `convert*` call with `to: 'json'`) — so a document converted
55
+ * once (e.g. through the warm PDF {@link Pipeline}) chunks without re-converting.
56
+ */
57
+ export declare function chunkDocument(documentJson: string, options?: ChunkOptions | null): Array<Chunk>
58
+ /** Async (Promise) {@link chunkDocument}. */
59
+ export declare function chunkDocumentAsync(documentJson: string, options?: ChunkOptions | null): Promise<Array<Chunk>>
60
+
30
61
  /** A reusable converter holding config (strict / fetchImages / allowedFormats). */
31
62
  export declare class DocumentConverter {
32
63
  constructor(options?: ConverterOptions | null)
@@ -37,15 +68,40 @@ export declare class DocumentConverter {
37
68
  convertFileStreaming(path: string, callback: StreamCallback, options?: OutputOptions | null): void
38
69
  }
39
70
 
71
+ /** Output options for {@link Pipeline.streamFileMarkdown} (streamable modes only). */
72
+ export interface PipelineStreamOptions {
73
+ imageMode?: 'placeholder' | 'embedded'
74
+ artifactsDir?: string
75
+ }
76
+
40
77
  /**
41
78
  * A reusable PDF/image pipeline that keeps the ONNX models loaded across calls.
42
79
  * Use instead of the per-call functions when converting many PDFs/images — the
43
80
  * one-shot path reloads every model each call. Handles `pdf` and `image` inputs.
81
+ *
82
+ * The `*Async` variants run the conversion off the event loop; overlapping
83
+ * calls on one instance queue (the models are mutable sessions), so batch
84
+ * throughput comes from keeping the models warm, not from parallel calls.
44
85
  */
45
86
  export declare class Pipeline {
46
87
  constructor(options?: ConverterOptions | null)
47
88
  convertFile(path: string, options?: OutputOptions | null): ConvertResult
48
89
  convert(input: ConvertInput, options?: OutputOptions | null): ConvertResult
90
+ /** Async (Promise) file conversion on the warm pipeline, off the event loop. */
91
+ convertFileAsync(path: string, options?: OutputOptions | null): Promise<ConvertResult>
92
+ /** Async (Promise) bytes conversion on the warm pipeline, off the event loop. */
93
+ convertAsync(input: ConvertInput, options?: OutputOptions | null): Promise<ConvertResult>
94
+ /** Callback-form streaming (prefer {@link Pipeline.streamFileMarkdown}). */
95
+ convertFileStreaming(path: string, callback: StreamCallback, options?: OutputOptions | null): void
96
+ /**
97
+ * Stream a PDF's Markdown in chunks through the warm pipeline, in document
98
+ * order, as pages finish converting (an image arrives as a single chunk).
99
+ * Concatenating the chunks reproduces the buffered Markdown byte-for-byte.
100
+ */
101
+ streamFileMarkdown(
102
+ filePath: string,
103
+ options?: PipelineStreamOptions,
104
+ ): AsyncGenerator<string, void, unknown>
49
105
  }
50
106
 
51
107
  // --- dependency provisioning (PDF/image ML pipeline) -----------------------
@@ -62,6 +118,8 @@ export interface DependencyStatus {
62
118
  ocr: boolean
63
119
  /** TableFormer encoder/decoder/bbox present. */
64
120
  tableformer: boolean
121
+ /** Hybrid-chunker tokenizer (models/chunk/tokenizer.json) present. */
122
+ chunkTokenizer: boolean
65
123
  /** True when the minimum for PDF (pdfium + layout) is present. */
66
124
  ready: boolean
67
125
  /** Human-readable list of the missing required assets. */
@@ -95,3 +153,32 @@ export declare function streamFileMarkdown(
95
153
  filePath: string,
96
154
  options?: StreamOptions,
97
155
  ): AsyncGenerator<string, void, unknown>
156
+
157
+ /**
158
+ * Stream a file's chunks as the chunkers produce them — the streaming
159
+ * counterpart of {@link chunkFile}. The first chunk is ready (e.g. for
160
+ * embedding) while the rest of the document is still being chunked, and no
161
+ * all-chunks array is materialized. Abandoning the generator early (`break`)
162
+ * cancels the background chunking. Throws for PDF/image/METS if deps aren't
163
+ * installed.
164
+ */
165
+ export declare function streamFileChunks(
166
+ filePath: string,
167
+ options?: ChunkOptions | null,
168
+ ): AsyncGenerator<Chunk, void, unknown>
169
+
170
+ /** Streaming counterpart of {@link chunk} (same contract as {@link streamFileChunks}). */
171
+ export declare function streamChunks(
172
+ input: ConvertInput,
173
+ options?: ChunkOptions | null,
174
+ ): AsyncGenerator<Chunk, void, unknown>
175
+
176
+ /**
177
+ * Streaming counterpart of {@link chunkDocument}: chunk an already-converted
178
+ * docling-core JSON document (same contract as {@link streamFileChunks}).
179
+ * Touches no ML models.
180
+ */
181
+ export declare function streamDocumentChunks(
182
+ documentJson: string,
183
+ options?: ChunkOptions | null,
184
+ ): AsyncGenerator<Chunk, void, unknown>
package/index.js CHANGED
@@ -5,14 +5,15 @@
5
5
  // 1. dependency guards — converting a PDF/image/METS input throws a clear
6
6
  // error unless the ML models + pdfium are on disk (see
7
7
  // scripts/download_dependencies.sh);
8
- // 2. a `streamFileMarkdown` async generator over Markdown chunks.
8
+ // 2. `streamFileMarkdown` async generators over Markdown chunks (module-level
9
+ // and on the warm `Pipeline`).
9
10
  //
10
11
  // Works in Node.js and Bun (Bun implements N-API).
11
12
 
12
13
  'use strict'
13
14
 
14
15
  const native = require('./native.js')
15
- const { checkDependencies, assertMlReady } = require('./deps.js')
16
+ const { checkDependencies, assertMlReady, defaultChunkTokenizer } = require('./deps.js')
16
17
 
17
18
  // Resolve the format id of an input for the ML guard. Uses the native
18
19
  // extension→format map; falls back to an explicitly-passed format string.
@@ -23,6 +24,48 @@ function mlFormatOf(name, format) {
23
24
  return native.formatFromName(name || '') || ''
24
25
  }
25
26
 
27
+ // Bridge a native (err, chunk) streaming callback into an async generator.
28
+ // `start` receives the callback and kicks off the native conversion. Chunks
29
+ // are delivered on the event loop (via a threadsafe function); a null chunk
30
+ // ends the stream, a non-null err ends it with a throw.
31
+ async function* chunkStream(start) {
32
+ const queue = []
33
+ let done = false
34
+ let failure = null
35
+ let notify = null
36
+ const wake = () => {
37
+ if (notify) {
38
+ const n = notify
39
+ notify = null
40
+ n()
41
+ }
42
+ }
43
+
44
+ start((err, chunk) => {
45
+ if (err) {
46
+ failure = err
47
+ done = true
48
+ } else if (chunk === null || chunk === undefined) {
49
+ done = true
50
+ } else {
51
+ queue.push(chunk)
52
+ }
53
+ wake()
54
+ })
55
+
56
+ while (true) {
57
+ if (queue.length > 0) {
58
+ yield queue.shift()
59
+ continue
60
+ }
61
+ if (failure) throw failure
62
+ if (done) return
63
+ await new Promise((resolve) => {
64
+ notify = resolve
65
+ })
66
+ }
67
+ }
68
+
26
69
  // --- guarded one-shot functions --------------------------------------------
27
70
 
28
71
  function convertFile(path, options) {
@@ -46,6 +89,48 @@ async function convertAsync(input, options) {
46
89
  return native.convertAsync(input, options)
47
90
  }
48
91
 
92
+ // --- guarded chunking functions ---------------------------------------------
93
+
94
+ // For the hybrid chunker with no explicit tokenizer, resolve the default one
95
+ // (models/chunk/tokenizer.json) through the same install-home logic as the ML
96
+ // models — so DOCLING_RS_HOME / ~/.cache installs work, not only ./models. The
97
+ // native side keeps its own CWD-relative fallback as a backstop.
98
+ function withDefaultTokenizer(options) {
99
+ if (!options || options.tokenizer) return options
100
+ if (String(options.chunker || '').toLowerCase() !== 'hybrid') return options
101
+ const tokenizer = defaultChunkTokenizer()
102
+ return tokenizer ? { ...options, tokenizer } : options
103
+ }
104
+
105
+ function chunkFile(path, options) {
106
+ assertMlReady(mlFormatOf(path))
107
+ return native.chunkFile(path, withDefaultTokenizer(options))
108
+ }
109
+
110
+ function chunk(input, options) {
111
+ assertMlReady(mlFormatOf(input && input.name, input && input.format))
112
+ return native.chunk(input, withDefaultTokenizer(options))
113
+ }
114
+
115
+ // async so a guard failure surfaces as a rejected promise, not a sync throw.
116
+ async function chunkFileAsync(path, options) {
117
+ assertMlReady(mlFormatOf(path))
118
+ return native.chunkFileAsync(path, withDefaultTokenizer(options))
119
+ }
120
+
121
+ async function chunkAsync(input, options) {
122
+ assertMlReady(mlFormatOf(input && input.name, input && input.format))
123
+ return native.chunkAsync(input, withDefaultTokenizer(options))
124
+ }
125
+
126
+ function chunkDocument(documentJson, options) {
127
+ return native.chunkDocument(documentJson, withDefaultTokenizer(options))
128
+ }
129
+
130
+ async function chunkDocumentAsync(documentJson, options) {
131
+ return native.chunkDocumentAsync(documentJson, withDefaultTokenizer(options))
132
+ }
133
+
49
134
  // --- guarded classes --------------------------------------------------------
50
135
 
51
136
  class DocumentConverter {
@@ -94,6 +179,41 @@ class Pipeline {
94
179
  assertMlReady(mlFormatOf(input && input.name, input && input.format))
95
180
  return this._inner.convert(input, options)
96
181
  }
182
+
183
+ // async so a guard failure surfaces as a rejected promise, not a sync throw.
184
+ async convertFileAsync(path, options) {
185
+ assertMlReady(mlFormatOf(path))
186
+ return this._inner.convertFileAsync(path, options)
187
+ }
188
+
189
+ async convertAsync(input, options) {
190
+ assertMlReady(mlFormatOf(input && input.name, input && input.format))
191
+ return this._inner.convertAsync(input, options)
192
+ }
193
+
194
+ convertFileStreaming(path, callback, options) {
195
+ assertMlReady(mlFormatOf(path))
196
+ return this._inner.convertFileStreaming(path, callback, options)
197
+ }
198
+
199
+ /**
200
+ * Stream a PDF's Markdown in chunks through the warm pipeline, in document
201
+ * order, as pages finish converting (an image arrives as a single chunk).
202
+ * Same contract as the module-level `streamFileMarkdown`, but reusing this
203
+ * instance's loaded models — no per-call model reload.
204
+ *
205
+ * @param {string} filePath
206
+ * @param {object} [options] output options (`imageMode`: `placeholder` or
207
+ * `embedded`; `referenced` is rejected)
208
+ * @returns {AsyncGenerator<string, void, unknown>}
209
+ */
210
+ async *streamFileMarkdown(filePath, options = {}) {
211
+ assertMlReady(mlFormatOf(filePath))
212
+ const { imageMode, artifactsDir } = options
213
+ yield* chunkStream((callback) =>
214
+ this._inner.convertFileStreaming(filePath, callback, { imageMode, artifactsDir }),
215
+ )
216
+ }
97
217
  }
98
218
 
99
219
  // --- streaming --------------------------------------------------------------
@@ -113,49 +233,54 @@ async function* streamFileMarkdown(filePath, options = {}) {
113
233
  assertMlReady(mlFormatOf(filePath))
114
234
  const { strict, fetchImages, allowedFormats, imageMode, artifactsDir } = options
115
235
  const converter = new native.DocumentConverter({ strict, fetchImages, allowedFormats })
236
+ yield* chunkStream((callback) =>
237
+ converter.convertFileStreaming(filePath, callback, { imageMode, artifactsDir }),
238
+ )
239
+ }
116
240
 
117
- // Bridge the native (err, chunk) callback into an async generator. Chunks are
118
- // delivered on the event loop (via a threadsafe function); a null chunk ends
119
- // the stream, a non-null err ends it with a throw.
120
- const queue = []
121
- let done = false
122
- let failure = null
123
- let notify = null
124
- const wake = () => {
125
- if (notify) {
126
- const n = notify
127
- notify = null
128
- n()
129
- }
130
- }
241
+ /**
242
+ * Stream a file's chunks as the chunkers produce them the streaming
243
+ * counterpart of `chunkFile`. The first chunk is ready (e.g. for embedding)
244
+ * while the rest of the document is still being chunked, and no all-chunks
245
+ * array is materialized. Abandoning the generator early (`break`) cancels the
246
+ * background chunking.
247
+ *
248
+ * @param {string} filePath
249
+ * @param {object} [options] same `ChunkOptions` as `chunkFile`
250
+ * @returns {AsyncGenerator<Chunk, void, unknown>}
251
+ */
252
+ async function* streamFileChunks(filePath, options = {}) {
253
+ assertMlReady(mlFormatOf(filePath))
254
+ const opts = withDefaultTokenizer(options)
255
+ yield* chunkStream((callback) => native.chunkFileStreaming(filePath, callback, opts))
256
+ }
131
257
 
132
- converter.convertFileStreaming(
133
- filePath,
134
- (err, chunk) => {
135
- if (err) {
136
- failure = err
137
- done = true
138
- } else if (chunk === null || chunk === undefined) {
139
- done = true
140
- } else {
141
- queue.push(chunk)
142
- }
143
- wake()
144
- },
145
- { imageMode, artifactsDir },
146
- )
258
+ /**
259
+ * Streaming counterpart of `chunk`: chunk in-memory bytes, yielding each chunk
260
+ * as it is produced (same contract as {@link streamFileChunks}).
261
+ *
262
+ * @param {object} input same `ConvertInput` as `chunk`
263
+ * @param {object} [options] same `ChunkOptions` as `chunk`
264
+ * @returns {AsyncGenerator<Chunk, void, unknown>}
265
+ */
266
+ async function* streamChunks(input, options = {}) {
267
+ assertMlReady(mlFormatOf(input && input.name, input && input.format))
268
+ const opts = withDefaultTokenizer(options)
269
+ yield* chunkStream((callback) => native.chunkStreaming(input, callback, opts))
270
+ }
147
271
 
148
- while (true) {
149
- if (queue.length > 0) {
150
- yield queue.shift()
151
- continue
152
- }
153
- if (failure) throw failure
154
- if (done) return
155
- await new Promise((resolve) => {
156
- notify = resolve
157
- })
158
- }
272
+ /**
273
+ * Streaming counterpart of `chunkDocument`: chunk an already-converted
274
+ * docling-core JSON document, yielding each chunk as it is produced (same
275
+ * contract as {@link streamFileChunks}). Touches no ML models — unguarded.
276
+ *
277
+ * @param {string} documentJson
278
+ * @param {object} [options] same `ChunkOptions` as `chunkDocument`
279
+ * @returns {AsyncGenerator<Chunk, void, unknown>}
280
+ */
281
+ async function* streamDocumentChunks(documentJson, options = {}) {
282
+ const opts = withDefaultTokenizer(options)
283
+ yield* chunkStream((callback) => native.chunkDocumentStreaming(documentJson, callback, opts))
159
284
  }
160
285
 
161
286
  // --- exports (explicit, so ESM named imports work in Node and Bun) ----------
@@ -164,9 +289,19 @@ module.exports.convert = convert
164
289
  module.exports.convertFile = convertFile
165
290
  module.exports.convertAsync = convertAsync
166
291
  module.exports.convertFileAsync = convertFileAsync
292
+ module.exports.chunk = chunk
293
+ module.exports.chunkFile = chunkFile
294
+ module.exports.chunkAsync = chunkAsync
295
+ module.exports.chunkFileAsync = chunkFileAsync
296
+ // Chunking an already-converted JSON document touches no ML models — unguarded.
297
+ module.exports.chunkDocument = chunkDocument
298
+ module.exports.chunkDocumentAsync = chunkDocumentAsync
167
299
  module.exports.DocumentConverter = DocumentConverter
168
300
  module.exports.Pipeline = Pipeline
169
301
  module.exports.streamFileMarkdown = streamFileMarkdown
302
+ module.exports.streamFileChunks = streamFileChunks
303
+ module.exports.streamChunks = streamChunks
304
+ module.exports.streamDocumentChunks = streamDocumentChunks
170
305
  module.exports.checkDependencies = checkDependencies
171
306
  module.exports.supportedFormats = native.supportedFormats
172
307
  module.exports.formatFromName = native.formatFromName
package/native.d.ts CHANGED
@@ -98,6 +98,100 @@ export declare function convert(input: ConvertInput, options?: ConvertOptions |
98
98
  export declare function convertFileAsync(path: string, options?: ConvertOptions | undefined | null): Promise<ConvertResult>
99
99
  /** Async (Promise-returning) [`convert`]. */
100
100
  export declare function convertAsync(input: ConvertInput, options?: ConvertOptions | undefined | null): Promise<ConvertResult>
101
+ /** Options for the chunk* functions. */
102
+ export interface ChunkOptions {
103
+ /**
104
+ * `"hierarchical"` (default): one chunk per document item, docling's
105
+ * structure-driven chunker. `"hybrid"`: tokenization-aware refinement —
106
+ * splits oversized chunks and merges undersized same-heading neighbours;
107
+ * requires `tokenizer`.
108
+ */
109
+ chunker?: string
110
+ /**
111
+ * Path to a HuggingFace `tokenizer.json` (e.g. all-MiniLM-L6-v2's) for the
112
+ * hybrid chunker's token counts. When omitted, falls back to
113
+ * `models/chunk/tokenizer.json` (populated by
114
+ * `scripts/install/download_dependencies.sh`).
115
+ */
116
+ tokenizer?: string
117
+ /**
118
+ * The hybrid chunker's token budget per chunk. Default `256` (docling's
119
+ * default for the MiniLM embedding model).
120
+ */
121
+ maxTokens?: number
122
+ /**
123
+ * Merge undersized peer chunks with the same headings (hybrid only).
124
+ * Default `true`, matching docling.
125
+ */
126
+ mergePeers?: boolean
127
+ }
128
+ /** One chunk record — the analogue of docling's `DocChunk`. */
129
+ export interface Chunk {
130
+ /** The chunk body (markdown-flavoured text, same as docling's `DocChunk.text`). */
131
+ text: string
132
+ /**
133
+ * The heading path above the chunk, outermost first; absent for content
134
+ * above any heading.
135
+ */
136
+ headings?: Array<string>
137
+ /**
138
+ * JSON-pointer refs of the document items the chunk was built from
139
+ * (`"#/texts/12"`, `"#/tables/0"`, …).
140
+ */
141
+ docItems: Array<string>
142
+ /**
143
+ * The embedding-ready rendering: heading path + text, newline-joined
144
+ * (docling's `chunker.contextualize(chunk)`).
145
+ */
146
+ contextualized: string
147
+ }
148
+ /**
149
+ * Chunk a file on disk with docling's chunkers: convert it, then run the
150
+ * hierarchical (default) or hybrid chunker over the document.
151
+ */
152
+ export declare function chunkFile(path: string, options?: ChunkOptions | undefined | null): Array<Chunk>
153
+ /**
154
+ * Async (Promise-returning) [`chunk_file`]; conversion + chunking run on the
155
+ * libuv thread pool.
156
+ */
157
+ export declare function chunkFileAsync(path: string, options?: ChunkOptions | undefined | null): Promise<Array<Chunk>>
158
+ /** Chunk in-memory bytes (same contract as [`convert`], then chunk). */
159
+ export declare function chunk(input: ConvertInput, options?: ChunkOptions | undefined | null): Array<Chunk>
160
+ /** Async (Promise-returning) [`chunk`]. */
161
+ export declare function chunkAsync(input: ConvertInput, options?: ChunkOptions | undefined | null): Promise<Array<Chunk>>
162
+ /**
163
+ * Chunk an already-converted document, passed as docling-core JSON (the
164
+ * `content` of a `convert*` call with `to: "json"`) — so a document converted
165
+ * once (e.g. through the warm PDF `Pipeline`) can be chunked without
166
+ * re-converting.
167
+ */
168
+ export declare function chunkDocument(documentJson: string, options?: ChunkOptions | undefined | null): Array<Chunk>
169
+ /** Async (Promise-returning) [`chunk_document`]. */
170
+ export declare function chunkDocumentAsync(documentJson: string, options?: ChunkOptions | undefined | null): Promise<Array<Chunk>>
171
+ /**
172
+ * Chunk a file and stream each chunk as the chunkers produce it — no
173
+ * all-chunks array is materialized, and the first chunk reaches JS while the
174
+ * rest of the document is still being chunked.
175
+ *
176
+ * `callback` is invoked as `(err, chunk)`: once per chunk with `chunk` a
177
+ * `Chunk`, once with `chunk === null` at the end, or once with a non-null
178
+ * `err` on failure. Prefer the `streamFileChunks` async-generator wrapper in
179
+ * JS over calling this directly.
180
+ */
181
+ export declare function chunkFileStreaming(path: string, callback: (err: Error | null, arg: Chunk | undefined | null) => any, options?: ChunkOptions | undefined | null): void
182
+ /**
183
+ * Streaming [`chunk`]: chunk in-memory bytes, pushing each chunk through the
184
+ * callback (same contract as [`chunk_file_streaming`]). Prefer the
185
+ * `streamChunks` async-generator wrapper in JS.
186
+ */
187
+ export declare function chunkStreaming(input: ConvertInput, callback: (err: Error | null, arg: Chunk | undefined | null) => any, options?: ChunkOptions | undefined | null): void
188
+ /**
189
+ * Streaming [`chunk_document`]: chunk an already-converted docling-core JSON
190
+ * document, pushing each chunk through the callback (same contract as
191
+ * [`chunk_file_streaming`]). Prefer the `streamDocumentChunks`
192
+ * async-generator wrapper in JS.
193
+ */
194
+ export declare function chunkDocumentStreaming(documentJson: string, callback: (err: Error | null, arg: Chunk | undefined | null) => any, options?: ChunkOptions | undefined | null): void
101
195
  /** The list of supported input format ids. */
102
196
  export declare function supportedFormats(): Array<string>
103
197
  /**
@@ -154,4 +248,25 @@ export declare class Pipeline {
154
248
  convertFile(path: string, options?: OutputOptions | undefined | null): ConvertResult
155
249
  /** Convert PDF or image bytes, reusing the warm models. */
156
250
  convert(input: ConvertInput, options?: OutputOptions | undefined | null): ConvertResult
251
+ /**
252
+ * Async (Promise-returning) file conversion on the warm pipeline. The
253
+ * CPU-bound work runs on the libuv thread pool, keeping the event loop
254
+ * free; calls on the same instance run one at a time (the models are
255
+ * mutable sessions), so overlapping Promises queue in submission order.
256
+ */
257
+ convertFileAsync(path: string, options?: OutputOptions | undefined | null): Promise<ConvertResult>
258
+ /** Async (Promise-returning) bytes conversion on the warm pipeline. */
259
+ convertAsync(input: ConvertInput, options?: OutputOptions | undefined | null): Promise<ConvertResult>
260
+ /**
261
+ * Stream a PDF's Markdown in chunks through the warm pipeline, in document
262
+ * order, as pages finish converting (an image converts in one step and
263
+ * arrives as a single chunk).
264
+ *
265
+ * `callback` is invoked as `(err, chunk)`: once per Markdown chunk with
266
+ * `chunk` a string, once with `chunk === null` at the end, or once with a
267
+ * non-null `err` on failure. Only `placeholder` / `embedded` image modes
268
+ * stream; `referenced` is rejected. Prefer the `streamFileMarkdown`
269
+ * async-generator wrapper in JS over calling this directly.
270
+ */
271
+ convertFileStreaming(path: string, callback: (err: Error | null, arg: string | undefined | null) => any, options?: OutputOptions | undefined | null): void
157
272
  }
package/native.js CHANGED
@@ -310,7 +310,7 @@ if (!nativeBinding) {
310
310
  throw new Error(`Failed to load native binding`)
311
311
  }
312
312
 
313
- const { convertFile, convert, convertFileAsync, convertAsync, DocumentConverter, Pipeline, supportedFormats, formatFromName } = nativeBinding
313
+ const { convertFile, convert, convertFileAsync, convertAsync, DocumentConverter, Pipeline, chunkFile, chunkFileAsync, chunk, chunkAsync, chunkDocument, chunkDocumentAsync, chunkFileStreaming, chunkStreaming, chunkDocumentStreaming, supportedFormats, formatFromName } = nativeBinding
314
314
 
315
315
  module.exports.convertFile = convertFile
316
316
  module.exports.convert = convert
@@ -318,5 +318,14 @@ module.exports.convertFileAsync = convertFileAsync
318
318
  module.exports.convertAsync = convertAsync
319
319
  module.exports.DocumentConverter = DocumentConverter
320
320
  module.exports.Pipeline = Pipeline
321
+ module.exports.chunkFile = chunkFile
322
+ module.exports.chunkFileAsync = chunkFileAsync
323
+ module.exports.chunk = chunk
324
+ module.exports.chunkAsync = chunkAsync
325
+ module.exports.chunkDocument = chunkDocument
326
+ module.exports.chunkDocumentAsync = chunkDocumentAsync
327
+ module.exports.chunkFileStreaming = chunkFileStreaming
328
+ module.exports.chunkStreaming = chunkStreaming
329
+ module.exports.chunkDocumentStreaming = chunkDocumentStreaming
321
330
  module.exports.supportedFormats = supportedFormats
322
331
  module.exports.formatFromName = formatFromName
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docling.rs",
3
- "version": "0.32.0",
3
+ "version": "0.37.0",
4
4
  "description": "Node.js / Bun bindings for docling.rs — a Rust port of docling. Convert Markdown, HTML, DOCX, PPTX, XLSX, PDF, images and more into a unified DoclingDocument (Markdown or docling-core JSON).",
5
5
  "keywords": [
6
6
  "docling",
@@ -64,8 +64,8 @@
64
64
  "@napi-rs/cli": "^2.18.4"
65
65
  },
66
66
  "optionalDependencies": {
67
- "docling.rs-linux-x64-gnu": "0.32.0",
68
- "docling.rs-linux-arm64-gnu": "0.32.0",
69
- "docling.rs-win32-x64-msvc": "0.32.0"
67
+ "docling.rs-linux-x64-gnu": "0.37.0",
68
+ "docling.rs-linux-arm64-gnu": "0.37.0",
69
+ "docling.rs-win32-x64-msvc": "0.37.0"
70
70
  }
71
71
  }