orator-conversion 1.0.3 → 1.0.5

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/debug/Harness.js CHANGED
@@ -1,10 +1,48 @@
1
1
  const libFable = require('fable');
2
+ const libPath = require('path');
3
+
4
+ // Parse command-line flags before creating Fable so LogStreams can be set
5
+ let tmpUltravisorURL = null;
6
+ let tmpLogFilePath = null;
7
+ for (let i = 2; i < process.argv.length; i++)
8
+ {
9
+ if ((process.argv[i] === '--ultravisor' || process.argv[i] === '-u'))
10
+ {
11
+ if (process.argv[i + 1] && !process.argv[i + 1].startsWith('-'))
12
+ {
13
+ tmpUltravisorURL = process.argv[++i];
14
+ }
15
+ else
16
+ {
17
+ tmpUltravisorURL = 'http://localhost:54321';
18
+ }
19
+ }
20
+ if (process.argv[i] === '--logfile' || process.argv[i] === '-l')
21
+ {
22
+ if (process.argv[i + 1] && !process.argv[i + 1].startsWith('-'))
23
+ {
24
+ tmpLogFilePath = libPath.resolve(process.argv[++i]);
25
+ }
26
+ else
27
+ {
28
+ tmpLogFilePath = libPath.resolve(`orator-conversion-${new Date().toISOString().replace(/[:.]/g, '-')}.log`);
29
+ }
30
+ }
31
+ }
32
+
33
+ let tmpLogStreams = [{ level: 'trace', streamtype: 'process.stdout' }];
34
+ if (tmpLogFilePath)
35
+ {
36
+ tmpLogStreams.push({ loggertype: 'simpleflatfile', level: 'trace', path: tmpLogFilePath });
37
+ console.log(`[Orator-Conversion] Logging to file: ${tmpLogFilePath}`);
38
+ }
2
39
 
3
40
  const defaultFableSettings = (
4
41
  {
5
42
  Product: 'Orator-FileTranslation',
6
43
  ProductVersion: '1.0.0',
7
- APIServerPort: 8765
44
+ APIServerPort: 8765,
45
+ LogStreams: tmpLogStreams
8
46
  });
9
47
 
10
48
  let _Fable = new libFable(defaultFableSettings);
