ocr-upload.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /**
  2. * @node ocr-upload
  3. * @name OCR Upload
  4. * @category ocr
  5. * @version 1.3.0
  6. * @description Upload a file for OCR processing via pdftoimageapi
  7. * @icon file-scan
  8. */
  9. const configSchema = {
  10. type: 'object',
  11. properties: {
  12. fileSource: {
  13. type: 'string',
  14. title: 'File Source',
  15. description: 'Path to file data (e.g., {{item.attachments[0]}} for loop iterations, or {{data.attachments[0]}} for direct input). Leave empty to auto-detect from input.'
  16. },
  17. baseUrl: {
  18. type: 'string',
  19. title: 'API Base URL',
  20. default: 'http://localhost:3000',
  21. description: 'Base URL of the OCR API'
  22. },
  23. credentialId: {
  24. type: 'string',
  25. title: 'Credential',
  26. description: 'Bearer or API Key credential for authentication',
  27. dynamicOptions: {
  28. source: 'credentials',
  29. filter: { type: 'api_key' }
  30. }
  31. },
  32. processingMode: {
  33. type: 'string',
  34. title: 'Processing Mode',
  35. enum: ['auto', 'single', 'batch', 'smart'],
  36. default: 'auto',
  37. description: 'How to process the uploaded file'
  38. },
  39. batchSize: {
  40. type: 'number',
  41. title: 'Batch Size',
  42. default: 5,
  43. description: 'Number of pages per batch (for batch mode)'
  44. },
  45. targetWidth: {
  46. type: 'number',
  47. title: 'Target Width',
  48. description: 'Target width in pixels for image conversion'
  49. },
  50. groupId: {
  51. type: 'string',
  52. title: 'Group ID',
  53. description: 'Optional group ID to associate jobs'
  54. }
  55. },
  56. required: ['baseUrl']
  57. };
  58. const inputSchema = {
  59. type: 'object',
  60. properties: {
  61. file: {
  62. type: 'object',
  63. description: 'Binary file object with type, data (base64), mimeType, filename, size'
  64. },
  65. storage: {
  66. type: 'object',
  67. description: 'Storage reference to retrieve file from database',
  68. properties: {
  69. collection: { type: 'string' },
  70. id: { type: 'string' }
  71. }
  72. }
  73. }
  74. };
  75. const outputSchema = {
  76. type: 'object',
  77. properties: {
  78. job: {
  79. type: 'object',
  80. properties: {
  81. id: { type: 'string' },
  82. status: { type: 'string' },
  83. filename: { type: 'string' },
  84. created_at: { type: 'string' }
  85. }
  86. }
  87. }
  88. };
  89. const outputs = [
  90. { name: 'main', displayName: 'Job Created', type: 'object', color: '#10b981' }
  91. ];
  92. function isBinaryInput(input) {
  93. // Binary input can have either data (base64) or filePath (disk reference)
  94. return input && input.type === 'binary' && (typeof input.data === 'string' || typeof input.filePath === 'string');
  95. }
  96. function resolveBinaryData(file) {
  97. // If file has filePath, read from disk
  98. if (file.filePath && !file.data) {
  99. const readResult = smartbotic.fs.readFile(file.filePath, 'base64');
  100. if (readResult.success) {
  101. file.data = readResult.data;
  102. smartbotic.log.info(`Loaded binary from disk: ${file.filePath}`);
  103. } else {
  104. throw new Error(`Failed to read file from disk: ${file.filePath} - ${readResult.error}`);
  105. }
  106. }
  107. return file;
  108. }
  109. module.exports = {
  110. configSchema,
  111. inputSchema,
  112. outputSchema,
  113. outputs,
  114. async execute(config, input, context) {
  115. const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
  116. let file = null;
  117. // First priority: use configured fileSource if provided
  118. // The expression (e.g., {{item.attachments[0]}}) is evaluated by the workflow engine,
  119. // so config.fileSource contains the actual file object
  120. if (config.fileSource && typeof config.fileSource === 'object') {
  121. if (isBinaryInput(config.fileSource)) {
  122. file = config.fileSource;
  123. smartbotic.log.info('OCR Upload: Using configured file source');
  124. } else if (Array.isArray(config.fileSource) && config.fileSource.length > 0) {
  125. // If fileSource resolved to an array (e.g., attachments array), use first item
  126. file = config.fileSource[0];
  127. smartbotic.log.info(`OCR Upload: Using first item from configured file source array: ${file.filename}`);
  128. }
  129. }
  130. // Fallback: auto-detect from various input formats
  131. if (!file) {
  132. if (isBinaryInput(input.file)) {
  133. file = input.file;
  134. } else if (isBinaryInput(input)) {
  135. file = input;
  136. } else if (input.data && isBinaryInput(input.data)) {
  137. // Handle data wrapper from upstream nodes
  138. file = input.data;
  139. } else if (input.attachments && Array.isArray(input.attachments) && input.attachments.length > 0) {
  140. // Handle output from IMAP Extract Attachments - use first attachment
  141. file = input.attachments[0];
  142. smartbotic.log.info(`Using first attachment from array: ${file.filename}`);
  143. } else if (input.storage && input.storage.collection && input.storage.id) {
  144. // Load from storage database (metadata with filePath)
  145. const storageResult = smartbotic.storage.get(input.storage.collection, input.storage.id);
  146. if (!storageResult.found) {
  147. throw new Error(`File not found in storage: ${input.storage.collection}/${input.storage.id}`);
  148. }
  149. const doc = storageResult.document;
  150. file = {
  151. type: 'binary',
  152. filePath: doc.filePath, // Reference to disk location
  153. mimeType: doc.mimeType,
  154. filename: doc.filename,
  155. size: doc.size
  156. };
  157. }
  158. }
  159. if (!file) {
  160. // Log available input keys for debugging
  161. const inputKeys = Object.keys(input || {}).join(', ');
  162. smartbotic.log.warn(`OCR Upload: No binary file found. Input keys: ${inputKeys}`);
  163. throw new Error('No file provided. Configure the "File Source" field with an expression like {{item.attachments[0]}} for loop iterations, or connect to IMAP Extract Attachments.');
  164. }
  165. // Resolve file data from disk if needed
  166. file = resolveBinaryData(file);
  167. const headers = {};
  168. if (config.credentialId) {
  169. const auth = smartbotic.credentials.get(config.credentialId);
  170. if (!auth.success) {
  171. throw new Error(`Failed to load credential: ${auth.error}`);
  172. }
  173. headers[auth.headerName] = auth.headerValue;
  174. }
  175. smartbotic.log.info(`OCR Upload: ${file.filename} (${file.size} bytes, type: ${file.mimeType})`);
  176. // Build form data for additional parameters
  177. const formData = {};
  178. if (config.processingMode && config.processingMode !== 'auto') {
  179. formData.processing_mode = config.processingMode;
  180. }
  181. if (config.batchSize) {
  182. formData.batch_size = String(config.batchSize);
  183. }
  184. if (config.targetWidth) {
  185. formData.target_width = String(config.targetWidth);
  186. }
  187. if (config.groupId) {
  188. formData.group_id = config.groupId;
  189. }
  190. // Use the new multipart file upload support with proper binary encoding
  191. const response = smartbotic.http.request({
  192. method: 'POST',
  193. url: `${baseUrl}/api/ocr/upload`,
  194. headers: headers,
  195. files: [{
  196. name: 'file',
  197. filename: file.filename,
  198. mimeType: file.mimeType,
  199. data: file.data // base64 encoded - will be decoded to binary by http.request
  200. }],
  201. formData: formData,
  202. timeout: 120000
  203. });
  204. if (response.status < 200 || response.status >= 300) {
  205. const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
  206. if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`);
  207. }
  208. const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
  209. return {
  210. job: data.job || data
  211. };
  212. }
  213. };