@simpleplatform/sdk 2.3.0 → 2.4.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
@@ -382,6 +382,60 @@ console.log(result.data.summary) // "Customer called regarding..."
382
382
  console.log(result.data.participants) // ["Customer", "Support Agent"]
383
383
  ```
384
384
 
385
+ #### Transcribe PDF Pages
386
+
387
+ Read the pages of a PDF that have no usable text of their own — scans,
388
+ image-only exhibits — from their images. Each such page is transcribed once,
389
+ and it is the same transcription an `extract` or `summarize` that asks for text
390
+ is given in the page's place, so a quote on an image page can be checked
391
+ against the text the answer was built on. Pages with a readable text layer are
392
+ not returned.
393
+
394
+ ```typescript
395
+ import { transcribePages } from '@simpleplatform/sdk/ai'
396
+
397
+ const { data } = await transcribePages(
398
+ { ...contract, first_page: 40, last_page: 52 },
399
+ {},
400
+ request.context
401
+ )
402
+
403
+ for (const page of data.pages) {
404
+ if ('error' in page) {
405
+ console.log(`page ${page.page} could not be read: ${page.error}`)
406
+ continue
407
+ }
408
+
409
+ console.log(page.page, page.text.includes(quote))
410
+ }
411
+ ```
412
+
413
+ Pages are numbered in the original document. Transcriptions are kept per
414
+ version of the file and page, so a page already read for an `extract` or
415
+ `summarize` that asked for text (`deliver_as: 'text'`) is not read again.
416
+
417
+ A page is not transcribed twice to check itself: that would be the same model
418
+ reading the same image again, doubling the cost of every scanned page without
419
+ adding independence. To check an answer independently, read the pages a second
420
+ way — as the document itself (`deliver_as: 'document'`) — and compare.
421
+
422
+ #### How Files Travelled
423
+
424
+ Every AI result says how each file it carried reached the model, in
425
+ `metadata.delivery`: `deliveredAs` (`'document'`, `'text'` or `'image'`), the
426
+ range it was cut to, the pages transcribed from their images, and — when text
427
+ was asked for and the document was sent instead — a `fallback` naming the pages
428
+ that could not be read and why.
429
+
430
+ ```typescript
431
+ const result = await extract({ ...contract, deliver_as: 'text' }, { prompt, schema }, request.context)
432
+
433
+ for (const file of result.metadata.delivery ?? []) {
434
+ if (file.fallback)
435
+ console.log(`${file.filename} was read as a PDF: ${file.fallback.message}`)
436
+ }
437
+ ```
438
+
385
439
  ### GraphQL Module
386
440
 
387
441
  Execute type-safe database operations with GraphQL:
package/dist/ai.d.ts CHANGED
@@ -88,8 +88,12 @@ export type JSONSchema = JSONSchemaArray | JSONSchemaBoolean | JSONSchemaNumber
88
88
  * file,
89
89
  * which only ever travels as text.
90
90
  *
91
- * A page the platform cannot read as text sends the pages asked for as a PDF
92
- * instead, so an answer is never built on text with a hole in it.
91
+ * A page with no usable text of its own — a scan, or text that reads as noise
92
+ * — is read from its image instead, and that text stands in its place, marked
93
+ * `[Transcribed from the page image.]` under its page label. Only if that
94
+ * reading fails are the pages asked for sent as a PDF, so an answer is never
95
+ * built on text with a hole in it. The answer's `metadata.delivery` says which
96
+ * happened.
93
97
  *
94
98
  * Pages asked for as text arrive numbered from 1, because by then they are a
95
99
  * document of their own. A line above them says which pages of which document
@@ -203,6 +207,62 @@ export interface AITranscribeOptions extends Omit<AICommonOptions, 'prompt'> {
203
207
  */
204
208
  summarize?: boolean;
205
209
  }
210
+ /**
211
+ * A PDF whose pages are to be transcribed, and optionally which of them.
212
+ *
213
+ * `first_page` and `last_page` work as they do for any AI operation: counted
214
+ * from 1, both ends included, named together or not at all, and cut out first.
215
+ * `deliver_as` has no meaning here, because the pages are answered as text.
216
+ */
217
+ export type AITranscribePagesInput = DocumentHandle & Omit<AIDocumentInput, 'deliver_as' | keyof DocumentHandle>;
218
+ /**
219
+ * Configuration options for `transcribePages`. There is no prompt and no
220
+ * reasoning switch: the instructions are the platform's, and every page is
221
+ * read at the platform's own accuracy-first effort.
222
+ */
223
+ export interface AITranscribePagesOptions {
224
+ /**
225
+ * If true, runs the operation again rather than serving its kept answer.
226
+ * A page already read is still served from its kept read, which is tied to
227
+ * this version of the file and to the platform's instructions.
228
+ */
229
+ regenerate?: boolean;
230
+ /** (Optional) The maximum time in milliseconds to wait for the operation. */
231
+ timeout?: number;
232
+ }
233
+ /**
234
+ * One page's transcription, or why it has none. Pages are numbered in the
235
+ * original document.
236
+ */
237
+ export type AIPageTranscription = {
238
+ /** Why the page could not be read. No partial text is given. */
239
+ error: string;
240
+ /** The page's number in the original document. */
241
+ page: number;
242
+ } | {
243
+ /** The page's number in the original document. */
244
+ page: number;
245
+ /**
246
+ * The page's text as read from its image: verbatim markdown, in reading
247
+ * order, tables as markdown tables, `[illegible]` where a word could not
248
+ * be read. An empty string is a read that found the page blank.
249
+ */
250
+ text: string;
251
+ };
252
+ /**
253
+ * The response of `transcribePages`.
254
+ */
255
+ export interface AITranscriptionResult {
256
+ /** Every page of the document, or of the range, that has no usable text layer. */
257
+ data: {
258
+ pages: AIPageTranscription[];
259
+ };
260
+ /**
261
+ * The operation's metadata. Its token counts are zero: each page read is
262
+ * charged where it was made, once, whichever operation asked for it.
263
+ */
264
+ metadata: AIExecutionResult['metadata'];
265
+ }
206
266
  /**
207
267
  * Options for searching faces within the Face Engine.
208
268
  */
@@ -218,6 +278,34 @@ export interface AIFaceSearchOptions {
218
278
  */
219
279
  similarityThreshold?: number;
220
280
  }
281
+ /**
282
+ * How one file an AI operation carried reached the model. Page numbers are
283
+ * the original document's, whatever range was cut from it.
284
+ */
285
+ export interface AIFileDelivery {
286
+ /** How the file travelled: as the document itself, as its text, or as an image. */
287
+ deliveredAs: 'document' | 'image' | 'text';
288
+ /**
289
+ * Present only when text was asked for and the document was sent instead:
290
+ * the pages whose images could not be read, and why.
291
+ */
292
+ fallback?: {
293
+ /** What went wrong, page by page, in the platform's words. */
294
+ message: string;
295
+ /** The pages that could not be transcribed. */
296
+ pages: number[];
297
+ /** Why the document travelled instead of its text. */
298
+ reason: 'transcription_failed';
299
+ };
300
+ /** The file's name. */
301
+ filename: string;
302
+ /** The first page of the range the file was cut to, when one was named. */
303
+ firstPage?: number;
304
+ /** The last page of the range the file was cut to, when one was named. */
305
+ lastPage?: number;
306
+ /** The pages whose text was read from their images rather than their text layer. */
307
+ transcribedPages: number[];
308
+ }
221
309
  /**
222
310
  * The response structure from a successful AI `extract` or `summarize` operation.
223
311
  */
@@ -232,6 +320,12 @@ export interface AIExecutionResult {
232
320
  * providing context to the user.
233
321
  */
234
322
  metadata: {
323
+ /**
324
+ * How each file the operation carried reached the model, in input order.
325
+ * Absent when the operation carried no file, and on an answer kept from
326
+ * before the platform reported it.
327
+ */
328
+ delivery?: AIFileDelivery[];
235
329
  /** The number of tokens in the input prompt. */
236
330
  inputTokens: number;
237
331
  /** The number of tokens in the generated output. */
@@ -301,6 +395,48 @@ export declare function summarize(input: AIDocumentInput | DocumentHandle | obje
301
395
  * // Returns: { language: "en", summary: "...", participants: ["Participant 1", "Participant 2"] }
302
396
  */
303
397
  export declare function transcribe(input: DocumentHandle, options: AITranscribeOptions, context: Context): Promise<AIExecutionResult>;
398
+ /**
399
+ * Transcribes the pages of a PDF that have no usable text of their own — scans,
400
+ * image-only exhibits, text that reads as noise — from their images.
401
+ *
402
+ * Every such page is read once, and it is the same transcription an `extract`
403
+ * or `summarize` that asked for text is given in the page's place, so a caller
404
+ * can check that a quote on an image page is really there by looking for it in
405
+ * the text the answer was built on. Pages the platform reads as text are
406
+ * neither read nor returned.
407
+ *
408
+ * A page is not transcribed a second time to check the first: that would be
409
+ * the same model reading the same image again, doubling the cost of every
410
+ * scanned page without being independent of the first read. An independent
411
+ * check reads the pages a second way — as the document itself
412
+ * (`deliver_as: 'document'`) — and compares the answers.
413
+ *
414
+ * Transcriptions are kept per version of the file and page, and shared with
415
+ * every other AI operation: a page already read for an `extract` or
416
+ * `summarize` that asked for text is not read again.
417
+ *
418
+ * @param document The PDF, as a DocumentHandle. Add `first_page` and
419
+ * `last_page` to transcribe only those pages; they are counted from 1 and
420
+ * the answer numbers pages in the original document.
421
+ * @param options Optional `regenerate` and `timeout`.
422
+ * @param context The execution context provided by the host.
423
+ * @returns A promise that resolves to each page's transcription, or why it has none.
424
+ * @throws Will throw an error if the operation fails or the input is not a PDF.
425
+ *
426
+ * @example
427
+ * const { data } = await transcribePages(
428
+ * { ...contract, first_page: 40, last_page: 52 },
429
+ * {},
430
+ * context,
431
+ * )
432
+ *
433
+ * for (const page of data.pages) {
434
+ * if ('error' in page)
435
+ * continue
436
+ * const quoted = page.text.includes(quote)
437
+ * }
438
+ */
439
+ export declare function transcribePages(document: AITranscribePagesInput, options: AITranscribePagesOptions, context: Context): Promise<AITranscriptionResult>;
304
440
  /**
305
441
  * Enrolls a face for a subject. The face is associated with the given `subjectId`
306
442
  * within the tenant's secure collection.
package/dist/ai.js CHANGED
@@ -85,6 +85,7 @@ async function _executeAIOperation(operation, input, options, context) {
85
85
  return {
86
86
  data,
87
87
  metadata: {
88
+ ...(Array.isArray(metadata.delivery) && { delivery: metadata.delivery.map(_fileDelivery) }),
88
89
  inputTokens: metadata.input_tokens,
89
90
  outputTokens: metadata.output_tokens,
90
91
  reasoning: metadata.reasoning,
@@ -92,6 +93,38 @@ async function _executeAIOperation(operation, input, options, context) {
92
93
  },
93
94
  };
94
95
  }
96
+ /**
97
+ * One file's delivery as the platform reports it, in the SDK's own casing.
98
+ * A key the platform sends as `null` is left out rather than carried as one.
99
+ *
100
+ * @internal
101
+ */
102
+ function _fileDelivery(file) {
103
+ return {
104
+ deliveredAs: file.delivered_as,
105
+ ...(file.fallback && { fallback: file.fallback }),
106
+ filename: file.filename,
107
+ ...(typeof file.first_page === 'number' && { firstPage: file.first_page }),
108
+ ...(typeof file.last_page === 'number' && { lastPage: file.last_page }),
109
+ transcribedPages: Array.isArray(file.transcribed_pages) ? file.transcribed_pages : [],
110
+ };
111
+ }
112
+ /**
113
+ * One page's transcription as the platform reports it: its text or its error,
114
+ * never both. An error wins, so no partial text is handed on, and a page that
115
+ * carries no text is reported as unread rather than as text that is not there.
116
+ *
117
+ * @internal
118
+ */
119
+ function _pageTranscription(page) {
120
+ if (typeof page.text === 'string' && typeof page.error !== 'string') {
121
+ return { page: page.page, text: page.text };
122
+ }
123
+ return {
124
+ error: typeof page.error === 'string' ? page.error : 'no transcription was returned for this page',
125
+ page: page.page,
126
+ };
127
+ }
95
128
  // ============================================================================
96
129
  // Public SDK Functions
97
130
  // ============================================================================
@@ -284,6 +317,61 @@ export async function transcribe(input, options, context) {
284
317
  schema,
285
318
  }, context);
286
319
  }
320
+ /**
321
+ * Transcribes the pages of a PDF that have no usable text of their own — scans,
322
+ * image-only exhibits, text that reads as noise — from their images.
323
+ *
324
+ * Every such page is read once, and it is the same transcription an `extract`
325
+ * or `summarize` that asked for text is given in the page's place, so a caller
326
+ * can check that a quote on an image page is really there by looking for it in
327
+ * the text the answer was built on. Pages the platform reads as text are
328
+ * neither read nor returned.
329
+ *
330
+ * A page is not transcribed a second time to check the first: that would be
331
+ * the same model reading the same image again, doubling the cost of every
332
+ * scanned page without being independent of the first read. An independent
333
+ * check reads the pages a second way — as the document itself
334
+ * (`deliver_as: 'document'`) — and compares the answers.
335
+ *
336
+ * Transcriptions are kept per version of the file and page, and shared with
337
+ * every other AI operation: a page already read for an `extract` or
338
+ * `summarize` that asked for text is not read again.
339
+ *
340
+ * @param document The PDF, as a DocumentHandle. Add `first_page` and
341
+ * `last_page` to transcribe only those pages; they are counted from 1 and
342
+ * the answer numbers pages in the original document.
343
+ * @param options Optional `regenerate` and `timeout`.
344
+ * @param context The execution context provided by the host.
345
+ * @returns A promise that resolves to each page's transcription, or why it has none.
346
+ * @throws Will throw an error if the operation fails or the input is not a PDF.
347
+ *
348
+ * @example
349
+ * const { data } = await transcribePages(
350
+ * { ...contract, first_page: 40, last_page: 52 },
351
+ * {},
352
+ * context,
353
+ * )
354
+ *
355
+ * for (const page of data.pages) {
356
+ * if ('error' in page)
357
+ * continue
358
+ * const quoted = page.text.includes(quote)
359
+ * }
360
+ */
361
+ export async function transcribePages(document, options, context) {
362
+ if (!document || typeof document !== 'object' || !document.file_hash) {
363
+ throw new Error('The `document` parameter must be a valid DocumentHandle for `transcribePages`.');
364
+ }
365
+ const mimeType = (document.mime_type || '').split(';')[0].trim().toLowerCase();
366
+ if (mimeType !== 'application/pdf') {
367
+ throw new Error('`transcribePages` reads the pages of a PDF.');
368
+ }
369
+ const result = await _executeAIOperation('transcribe', document, options, context);
370
+ return {
371
+ data: { pages: Array.isArray(result.data?.pages) ? result.data.pages.map(_pageTranscription) : [] },
372
+ metadata: result.metadata,
373
+ };
374
+ }
287
375
  // ============================================================================
288
376
  // Face Recognition API
289
377
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simpleplatform/sdk",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Simple Platform Typescript SDK",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://docs.simple.dev",