@@ -32,6 +70,30 @@ tmpAnticipate.anticipate(
32
70
 
33
71
  tmpAnticipate.anticipate(_Orator.startService.bind(_Orator));
34
72
 
73
+ // If an Ultravisor URL was provided, connect as a beacon
74
+ if (tmpUltravisorURL)
75
+ {
76
+ tmpAnticipate.anticipate(
77
+ (fNext) =>
78
+ {
79
+ _Fable.OratorFileTranslation.connectBeacon(
80
+ {
81
+ ServerURL: tmpUltravisorURL,
82
+ Name: 'orator-conversion',
83
+ BindAddresses: [{ IP: '127.0.0.1', Port: 8765, Protocol: 'http' }]
84
+ },
85
+ (pError) =>
86
+ {
87
+ if (pError)
88
+ {
89
+ _Fable.log.warn(`Beacon connection failed (server may not be running): ${pError.message}`);
90
+ _Fable.log.warn('HTTP endpoints are still available. Beacon will not be active.');
91
+ }
92
+ return fNext();
93
+ });
94
+ });
95
+ }
96
+
35
97
  tmpAnticipate.wait(
36
98
  (pError) =>
37
99
  {
@@ -45,4 +107,27 @@ tmpAnticipate.wait(
45
107
  _Fable.log.info(' POST http://127.0.0.1:8765/conversion/1.0/image/png-to-jpg');
46
108
  _Fable.log.info(' POST http://127.0.0.1:8765/conversion/1.0/pdf-to-page-png/:Page');
47
109
  _Fable.log.info(' POST http://127.0.0.1:8765/conversion/1.0/pdf-to-page-jpg/:Page');
110
+
111
+ if (tmpUltravisorURL)
112
+ {
113
+ _Fable.log.info('');
114
+ _Fable.log.info(`Beacon: connected to Ultravisor at ${tmpUltravisorURL}`);
115
+ }
48
116
  });
117
+
118
+ // Graceful shutdown
119
+ process.on('SIGINT', () =>
120
+ {
121
+ console.log('\n[Orator-Conversion] Shutting down...');
122
+ if (_Fable.OratorFileTranslation._BeaconService)
123
+ {
124
+ _Fable.OratorFileTranslation.disconnectBeacon(() =>
125
+ {
126
+ process.exit(0);
127
+ });
128
+ }
129
+ else
130
+ {
131
+ process.exit(0);
132
+ }
133
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orator-conversion",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "File format conversion endpoints for Orator service servers.",
5
5
  "main": "source/Orator-File-Translation.js",
6
6
  "scripts": {
@@ -42,7 +42,9 @@
42
42
  "homepage": "https://github.com/stevenvelozo/orator-conversion#readme",
43
43
  "dependencies": {
44
44
  "fable-serviceproviderbase": "^3.0.19",
45
- "sharp": "^0.34.5"
45
+ "sharp": "^0.34.5",
46
+ "ultravisor-beacon": "^0.0.5",
47
+ "ws": "^8.20.0"
46
48
  },
47
49
  "devDependencies": {
48
50
  "fable": "^3.1.67",
@@ -94,9 +94,9 @@ class ConversionCore
94
94
  }
95
95
 
96
96
  /**
97
- * Resize an image buffer with Sharp.
97
+ * Resize an image with Sharp.
98
98
  *
99
- * @param {Buffer} pInputBuffer - The image data (any format Sharp can read).
99
+ * @param {Buffer|string} pInput - Image data (Buffer) or file path (string).
100
100
  * @param {object} pOptions - Resize options.
101
101
  * @param {number} [pOptions.Width] - Target width in pixels.
102
102
  * @param {number} [pOptions.Height] - Target height in pixels.
@@ -107,7 +107,7 @@ class ConversionCore
107
107
  * @param {string} [pOptions.Position] - Resize position/gravity: 'centre', 'north', 'south', etc.
108
108
  * @param {Function} fCallback - Called with (pError, pOutputBuffer, pContentType).
109
109
  */
110
- imageResize(pInputBuffer, pOptions, fCallback)
110
+ imageResize(pInput, pOptions, fCallback)
111
111
  {
112
112
  let tmpOptions = pOptions || {};
113
113
  let tmpFormat = (tmpOptions.Format || 'jpeg').toLowerCase();
@@ -132,7 +132,8 @@ class ConversionCore
132
132
  tmpResizeConfig.position = tmpOptions.Position;
133
133
  }
134
134
 
135
- let tmpSharpInstance = libSharp(pInputBuffer);
135
+ // Accept file path or buffer; disable pixel limit for large scans
136
+ let tmpSharpInstance = libSharp(pInput, { limitInputPixels: false });
136
137
 
137
138
  if (tmpAutoOrient)
138
139
  {
@@ -268,9 +268,33 @@ class OratorConversionBeaconProvider extends libBeaconCapabilityProvider
268
268
  */
269
269
  execute(pAction, pWorkItem, pContext, fCallback, fReportProgress)
270
270
  {
271
+ let tmpLog = pContext && pContext.log ? pContext.log : { info: console.log, warn: console.warn, error: console.error };
272
+ tmpLog.info(`[OratorConversion] execute: action="${pAction}" workItem=${pWorkItem.WorkItemHash || '?'} settings=${JSON.stringify(pWorkItem.Settings || {}).substring(0, 200)}`);
271
273
  let tmpSettings = pWorkItem.Settings || {};
272
274
  let tmpStagingPath = pContext.StagingPath || process.cwd();
273
275
 
276
+ // Coerce settings types from the action's schema.
277
+ // Template engines and JSON transport may deliver numbers as strings.
278
+ let tmpActionDef = this.actions[pAction];
279
+ if (tmpActionDef && tmpActionDef.SettingsSchema)
280
+ {
281
+ for (let i = 0; i < tmpActionDef.SettingsSchema.length; i++)
282
+ {
283
+ let tmpField = tmpActionDef.SettingsSchema[i];
284
+ let tmpVal = tmpSettings[tmpField.Name];
285
+ if (tmpVal === undefined || tmpVal === null || tmpVal === '') { continue; }
286
+ if (tmpField.DataType === 'Number' && typeof tmpVal === 'string')
287
+ {
288
+ let tmpNum = Number(tmpVal);
289
+ if (!isNaN(tmpNum)) { tmpSettings[tmpField.Name] = tmpNum; }
290
+ }
291
+ else if (tmpField.DataType === 'Boolean' && typeof tmpVal === 'string')
292
+ {
293
+ tmpSettings[tmpField.Name] = (tmpVal === 'true' || tmpVal === '1');
294
+ }
295
+ }
296
+ }
297
+
274
298
  let tmpInputPath = this._resolvePath(tmpSettings.InputFile, tmpStagingPath);
275
299
  let tmpOutputPath = this._resolvePath(tmpSettings.OutputFile, tmpStagingPath);
276
300
 
@@ -284,14 +308,17 @@ class OratorConversionBeaconProvider extends libBeaconCapabilityProvider
284
308
 
285
309
  if (!libFS.existsSync(tmpInputPath))
286
310
  {
311
+ tmpLog.warn(`[OratorConversion] Input file NOT FOUND: ${tmpInputPath} (settings.InputFile=${tmpSettings.InputFile})`);
287
312
  return fCallback(null, {
288
313
  Outputs: { StdOut: `Input file not found: ${tmpSettings.InputFile}`, ExitCode: -1, Result: '' },
289
314
  Log: [`OratorConversion: input file not found: ${tmpInputPath}`]
290
315
  });
291
316
  }
292
317
 
293
- // File-path actions (video, audio, probe) skip the buffer read
294
- let tmpFilePathActions = { 'MediaProbe': true, 'VideoExtractFrame': true, 'VideoThumbnail': true, 'AudioExtractSegment': true, 'AudioWaveform': true };
318
+ tmpLog.info(`[OratorConversion] Input file OK: ${tmpInputPath} (${libFS.statSync(tmpInputPath).size} bytes)`);
319
+
320
+ // File-path actions skip the buffer read — Sharp and ffmpeg handle files directly
321
+ let tmpFilePathActions = { 'ImageResize': true, 'ImageConvert': true, 'MediaProbe': true, 'VideoExtractFrame': true, 'VideoThumbnail': true, 'AudioExtractSegment': true, 'AudioWaveform': true };
295
322
  if (tmpFilePathActions[pAction])
296
323
  {
297
324
  return this._executeFilePathAction(pAction, tmpSettings, tmpInputPath, tmpOutputPath, fCallback, fReportProgress);
@@ -325,12 +352,15 @@ class OratorConversionBeaconProvider extends libBeaconCapabilityProvider
325
352
  {
326
353
  if (pError)
327
354
  {
355
+ tmpLog.error(`[OratorConversion] ${pAction} FAILED: ${pError.message}`);
328
356
  return fCallback(null, {
329
357
  Outputs: { StdOut: `Conversion failed: ${pError.message}`, ExitCode: 1, Result: '' },
330
358
  Log: [`OratorConversion ${pAction} error: ${pError.message}`]
331
359
  });
332
360
  }
333
361
 
362
+ tmpLog.info(`[OratorConversion] ${pAction} SUCCESS: ${pOutputBuffer.length} bytes → ${tmpOutputPath}`);
363
+
334
364
  try
335
365
  {
336
366
  libFS.writeFileSync(tmpOutputPath, pOutputBuffer);
@@ -697,6 +727,102 @@ class OratorConversionBeaconProvider extends libBeaconCapabilityProvider
697
727
  break;
698
728
  }
699
729
 
730
+ case 'ImageResize':
731
+ {
732
+ if (fReportProgress) fReportProgress({ Percent: 10, Message: 'Resizing image...' });
733
+ this._Core.imageResize(pInputPath,
734
+ {
735
+ Width: pSettings.Width,
736
+ Height: pSettings.Height,
737
+ Format: pSettings.Format,
738
+ Quality: pSettings.Quality,
739
+ AutoOrient: pSettings.AutoOrient,
740
+ Fit: pSettings.Fit,
741
+ Position: pSettings.Position
742
+ },
743
+ (pError, pOutputBuffer, pContentType) =>
744
+ {
745
+ if (pError)
746
+ {
747
+ return fCallback(null, {
748
+ Outputs: { StdOut: `Resize failed: ${pError.message}`, ExitCode: 1, Result: '' },
749
+ Log: [`OratorConversion ImageResize error: ${pError.message}`]
750
+ });
751
+ }
752
+
753
+ try
754
+ {
755
+ libFS.writeFileSync(pOutputPath, pOutputBuffer);
756
+ }
757
+ catch (pWriteError)
758
+ {
759
+ return fCallback(null, {
760
+ Outputs: { StdOut: `Write failed: ${pWriteError.message}`, ExitCode: 1, Result: '' },
761
+ Log: [`OratorConversion ImageResize write error: ${pWriteError.message}`]
762
+ });
763
+ }
764
+
765
+ return fCallback(null, {
766
+ Outputs:
767
+ {
768
+ StdOut: `Resized ${pSettings.InputFile} → ${pSettings.OutputFile}`,
769
+ ExitCode: 0,
770
+ Result: pOutputPath,
771
+ ContentType: pContentType || '',
772
+ OutputSize: pOutputBuffer.length
773
+ },
774
+ Log: [`OratorConversion ImageResize: ${pSettings.InputFile} → ${pSettings.OutputFile} (${pOutputBuffer.length} bytes)`]
775
+ });
776
+ });
777
+ break;
778
+ }
779
+
780
+ case 'ImageConvert':
781
+ {
782
+ if (fReportProgress) fReportProgress({ Percent: 10, Message: 'Converting image...' });
783
+ this._Core.imageResize(pInputPath,
784
+ {
785
+ Format: pSettings.Format,
786
+ Quality: pSettings.Quality,
787
+ AutoOrient: pSettings.AutoOrient
788
+ },
789
+ (pError, pOutputBuffer, pContentType) =>
790
+ {
791
+ if (pError)
792
+ {
793
+ return fCallback(null, {
794
+ Outputs: { StdOut: `Convert failed: ${pError.message}`, ExitCode: 1, Result: '' },
795
+ Log: [`OratorConversion ImageConvert error: ${pError.message}`]
796
+ });
797
+ }
798
+
799
+ try
800
+ {
801
+ libFS.writeFileSync(pOutputPath, pOutputBuffer);
802
+ }
803
+ catch (pWriteError)
804
+ {
805
+ return fCallback(null, {
806
+ Outputs: { StdOut: `Write failed: ${pWriteError.message}`, ExitCode: 1, Result: '' },
807
+ Log: [`OratorConversion ImageConvert write error: ${pWriteError.message}`]
808
+ });
809
+ }
810
+
811
+ return fCallback(null, {
812
+ Outputs:
813
+ {
814
+ StdOut: `Converted ${pSettings.InputFile} → ${pSettings.OutputFile}`,
815
+ ExitCode: 0,
816
+ Result: pOutputPath,
817
+ ContentType: pContentType || '',
818
+ OutputSize: pOutputBuffer.length
819
+ },
820
+ Log: [`OratorConversion ImageConvert: ${pSettings.InputFile} → ${pSettings.OutputFile} (${pOutputBuffer.length} bytes)`]
821
+ });
822
+ });
823
+ break;
824
+ }
825
+
700
826
  default:
701
827
  return fCallback(null, {
702
828
  Outputs: { StdOut: `Unknown file-path action: ${pAction}`, ExitCode: -1, Result: '' },
@@ -6,6 +6,9 @@ const libFS = require('fs');
6
6
  const libPath = require('path');
7
7
  const libOS = require('os');
8
8
 
9
+ const libBeaconService = require('ultravisor-beacon');
10
+ const libOratorConversionBeaconProvider = require('./Orator-Conversion-BeaconProvider.js');
11
+
9
12
  const libEndpointImageJpgToPng = require('./endpoints/Endpoint-Image-JpgToPng.js');
10
13
  const libEndpointImagePngToJpg = require('./endpoints/Endpoint-Image-PngToJpg.js');
11
14
  const libEndpointImageResize = require('./endpoints/Endpoint-Image-Resize.js');
@@ -78,6 +81,9 @@ class OratorFileTranslation extends libFableServiceProviderBase
78
81
  // Array of instantiated endpoint services
79
82
  this._endpointServices = [];
80
83
 
84
+ // Beacon service reference (created on connectBeacon)
85
+ this._BeaconService = null;
86
+
81
87
  // Initialize the built-in converters
82
88
  this.initializeDefaultConverters();
83
89
  }
@@ -508,6 +514,145 @@ class OratorFileTranslation extends libFableServiceProviderBase
508
514
 
509
515
  return true;
510
516
  }
517
+
518
+ /**
519
+ * Connect to an Ultravisor coordinator as a beacon, exposing the same
520
+ * MediaConversion capabilities available through the HTTP endpoints.
521
+ *
522
+ * This makes the running orator-conversion server discoverable by
523
+ * Ultravisor and allows it to receive work items from the mesh.
524
+ *
525
+ * @param {object} pBeaconConfig Beacon configuration:
526
+ * - ServerURL {string} Ultravisor server URL (required)
527
+ * - Name {string} Beacon name (default: 'orator-conversion')
528
+ * - Password {string} Auth password (default: '')
529
+ * - MaxConcurrent {number} Max concurrent work items (default: 2)
530
+ * - StagingPath {string} Local staging directory (default: cwd)
531
+ * - Tags {object} Beacon tags (default: {})
532
+ * @param {Function} fCallback Called with (pError, pBeaconInfo)
533
+ */
534
+ connectBeacon(pBeaconConfig, fCallback)
535
+ {
536
+ if (!pBeaconConfig || !pBeaconConfig.ServerURL)
537
+ {
538
+ return fCallback(new Error('connectBeacon requires a ServerURL in the config.'));
539
+ }
540
+
541
+ if (this._BeaconService && this._BeaconService.isEnabled())
542
+ {
543
+ this.log.warn('OratorFileTranslation: beacon already connected.');
544
+ return fCallback(null);
545
+ }
546
+
547
+ // Register the beacon service type with fable if not already present
548
+ this.fable.addServiceTypeIfNotExists('UltravisorBeacon', libBeaconService);
549
+
550
+ // Default staging path to dist/orator-conversion-staging
551
+ let tmpStagingPath = pBeaconConfig.StagingPath || require('path').resolve(__dirname, '..', 'dist', 'orator-conversion-staging');
552
+
553
+ // Ensure staging directory exists
554
+ try
555
+ {
556
+ require('fs').mkdirSync(tmpStagingPath, { recursive: true });
557
+ }
558
+ catch (pMkdirError)
559
+ {
560
+ // Already exists — fine
561
+ }
562
+
563
+ // Instantiate the beacon service with the provided config
564
+ this._BeaconService = this.fable.instantiateServiceProviderWithoutRegistration('UltravisorBeacon',
565
+ {
566
+ ServerURL: pBeaconConfig.ServerURL,
567
+ Name: pBeaconConfig.Name || 'orator-conversion',
568
+ Password: pBeaconConfig.Password || '',
569
+ MaxConcurrent: pBeaconConfig.MaxConcurrent || 2,
570
+ StagingPath: tmpStagingPath,
571
+ Tags: pBeaconConfig.Tags || {},
572
+ BindAddresses: pBeaconConfig.BindAddresses || []
573
+ });
574
+
575
+ // Create the MediaConversion capability provider and register it
576
+ let tmpProvider = new libOratorConversionBeaconProvider(
577
+ {
578
+ PdftkPath: this.PdftkPath,
579
+ PdftoppmPath: this.PdftoppmPath,
580
+ FfmpegPath: pBeaconConfig.FfmpegPath || 'ffmpeg',
581
+ FfprobePath: pBeaconConfig.FfprobePath || 'ffprobe',
582
+ MaxFileSizeBytes: this.MaxFileSize,
583
+ LogLevel: this.LogLevel
584
+ });
585
+
586
+ // Initialize the provider (checks tool availability), then register and enable
587
+ tmpProvider.initialize(
588
+ (pInitError) =>
589
+ {
590
+ if (pInitError)
591
+ {
592
+ this.log.error(`OratorFileTranslation: beacon provider init failed: ${pInitError.message}`);
593
+ this._BeaconService = null;
594
+ return fCallback(pInitError);
595
+ }
596
+
597
+ // Register the capability with the beacon service
598
+ this._BeaconService.registerCapability(
599
+ {
600
+ Capability: tmpProvider.Capability,
601
+ Name: tmpProvider.Name,
602
+ actions: tmpProvider.actions,
603
+ execute: tmpProvider.execute.bind(tmpProvider),
604
+ initialize: (fCb) => { return fCb(null); }, // Already initialized above
605
+ shutdown: (fCb) => { return fCb(null); }
606
+ });
607
+
608
+ // Enable the beacon — authenticate, register, begin polling
609
+ this._BeaconService.enable(
610
+ (pEnableError, pBeaconInfo) =>
611
+ {
612
+ if (pEnableError)
613
+ {
614
+ this.log.error(`OratorFileTranslation: beacon enable failed: ${pEnableError.message}`);
615
+ this._BeaconService = null;
616
+ return fCallback(pEnableError);
617
+ }
618
+
619
+ this.log.info(`OratorFileTranslation: beacon connected as ${pBeaconInfo.BeaconID}`);
620
+ return fCallback(null, pBeaconInfo);
621
+ });
622
+ });
623
+ }
624
+
625
+ /**
626
+ * Disconnect the beacon from the Ultravisor coordinator.
627
+ *
628
+ * @param {Function} fCallback Called with (pError)
629
+ */
630
+ disconnectBeacon(fCallback)
631
+ {
632
+ if (!this._BeaconService || !this._BeaconService.isEnabled())
633
+ {
634
+ if (this.log)
635
+ {
636
+ this.log.info('OratorFileTranslation: beacon not connected, nothing to disconnect.');
637
+ }
638
+ return fCallback(null);
639
+ }
640
+
641
+ this._BeaconService.disable(
642
+ (pError) =>
643
+ {
644
+ if (pError)
645
+ {
646
+ this.log.warn(`OratorFileTranslation: beacon disconnect warning: ${pError.message}`);
647
+ }
648
+ else
649
+ {
650
+ this.log.info('OratorFileTranslation: beacon disconnected.');
651
+ }
652
+ this._BeaconService = null;
653
+ return fCallback(pError || null);
654
+ });
655
+ }
511
656
  }
512
657
 
513
658
  module.exports = OratorFileTranslation;