@vertesia/workflow 0.51.0 → 0.52.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 (141) hide show
  1. package/lib/cjs/activities/advanced/createOrUpdateDocumentFromInteractionRun.js +7 -1
  2. package/lib/cjs/activities/advanced/createOrUpdateDocumentFromInteractionRun.js.map +1 -1
  3. package/lib/cjs/activities/chunkDocument.js +39 -34
  4. package/lib/cjs/activities/chunkDocument.js.map +1 -1
  5. package/lib/cjs/activities/createDocumentFromOther.js +2 -2
  6. package/lib/cjs/activities/createDocumentFromOther.js.map +1 -1
  7. package/lib/cjs/activities/executeInteraction.js +11 -5
  8. package/lib/cjs/activities/executeInteraction.js.map +1 -1
  9. package/lib/cjs/activities/extractDocumentText.js +24 -6
  10. package/lib/cjs/activities/extractDocumentText.js.map +1 -1
  11. package/lib/cjs/activities/generateDocumentProperties.js +22 -4
  12. package/lib/cjs/activities/generateDocumentProperties.js.map +1 -1
  13. package/lib/cjs/activities/generateEmbeddings.js +58 -102
  14. package/lib/cjs/activities/generateEmbeddings.js.map +1 -1
  15. package/lib/cjs/activities/generateImageRendition.js +77 -34
  16. package/lib/cjs/activities/generateImageRendition.js.map +1 -1
  17. package/lib/cjs/activities/generateOrAssignContentType.js +3 -7
  18. package/lib/cjs/activities/generateOrAssignContentType.js.map +1 -1
  19. package/lib/cjs/activities/notifyWebhook.js.map +1 -1
  20. package/lib/cjs/conversion/image.js +80 -12
  21. package/lib/cjs/conversion/image.js.map +1 -1
  22. package/lib/cjs/dsl/setup/ActivityContext.js +30 -6
  23. package/lib/cjs/dsl/setup/ActivityContext.js.map +1 -1
  24. package/lib/cjs/dsl.js +1 -1
  25. package/lib/cjs/dsl.js.map +1 -1
  26. package/lib/cjs/errors.js +13 -1
  27. package/lib/cjs/errors.js.map +1 -1
  28. package/lib/cjs/iterative-generation/iterativeGenerationWorkflow.js +2 -1
  29. package/lib/cjs/iterative-generation/iterativeGenerationWorkflow.js.map +1 -1
  30. package/lib/cjs/system/notifyWebhookWorkflow.js +2 -1
  31. package/lib/cjs/system/notifyWebhookWorkflow.js.map +1 -1
  32. package/lib/cjs/system/recalculateEmbeddingsWorkflow.js +1 -1
  33. package/lib/cjs/system/recalculateEmbeddingsWorkflow.js.map +1 -1
  34. package/lib/cjs/utils/blobs.js +12 -6
  35. package/lib/cjs/utils/blobs.js.map +1 -1
  36. package/lib/cjs/utils/chunks.js +14 -0
  37. package/lib/cjs/utils/chunks.js.map +1 -0
  38. package/lib/cjs/utils/client.js +4 -3
  39. package/lib/cjs/utils/client.js.map +1 -1
  40. package/lib/cjs/utils/memory.js +2 -9
  41. package/lib/cjs/utils/memory.js.map +1 -1
  42. package/lib/esm/activities/advanced/createOrUpdateDocumentFromInteractionRun.js +7 -1
  43. package/lib/esm/activities/advanced/createOrUpdateDocumentFromInteractionRun.js.map +1 -1
  44. package/lib/esm/activities/chunkDocument.js +39 -34
  45. package/lib/esm/activities/chunkDocument.js.map +1 -1
  46. package/lib/esm/activities/createDocumentFromOther.js +1 -1
  47. package/lib/esm/activities/createDocumentFromOther.js.map +1 -1
  48. package/lib/esm/activities/executeInteraction.js +11 -5
  49. package/lib/esm/activities/executeInteraction.js.map +1 -1
  50. package/lib/esm/activities/extractDocumentText.js +24 -6
  51. package/lib/esm/activities/extractDocumentText.js.map +1 -1
  52. package/lib/esm/activities/generateDocumentProperties.js +22 -4
  53. package/lib/esm/activities/generateDocumentProperties.js.map +1 -1
  54. package/lib/esm/activities/generateEmbeddings.js +58 -69
  55. package/lib/esm/activities/generateEmbeddings.js.map +1 -1
  56. package/lib/esm/activities/generateImageRendition.js +78 -35
  57. package/lib/esm/activities/generateImageRendition.js.map +1 -1
  58. package/lib/esm/activities/generateOrAssignContentType.js +3 -7
  59. package/lib/esm/activities/generateOrAssignContentType.js.map +1 -1
  60. package/lib/esm/activities/notifyWebhook.js.map +1 -1
  61. package/lib/esm/conversion/image.js +80 -12
  62. package/lib/esm/conversion/image.js.map +1 -1
  63. package/lib/esm/dsl/setup/ActivityContext.js +31 -7
  64. package/lib/esm/dsl/setup/ActivityContext.js.map +1 -1
  65. package/lib/esm/dsl.js +1 -1
  66. package/lib/esm/dsl.js.map +1 -1
  67. package/lib/esm/errors.js +11 -0
  68. package/lib/esm/errors.js.map +1 -1
  69. package/lib/esm/iterative-generation/iterativeGenerationWorkflow.js +2 -1
  70. package/lib/esm/iterative-generation/iterativeGenerationWorkflow.js.map +1 -1
  71. package/lib/esm/system/notifyWebhookWorkflow.js +2 -1
  72. package/lib/esm/system/notifyWebhookWorkflow.js.map +1 -1
  73. package/lib/esm/system/recalculateEmbeddingsWorkflow.js +2 -2
  74. package/lib/esm/system/recalculateEmbeddingsWorkflow.js.map +1 -1
  75. package/lib/esm/utils/blobs.js +12 -6
  76. package/lib/esm/utils/blobs.js.map +1 -1
  77. package/lib/esm/utils/chunks.js +9 -0
  78. package/lib/esm/utils/chunks.js.map +1 -0
  79. package/lib/esm/utils/client.js +4 -3
  80. package/lib/esm/utils/client.js.map +1 -1
  81. package/lib/esm/utils/memory.js +2 -7
  82. package/lib/esm/utils/memory.js.map +1 -1
  83. package/lib/types/activities/advanced/createOrUpdateDocumentFromInteractionRun.d.ts +10 -0
  84. package/lib/types/activities/advanced/createOrUpdateDocumentFromInteractionRun.d.ts.map +1 -1
  85. package/lib/types/activities/chunkDocument.d.ts +15 -0
  86. package/lib/types/activities/chunkDocument.d.ts.map +1 -1
  87. package/lib/types/activities/createDocumentFromOther.d.ts.map +1 -1
  88. package/lib/types/activities/executeInteraction.d.ts +14 -3
  89. package/lib/types/activities/executeInteraction.d.ts.map +1 -1
  90. package/lib/types/activities/generateDocumentProperties.d.ts +1 -1
  91. package/lib/types/activities/generateDocumentProperties.d.ts.map +1 -1
  92. package/lib/types/activities/generateEmbeddings.d.ts +21 -17
  93. package/lib/types/activities/generateEmbeddings.d.ts.map +1 -1
  94. package/lib/types/activities/generateImageRendition.d.ts +3 -5
  95. package/lib/types/activities/generateImageRendition.d.ts.map +1 -1
  96. package/lib/types/activities/generateOrAssignContentType.d.ts.map +1 -1
  97. package/lib/types/activities/notifyWebhook.d.ts +1 -2
  98. package/lib/types/activities/notifyWebhook.d.ts.map +1 -1
  99. package/lib/types/conversion/image.d.ts +8 -6
  100. package/lib/types/conversion/image.d.ts.map +1 -1
  101. package/lib/types/dsl/setup/ActivityContext.d.ts +3 -0
  102. package/lib/types/dsl/setup/ActivityContext.d.ts.map +1 -1
  103. package/lib/types/dsl.d.ts +1 -1
  104. package/lib/types/dsl.d.ts.map +1 -1
  105. package/lib/types/errors.d.ts +6 -0
  106. package/lib/types/errors.d.ts.map +1 -1
  107. package/lib/types/iterative-generation/iterativeGenerationWorkflow.d.ts.map +1 -1
  108. package/lib/types/system/notifyWebhookWorkflow.d.ts.map +1 -1
  109. package/lib/types/system/recalculateEmbeddingsWorkflow.d.ts +2 -17
  110. package/lib/types/system/recalculateEmbeddingsWorkflow.d.ts.map +1 -1
  111. package/lib/types/utils/blobs.d.ts.map +1 -1
  112. package/lib/types/utils/chunks.d.ts +9 -0
  113. package/lib/types/utils/chunks.d.ts.map +1 -0
  114. package/lib/types/utils/client.d.ts.map +1 -1
  115. package/lib/types/utils/memory.d.ts +1 -5
  116. package/lib/types/utils/memory.d.ts.map +1 -1
  117. package/lib/workflows-bundle.js +15394 -14602
  118. package/package.json +8 -6
  119. package/src/activities/advanced/createOrUpdateDocumentFromInteractionRun.ts +20 -1
  120. package/src/activities/chunkDocument.ts +62 -42
  121. package/src/activities/createDocumentFromOther.ts +1 -1
  122. package/src/activities/executeInteraction.ts +27 -9
  123. package/src/activities/extractDocumentText.ts +28 -7
  124. package/src/activities/generateDocumentProperties.ts +37 -16
  125. package/src/activities/generateEmbeddings.ts +91 -79
  126. package/src/activities/generateImageRendition.ts +100 -53
  127. package/src/activities/generateOrAssignContentType.ts +5 -11
  128. package/src/activities/notifyWebhook.ts +2 -2
  129. package/src/conversion/image.test.ts +110 -18
  130. package/src/conversion/image.ts +90 -15
  131. package/src/conversion/pandoc.test.ts +7 -5
  132. package/src/dsl/setup/ActivityContext.ts +57 -16
  133. package/src/dsl.ts +1 -1
  134. package/src/errors.ts +27 -6
  135. package/src/iterative-generation/iterativeGenerationWorkflow.ts +2 -1
  136. package/src/system/notifyWebhookWorkflow.ts +2 -1
  137. package/src/system/recalculateEmbeddingsWorkflow.ts +2 -2
  138. package/src/utils/blobs.ts +11 -6
  139. package/src/utils/chunks.ts +17 -0
  140. package/src/utils/client.ts +4 -3
  141. package/src/utils/memory.ts +3 -8
