ocr-upload.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /**
  2. * @node ocr-upload
  3. * @name OCR Upload
  4. * @category ocr
  5. * @version 1.0.1
  6. * @description Upload a file for OCR processing via pdftoimageapi
  7. * @icon file-scan
  8. */
  9. const configSchema = {
  10. type: 'object',
  11. properties: {
  12. baseUrl: {
  13. type: 'string',
  14. title: 'API Base URL',
  15. default: 'http://localhost:3000',
  16. description: 'Base URL of the OCR API'
  17. },
  18. credentialId: {
  19. type: 'string',
  20. title: 'Credential',
  21. description: 'Bearer or API Key credential for authentication',
  22. dynamicOptions: {
  23. source: 'credentials',
  24. filter: { type: 'api_key' }
  25. }
  26. },
  27. processingMode: {
  28. type: 'string',
  29. title: 'Processing Mode',
  30. enum: ['auto', 'single', 'batch', 'smart'],
  31. default: 'auto',
  32. description: 'How to process the uploaded file'
  33. },
  34. batchSize: {
  35. type: 'number',
  36. title: 'Batch Size',
  37. default: 5,
  38. description: 'Number of pages per batch (for batch mode)'
  39. },
  40. targetWidth: {
  41. type: 'number',
  42. title: 'Target Width',
  43. description: 'Target width in pixels for image conversion'
  44. },
  45. groupId: {
  46. type: 'string',
  47. title: 'Group ID',
  48. description: 'Optional group ID to associate jobs'
  49. }
  50. },
  51. required: ['baseUrl']
  52. };
  53. const inputSchema = {
  54. type: 'object',
  55. properties: {
  56. file: {
  57. type: 'object',
  58. description: 'Binary file object with type, data (base64), mimeType, filename, size'
  59. },
  60. storage: {
  61. type: 'object',
  62. description: 'Storage reference to retrieve file from database',
  63. properties: {
  64. collection: { type: 'string' },
  65. id: { type: 'string' }
  66. }
  67. }
  68. }
  69. };
  70. const outputSchema = {
  71. type: 'object',
  72. properties: {
  73. job: {
  74. type: 'object',
  75. properties: {
  76. id: { type: 'string' },
  77. status: { type: 'string' },
  78. filename: { type: 'string' },
  79. created_at: { type: 'string' }
  80. }
  81. }
  82. }
  83. };
  84. const outputs = [
  85. { name: 'main', displayName: 'Job Created', type: 'object', color: '#10b981' }
  86. ];
  87. function isBinaryInput(input) {
  88. return input && input.type === 'binary' && typeof input.data === 'string';
  89. }
  90. function generateBoundary() {
  91. return '----SmartBoticBoundary' + smartbotic.utils.uuid().replace(/-/g, '');
  92. }
  93. function buildMultipartBody(boundary, file, options) {
  94. const crlf = '\r\n';
  95. let body = '';
  96. body += `--${boundary}${crlf}`;
  97. body += `Content-Disposition: form-data; name="file"; filename="${file.filename}"${crlf}`;
  98. body += `Content-Type: ${file.mimeType}${crlf}`;
  99. body += `Content-Transfer-Encoding: base64${crlf}`;
  100. body += crlf;
  101. body += file.data;
  102. body += crlf;
  103. if (options.processingMode && options.processingMode !== 'auto') {
  104. body += `--${boundary}${crlf}`;
  105. body += `Content-Disposition: form-data; name="processing_mode"${crlf}`;
  106. body += crlf;
  107. body += options.processingMode;
  108. body += crlf;
  109. }
  110. if (options.batchSize) {
  111. body += `--${boundary}${crlf}`;
  112. body += `Content-Disposition: form-data; name="batch_size"${crlf}`;
  113. body += crlf;
  114. body += String(options.batchSize);
  115. body += crlf;
  116. }
  117. if (options.targetWidth) {
  118. body += `--${boundary}${crlf}`;
  119. body += `Content-Disposition: form-data; name="target_width"${crlf}`;
  120. body += crlf;
  121. body += String(options.targetWidth);
  122. body += crlf;
  123. }
  124. if (options.groupId) {
  125. body += `--${boundary}${crlf}`;
  126. body += `Content-Disposition: form-data; name="group_id"${crlf}`;
  127. body += crlf;
  128. body += options.groupId;
  129. body += crlf;
  130. }
  131. body += `--${boundary}--${crlf}`;
  132. return body;
  133. }
  134. module.exports = {
  135. configSchema,
  136. inputSchema,
  137. outputSchema,
  138. outputs,
  139. async execute(config, input, context) {
  140. const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
  141. let file = null;
  142. if (isBinaryInput(input.file)) {
  143. file = input.file;
  144. } else if (isBinaryInput(input)) {
  145. file = input;
  146. } else if (input.storage && input.storage.collection && input.storage.id) {
  147. const storageResult = smartbotic.storage.get(input.storage.collection, input.storage.id);
  148. if (!storageResult.found) {
  149. throw new Error(`File not found in storage: ${input.storage.collection}/${input.storage.id}`);
  150. }
  151. const doc = storageResult.document;
  152. file = {
  153. type: 'binary',
  154. data: doc.data,
  155. mimeType: doc.mimeType,
  156. filename: doc.filename,
  157. size: doc.size
  158. };
  159. }
  160. if (!file) {
  161. throw new Error('No file provided. Provide either a binary file object or a storage reference.');
  162. }
  163. const headers = {};
  164. if (config.credentialId) {
  165. const auth = smartbotic.credentials.get(config.credentialId);
  166. if (!auth.success) {
  167. throw new Error(`Failed to load credential: ${auth.error}`);
  168. }
  169. headers[auth.headerName] = auth.headerValue;
  170. }
  171. const boundary = generateBoundary();
  172. headers['Content-Type'] = `multipart/form-data; boundary=${boundary}`;
  173. const body = buildMultipartBody(boundary, file, {
  174. processingMode: config.processingMode,
  175. batchSize: config.batchSize,
  176. targetWidth: config.targetWidth,
  177. groupId: config.groupId
  178. });
  179. smartbotic.log.info(`OCR Upload: ${file.filename} (${file.size} bytes)`);
  180. const response = smartbotic.http.request({
  181. method: 'POST',
  182. url: `${baseUrl}/api/ocr/upload`,
  183. headers: headers,
  184. body: body,
  185. timeout: 120000
  186. });
  187. if (response.status < 200 || response.status >= 300) {
  188. const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
  189. 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}`);
  190. }
  191. const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
  192. return {
  193. job: data.job || data
  194. };
  195. }
  196. };