@@ -1,19 +1,42 @@
1
- import { VertesiaClient } from "@vertesia/client";
2
- import { ContentObject, DSLActivityExecutionPayload, DSLActivitySpec, ProjectConfigurationEmbeddings, SupportedEmbeddingTypes } from "@vertesia/common";
3
1
  import { EmbeddingsResult } from "@llumiverse/core";
4
2
  import { log } from "@temporalio/activity";
5
- import * as tf from '@tensorflow/tfjs-node';
3
+ import { VertesiaClient } from "@vertesia/client";
4
+ import { ContentObject, DSLActivityExecutionPayload, DSLActivitySpec, ProjectConfigurationEmbeddings, SupportedEmbeddingTypes } from "@vertesia/common";
6
5
  import { setupActivity } from "../dsl/setup/ActivityContext.js";
7
6
  import { NoDocumentFound } from '../errors.js';
8
7
  import { fetchBlobAsBase64, md5 } from "../utils/blobs.js";
8
+ import { DocPart, getContentParts } from "../utils/chunks.js";
9
9
  import { countTokens } from "../utils/tokens.js";
10
10
 
11
11
 
12
12
  export interface GenerateEmbeddingsParams {
13
+
14
+ /**
15
+ * The model to use for embedding generation
16
+ * If not set, the default model for the project will be used
17
+ */
13
18
  model?: string;
19
+
20
+ /**
21
+ * The environment to use for embedding generation
22
+ * If not set, the default environment for the project will be used
23
+ */
14
24
  environment?: string;
25
+
26
+ /**
27
+ * If true, force embedding generation even if the document already has embeddings
28
+ */
15
29
  force?: boolean;
30
+
31
+ /**
32
+ * The embedding type to generate
33
+ */
16
34
  type: SupportedEmbeddingTypes;
35
+
36
+ /**
37
+ * The DocParts to use for long documents
38
+ */
39
+ parts?: DocPart[];
17
40
  }
18
41
 
19
42
  export interface GenerateEmbeddings extends DSLActivitySpec<GenerateEmbeddingsParams> {
@@ -103,7 +126,7 @@ interface ExecuteGenerateEmbeddingsParams {
103
126
  force?: boolean;
104
127
  }
105
128
 
106
- async function generateTextEmbeddings({ document, client, type, config }: ExecuteGenerateEmbeddingsParams) {
129
+ async function generateTextEmbeddings({ document, client, type, config }: ExecuteGenerateEmbeddingsParams, parts?: DocPart[],) {
107
130
  // if (!force && document.embeddings[type]?.etag === (document.text_etag ?? md5(document.text))) {
108
131
  // return { id: objectId, status: "skipped", message: "embeddings already generated" }
109
132
  // }
@@ -125,6 +148,8 @@ async function generateTextEmbeddings({ document, client, type, config }: Execut
125
148
 
126
149
  const { environment, model } = config;
127
150
 
151
+ const partDefinitions = parts ?? [];
152
+
128
153
  // Count tokens if not already done
129
154
  if (!document.tokens?.count && type === SupportedEmbeddingTypes.text) {
130
155
  log.debug('Updating token count for document: ' + document.id);
@@ -150,79 +175,64 @@ async function generateTextEmbeddings({ document, client, type, config }: Execut
150
175
  if (type === SupportedEmbeddingTypes.text && document.tokens?.count && document.tokens?.count > maxTokens) {
151
176
  log.info('Document too large, generating embeddings for parts');
152
177
 
153
- if (!document.parts || document.parts.length === 0) {
154
- return { id: document.id, status: "skipped", message: "no parts found" }
178
+
179
+ if (!document.text) {
180
+ return { id: document.id, status: "failed", message: "no text found" }
155
181
  }
156
182
 
157
- const docParts = await Promise.all(document.parts?.map(async (partId) => client.objects.retrieve(partId, "+text +embeddings +properties +tokens")));
158
- log.info(`Retrieved ${docParts.length} parts`)
183
+ if (!partDefinitions || partDefinitions.length === 0) {
184
+ log.info('No parts found for document, skipping embeddings generation');
185
+ return { id: document.id, status: "failed", message: "no parts found" }
186
+ }
159
187
 
160
- const generatePartEmbeddings = async (part: ContentObject<any>, i: number) => {
161
- try {
162
- log.info(`Generating embeddings for part ${part.id}`, { text_len: part.text?.length })
163
- if (!part.text) {
164
- return { id: part.id, number: i, result: null, status: "skipped", message: "no text found" }
165
- }
166
188
 
167
- if (part.tokens?.count && part.tokens.count > maxTokens) {
168
- log.info('Part too large, skipping embeddings generation for part', { part: part.id, tokens: part.tokens.count });
169
- return { id: part.id, number: i, result: null, message: "part too large" }
189
+ log.info('Generating embeddings for parts', { parts: partDefinitions, max_tokens: maxTokens });
190
+ const docParts = getContentParts(document.text, partDefinitions);
191
+
192
+
193
+ log.info(`Retrieved ${docParts.length} parts`)
194
+ const start = new Date().getTime();
195
+ const generatePartEmbeddings = async (partContent: string, i: number) => {
196
+ const localStart = new Date().getTime();
197
+ try {
198
+ log.info(`Generating embeddings for part ${i}`, { text_len: partContent.length })
199
+ if (!partContent) {
200
+ return { id: i, number: i, result: null, status: "skipped", message: "no text found" }
170
201
  }
171
202
 
172
- const e = await generateEmbeddingsFromStudio(part.text, environment, client, model).catch(e => {
173
- log.error('Error generating embeddings for part', { part: part.id, tokens: part.tokens, text_length: part.text?.length, error: e });
203
+ const e = await generateEmbeddingsFromStudio(partContent, environment, client, model).catch(e => {
204
+ log.error('Error generating embeddings for part ' + i, { text_length: partContent.length, error: e });
174
205
  return null;
175
206
  });
176
207
 
177
208
  if (!e || !e.values) {
178
- return { id: part.id, number: i, result: null, message: "no embeddings generated" }
209
+ return { id: i, number: i, result: null, message: "no embeddings generated" }
179
210
  }
180
211
 
181
- log.info(`Embeddings generated for part ${part.id}, updating object in the store.`)
182
- await client.objects.setEmbedding(part.id, SupportedEmbeddingTypes.text,
183
- {
184
- values: e.values,
185
- model: e.model,
186
- etag: part.text_etag
187
- }).catch(err => {
188
- log.info(`Error updating embeddings on part ${part.id}`);
189
- return { id: part.id, number: i, result: null, message: "error setting embeddings on part", error: err.message }
190
- })
191
-
192
- log.info('Generated embeddings for part: ' + part.id);
193
- return { id: part.id, number: i, result: e }
212
+ if (e.values.length === 0) {
213
+ return { id: i, number: i, result: null, message: "no embeddings generated" }
214
+ }
215
+ log.info(`Generated embeddings for part ${i}`, { len: e.values.length, duration: new Date().getTime() - localStart });
216
+
217
+ return { inumber: i, result: e }
194
218
  } catch (err: any) {
195
- log.info(`Error generating ${type} embeddings for part ${part.id} of ${document.id}`, { error: err });
196
- return { id: part.id, number: i, result: null, message: "error generating embeddings", error: err.message }
219
+ log.info(`Error generating ${type} embeddings for part ${i} of ${document.id}`, { error: err });
220
+ return { number: i, result: null, message: "error generating embeddings", error: err.message }
197
221
  }
198
222
  }
199
223
 
200
- const promises = docParts.map((p, i) => generatePartEmbeddings(p, i))
201
- const res = await Promise.all(promises);
202
- // let i = 0;
203
- // for (const p of docParts) {
204
- // log.info(`Processing part ${p.id}`)
205
- // const r = await generatePartEmbeddings(p, i++);
206
- // res.push(r)
207
- // }
208
-
209
-
210
- // Filter out parts without embeddings
211
- const validEmbeddings = res.filter(item => item.result !== null) as { id: string, number: number, result: EmbeddingsResult }[];
212
-
213
- // Compute the document-level embedding using TensorFlow for attention mechanism
214
- log.info('Computing document-level embedding using TF');
215
- const documentEmbedding = computeAttentionEmbedding(validEmbeddings.map(item => item.result.values));
216
-
217
- // Save the document-level embedding
224
+ const partEmbeddings = await Promise.all(docParts.map((part, i) => generatePartEmbeddings(part, i)));
225
+ const validPartEmbeddings = partEmbeddings.filter(e => e.result !== null).map(e => e.result);
226
+ const averagedEmbedding = computeAttentionEmbedding(validPartEmbeddings.map(e => e.values));
227
+ log.info(`Averaged embeddings for document ${document.id} in ${(new Date().getTime() - start) / 1000} seconds`, { len: averagedEmbedding.length, count: validPartEmbeddings.length, max_tokens: maxTokens });
218
228
  await client.objects.setEmbedding(document.id, type,
219
229
  {
220
- values: documentEmbedding,
221
- model: "attention",
230
+ values: averagedEmbedding,
231
+ model: validPartEmbeddings[0].model,
222
232
  etag: document.text_etag
223
233
  }
224
234
  );
225
- return { id: document.id, status: "completed", parts: docParts.map(i => i.id), len: documentEmbedding.length, part_embeddings: res.map(r => { return { id: r.id, status: r.status, error: r.error, message: r.message } }) }
235
+ log.info(`Object ${document.id} embedding set`, { type, len: averagedEmbedding.length });
226
236
 
227
237
  } else {
228
238
  log.info(`Generating ${type} embeddings for document`);
@@ -311,35 +321,37 @@ async function generateEmbeddingsFromStudio(text: string, env: string, client: V
311
321
 
312
322
  }
313
323
 
314
- function computeAttentionEmbedding(embeddingsArray: number[][], axis: number = 0) {
315
- if (embeddingsArray.length === 0) return [];
316
- log.info('Computing attention embedding for', { embeddingsArrays: embeddingsArray.map(a => a.length) });
324
+ //Simplified attention mechanism
325
+ // This is a naive implementation and should be replaced with a more sophisticated
326
+ // using tensorflow in a specific package
327
+ function computeAttentionEmbedding(chunkEmbeddings: number[][]): number[] {
328
+ if (chunkEmbeddings.length === 0) return [];
329
+
317
330
  const start = new Date().getTime();
318
331
 
319
- // Convert embeddings array to TensorFlow tensor
320
- const embeddingsTensor = tf.tensor(embeddingsArray);
332
+ // Generate random attention weights
333
+ const attentionWeights = chunkEmbeddings.map(() => Math.random());
321
334
 
322
- // Initialize trainable attention weights
323
- const attentionWeights = tf.variable(tf.randomNormal([embeddingsArray.length]), true);
335
+ // Apply softmax to get attention scores
336
+ const expWeights = attentionWeights.map(w => Math.exp(w));
337
+ const sumExpWeights = expWeights.reduce((sum, val) => sum + val, 0);
338
+ const attentionScores = expWeights.map(w => w / sumExpWeights);
324
339
 
325
- // Compute attention scoresje sui
326
- const attentionScores = tf.softmax(attentionWeights);
340
+ // Get embedding dimension
341
+ const embeddingDim = chunkEmbeddings[0].length;
327
342
 
328
- // Compute weighted sum of embeddings
329
- const weightedEmbeddings = tf.mul(embeddingsTensor.transpose(), attentionScores).transpose();
330
- const documentEmbeddingTensor = tf.sum(weightedEmbeddings, axis);
343
+ // Initialize document embedding
344
+ const documentEmbedding = new Array(embeddingDim).fill(0);
331
345
 
332
- // Convert the result back to a JavaScript array
333
- const documentEmbedding = documentEmbeddingTensor.arraySync() as number[];
334
- const duration = (new Date().getTime() - start);
335
- log.info(`Computed attention embeddings in ${duration}ms - array size: ${documentEmbedding.length}`, { length: documentEmbedding.length });
346
+ // Weighted sum of embeddings
347
+ for (let i = 0; i < chunkEmbeddings.length; i++) {
348
+ for (let j = 0; j < embeddingDim; j++) {
349
+ documentEmbedding[j] += chunkEmbeddings[i][j] * attentionScores[i];
350
+ }
351
+ }
336
352
 
337
- // Clean up tensors
338
- embeddingsTensor.dispose();
339
- attentionWeights.dispose();
340
- attentionScores.dispose();
341
- weightedEmbeddings.dispose();
342
- documentEmbeddingTensor.dispose();
353
+ const duration = new Date().getTime() - start;
354
+ console.log(`Computed document embedding in ${duration}ms for ${chunkEmbeddings.length} chunks`);
343
355
 
344
356
  return documentEmbedding;
345
- }
357
+ }
@@ -1,31 +1,27 @@
1
- import { DSLActivityExecutionPayload, DSLActivitySpec, RenditionProperties } from "@vertesia/common";
2
1
  import { log } from "@temporalio/activity";
2
+ import { NodeStreamSource } from "@vertesia/client/node";
3
+ import { DSLActivityExecutionPayload, DSLActivitySpec, RenditionProperties } from "@vertesia/common";
3
4
  import fs from 'fs';
4
- import sharp, { FormatEnum } from "sharp";
5
+ import ffmpeg from 'fluent-ffmpeg';
6
+ import path from 'path';
7
+ import os from 'os';
5
8
  import { imageResizer } from "../conversion/image.js";
6
- import { pdfToImages } from "../conversion/mutool.js";
7
9
  import { setupActivity } from "../dsl/setup/ActivityContext.js";
8
10
  import { NoDocumentFound, WorkflowParamNotFound } from "../errors.js";
9
- import { fetchBlobAsBuffer, saveBlobToTempFile } from "../utils/blobs.js";
10
- import { NodeStreamSource } from "../utils/memory.js";
11
+ import { saveBlobToTempFile } from "../utils/blobs.js";
12
+
11
13
  interface GenerateImageRenditionParams {
12
14
  max_hw: number; //maximum size of the longuest side of the image
13
- format: keyof FormatEnum; //format of the output image
14
- multi_page?: boolean; //if true, generate a multi-page rendition
15
+ format: string; //format of the output image
15
16
  }
16
17
 
17
-
18
18
  export interface GenerateImageRendition extends DSLActivitySpec<GenerateImageRenditionParams> {
19
-
20
- name: 'generateImageRendition';
21
-
19
+ name: "generateImageRendition";
22
20
  }
23
21
 
24
-
25
22
  export async function generateImageRendition(payload: DSLActivityExecutionPayload<GenerateImageRenditionParams>) {
26
23
  const { client, objectId, params } = await setupActivity<GenerateImageRenditionParams>(payload);
27
24
 
28
- const supportedNonImageInputTypes = ['application/pdf']
29
25
  const inputObject = await client.objects.retrieve(objectId).catch((err) => {
30
26
  log.error(`Failed to retrieve document ${objectId}`, err);
31
27
  if (err.response?.status === 404) {
@@ -33,7 +29,7 @@ export async function generateImageRendition(payload: DSLActivityExecutionPayloa
33
29
  }
34
30
  throw err;
35
31
  });
36
- const renditionType = await client.types.getTypeByName('Rendition');
32
+ const renditionType = await client.types.getTypeByName("Rendition");
37
33
 
38
34
  if (!params.format) {
39
35
  log.error(`Format not found`);
@@ -50,85 +46,136 @@ export async function generateImageRendition(payload: DSLActivityExecutionPayloa
50
46
  throw new NoDocumentFound(`Document ${objectId} has no source`, [objectId]);
51
47
  }
52
48
 
53
- if (!inputObject.content.type || (!inputObject.content.type?.startsWith('image/') && !supportedNonImageInputTypes.includes(inputObject.content.type))) {
54
- log.error(`Document ${objectId} is not an image`);
55
- throw new NoDocumentFound(`Document ${objectId} is not an image or pdf: ${inputObject.content.type}`, [objectId]);
49
+ if (!inputObject.content.type || (!inputObject.content.type?.startsWith("image/") && !inputObject.content.type?.startsWith("video/"))) {
50
+ log.error(`Document ${objectId} is not an image or a video: ${inputObject.content.type}`);
51
+ throw new NoDocumentFound(`Document ${objectId} is not an image or a video: ${inputObject.content.type}`, [objectId]);
56
52
  }
57
53
 
58
54
  //array of rendition files to upload
59
55
  let renditionPages: string[] = [];
60
56
 
61
- //if PDF, convert to pages
62
- if (inputObject.content.type === 'application/pdf') {
63
- const pdfBuffer = await fetchBlobAsBuffer(client, inputObject.content.source);
64
- const pages = await pdfToImages(pdfBuffer);
65
- if (!pages.length) {
66
- log.error(`Failed to convert pdf to image`);
67
- throw new Error(`Failed to convert pdf to image`);
57
+ if (inputObject.content.type.startsWith('image/')) {
58
+ const imageFile = await saveBlobToTempFile(client, inputObject.content.source);
59
+ log.info(`Image ${objectId} copied to ${imageFile}`);
60
+ renditionPages.push(imageFile);
61
+ } else if (inputObject.content.type.startsWith('video/')) {
62
+ const videoFile = await saveBlobToTempFile(client, inputObject.content.source);
63
+ const tempOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'video-rendition-'));
64
+ const thumbnailPath = path.join(tempOutputDir, 'thumbnail.png');
65
+
66
+ try {
67
+ // Extract a frame at 10% of the video duration
68
+ await new Promise<void>((resolve, reject) => {
69
+ ffmpeg.ffprobe(videoFile, (err, metadata) => {
70
+ if (err) {
71
+ log.error(`Failed to probe video metadata: ${err.message}`);
72
+ return reject(err);
73
+ }
74
+
75
+ const duration = metadata.format.duration || 0;
76
+ const timestamp = Math.max(0.1 * duration, 1);
77
+
78
+ ffmpeg(videoFile)
79
+ .screenshots({
80
+ timestamps: [timestamp],
81
+ filename: 'thumbnail.png',
82
+ folder: tempOutputDir,
83
+ size: `${params.max_hw}x?`
84
+ })
85
+ .on('end', () => {
86
+ log.info(`Video frame extraction complete for ${objectId}`);
87
+ resolve();
88
+ })
89
+ .on('error', (err) => {
90
+ log.error(`Error extracting frame from video: ${err.message}`);
91
+ reject(err);
92
+ });
93
+ });
94
+ });
95
+
96
+ if (fs.existsSync(thumbnailPath)) {
97
+ renditionPages.push(thumbnailPath);
98
+ } else {
99
+ throw new Error(`Failed to generate thumbnail for video ${objectId}`);
100
+ }
101
+ } catch (error) {
102
+ log.error(`Error generating image rendition for video: ${error instanceof Error ? error.message : 'Unknown error'}`);
103
+ throw new Error(`Failed to generate image rendition for video: ${objectId}`);
68
104
  }
69
- renditionPages = [...pages];
70
- } else if (inputObject.content.type.startsWith('image/')) {
71
- const tmpFile = await saveBlobToTempFile(client, inputObject.content.source);
72
- const filestats = fs.statSync(tmpFile);
73
- log.info(`Image ${objectId} copied to ${tmpFile}`, { filestats });
74
- renditionPages.push(tmpFile);
75
105
  }
76
106
 
77
107
  //generate rendition name, pass an index for multi parts
78
108
  const getRenditionName = (index: number = 0) => {
79
109
  const name = `renditions/${objectId}/${params.max_hw}/${index}.${params.format}`;
80
110
  return name;
81
- }
111
+ };
82
112
 
83
113
  if (!renditionPages || !renditionPages.length) {
84
114
  log.error(`Failed to generate rendition for ${objectId}`);
85
115
  throw new Error(`Failed to generate rendition for ${objectId}`);
86
116
  }
87
117
 
88
- log.info(`Uploading rendition for ${objectId} with ${renditionPages.length} pages (max_hw: ${params.max_hw}, format: ${params.format})`, { renditionPages });
118
+ log.info(
119
+ `Uploading rendition for ${objectId} with ${renditionPages.length} pages (max_hw: ${params.max_hw}, format: ${params.format})`,
120
+ { renditionPages },
121
+ );
89
122
  const uploads = renditionPages.map(async (page, i) => {
90
123
  const pageId = getRenditionName(i);
91
- const resized = sharp(page).pipe(imageResizer(params.max_hw, params.format));
92
-
93
- const source = new NodeStreamSource(
94
- resized,
95
- pageId.replace('renditions/', '').replace('/', '_'),
96
- 'image/' + params.format,
97
- pageId,
98
- )
99
-
100
- log.info(`Uploading rendition for ${objectId} page ${i} with max_hw: ${params.max_hw} and format: ${params.format}`);
101
- return client.objects.upload(source).catch((err) => {
102
- log.error(`Failed to upload rendition for ${objectId} page ${i}`, err);
124
+ let resizedImagePath = null;
125
+
126
+ try {
127
+ // Resize the image using ImageMagick
128
+ resizedImagePath = await imageResizer(page, params.max_hw, params.format);
129
+
130
+ // Create a read stream from the resized image file
131
+ const fileStream = fs.createReadStream(resizedImagePath);
132
+
133
+ const source = new NodeStreamSource(
134
+ fileStream,
135
+ pageId.split("/").pop() ?? "0." + params.format,
136
+ "image/" + params.format,
137
+ pageId,
138
+ );
139
+
140
+ log.info(
141
+ `Uploading rendition for ${objectId} page ${i} with max_hw: ${params.max_hw} and format: ${params.format}`,
142
+ );
143
+
144
+ const result = await client.objects.upload(source).catch((err) => {
145
+ log.error(`Failed to upload rendition for ${objectId} page ${i}`, { error: err });
146
+ return Promise.resolve(null);
147
+ });
148
+
149
+ return result;
150
+ } catch (error) {
151
+ log.error(`Failed to process rendition for ${objectId} page ${i}`, { error });
103
152
  return Promise.resolve(null);
104
- });
153
+ }
105
154
  });
106
155
 
107
156
  const uploaded = await Promise.all(uploads);
108
157
  if (!uploaded || !uploaded.length || !uploaded[0]) {
109
158
  log.error(`Failed to upload rendition for ${objectId}`);
110
- throw new Error(`Failed to upload rendition for ${objectId}`);
159
+ throw new Error(`Failed to upload rendition for ${objectId} - upload object is empty`);
111
160
  }
112
161
 
113
-
114
- log.info(`Creating rendition for ${objectId} with max_hw: ${params.max_hw} and format: ${params.format}`, { uploaded });
162
+ log.info(`Creating rendition for ${objectId} with max_hw: ${params.max_hw} and format: ${params.format}`, {
163
+ uploaded,
164
+ });
115
165
  const rendition = await client.objects.create({
116
166
  name: inputObject.name + ` [Rendition ${params.max_hw}]`,
117
167
  type: renditionType.id,
118
168
  parent: inputObject.id,
119
169
  content: uploaded[0],
120
170
  properties: {
121
- mime_type: 'image/' + params.format,
171
+ mime_type: "image/" + params.format,
122
172
  source_etag: inputObject.content.source,
123
173
  height: params.max_hw,
124
174
  width: params.max_hw,
125
- multipart: uploaded.length > 1,
126
- total_parts: uploaded.length
127
- } satisfies RenditionProperties
175
+ } satisfies RenditionProperties,
128
176
  });
129
177
 
130
178
  log.info(`Rendition ${rendition.id} created for ${objectId}`, { rendition });
131
179
 
132
180
  return { id: rendition.id, format: params.format, status: "success" };
133
-
134
181
  }
@@ -1,5 +1,5 @@
1
1
  import { log } from "@temporalio/activity";
2
- import { CreateContentObjectTypePayload, DSLActivityExecutionPayload, DSLActivitySpec } from "@vertesia/common";
2
+ import { ContentObjectTypeItem, CreateContentObjectTypePayload, DSLActivityExecutionPayload, DSLActivitySpec } from "@vertesia/common";
3
3
  import { ActivityContext, setupActivity } from "../dsl/setup/ActivityContext.js";
4
4
  import { TruncateSpec, truncByMaxTokens } from "../utils/tokens.js";
5
5
  import { InteractionExecutionParams, executeInteractionFromActivity } from "./executeInteraction.js";
@@ -56,12 +56,7 @@ export async function generateOrAssignContentType(payload: DSLActivityExecutionP
56
56
  const types = await client.types.list();
57
57
 
58
58
  //make a list of all existing types, and add hints if any
59
- const existing_types = types.map(t => t.name).filter(n => !["DocumentPart", "Rendition"].includes(n));
60
- if (params.typesHint) {
61
- const newHints = params.typesHint.filter((t: string) => !existing_types.includes(t));
62
- existing_types.push(...newHints);
63
- }
64
-
59
+ const existing_types = types.filter(t => !["DocumentPart", "Rendition"].includes(t.name));
65
60
  const content = object.text ? truncByMaxTokens(object.text, params.truncate || 4000) : undefined;
66
61
 
67
62
  const getImage = async () => {
@@ -82,7 +77,7 @@ export async function generateOrAssignContentType(payload: DSLActivityExecutionP
82
77
 
83
78
  const fileRef = await getImage();
84
79
 
85
- log.info("Execute SelectDocumentType interaction on content with \nexisting types: " + existing_types.join(","));
80
+ log.info("Execute SelectDocumentType interaction on content with \nexisting types: " + existing_types.map(t => t.name).join(","));
86
81
 
87
82
  const res = await executeInteractionFromActivity(client, interactionName, params, {
88
83
  existing_types, content, image: fileRef
@@ -98,7 +93,6 @@ export async function generateOrAssignContentType(payload: DSLActivityExecutionP
98
93
  if (!selectedType) {
99
94
  log.warn("Document type not idenfified: starting type generation");
100
95
  const newType = await generateNewType(context, existing_types, content, fileRef);
101
-
102
96
  selectedType = { id: newType.id, name: newType.name };
103
97
  }
104
98
 
@@ -119,14 +113,14 @@ export async function generateOrAssignContentType(payload: DSLActivityExecutionP
119
113
  };
120
114
  }
121
115
 
122
- async function generateNewType(context: ActivityContext<GenerateOrAssignContentTypeParams>, existing_types: string[], content?: string, fileRef?: string) {
116
+ async function generateNewType(context: ActivityContext<GenerateOrAssignContentTypeParams>, existing_types: ContentObjectTypeItem[], content?: string, fileRef?: string) {
123
117
  const { client, params } = context;
124
118
 
125
119
  const project = await context.fetchProject();
126
120
  const interactionName = params.interactionNames?.generateMetadataModel ?? INT_GENERATE_METADATA_MODEL;
127
121
 
128
122
  const genTypeRes = await executeInteractionFromActivity(client, interactionName, params, {
129
- existing_types: existing_types,
123
+ existing_types: existing_types.map(t => t.name),
130
124
  content: content,
131
125
  human_context: project?.configuration?.human_context ?? undefined,
132
126
  image: fileRef ? fileRef : undefined
@@ -1,9 +1,9 @@
1
- import { DSLActivityExecutionPayload, DSLActivitySpec } from "@vertesia/common";
2
1
  import { log } from "@temporalio/activity";
2
+ import { DSLActivityExecutionPayload, DSLActivitySpec } from "@vertesia/common";
3
3
  import { setupActivity } from "../dsl/setup/ActivityContext.js";
4
4
  import { WorkflowParamNotFound } from "../errors.js";
5
5
 
6
- interface NotifyWebhookParams {
6
+ export interface NotifyWebhookParams {
7
7
  target_url: string; //URL to send the notification to
8
8
  method: 'GET' | 'POST'; //HTTP method to use
9
9
  payload: Record<string, any>; //payload to send (if POST then as JSON body, if GET then as query string)