echogarden 0.11.18 → 0.11.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echogarden",
3
- "version": "0.11.18",
3
+ "version": "0.11.20",
4
4
  "description": "An integrated speech toolset, providing a variety of synthesis, recognition, alignment, and other processing tools, designed to be directly accessible to end-users.",
5
5
  "author": "Rotem Dan",
6
6
  "license": "GPL-3.0",
@@ -54,8 +54,8 @@
54
54
  "echogarden": "./dist/cli/CLILauncher.js"
55
55
  },
56
56
  "dependencies": {
57
- "@aws-sdk/client-polly": "^3.454.0",
58
- "@aws-sdk/client-transcribe-streaming": "^3.454.0",
57
+ "@aws-sdk/client-polly": "^3.529.1",
58
+ "@aws-sdk/client-transcribe-streaming": "^3.529.1",
59
59
  "@echogarden/espeak-ng-emscripten": "^0.1.2",
60
60
  "@echogarden/fasttext-wasm": "^0.1.0",
61
61
  "@echogarden/flite-wasi": "^0.1.1",
@@ -66,41 +66,42 @@
66
66
  "@echogarden/sonic-wasm": "^0.1.1",
67
67
  "@echogarden/speex-resampler-wasm": "^0.1.1",
68
68
  "@echogarden/svoxpico-wasm": "^0.1.0",
69
- "@mozilla/readability": "^0.4.4",
69
+ "@mozilla/readability": "^0.5.0",
70
70
  "alawmulaw": "^6.0.0",
71
71
  "buffer-split": "^1.0.0",
72
72
  "chalk": "^5.3.0",
73
73
  "cldr-segmentation": "^2.2.0",
74
74
  "command-exists": "^1.2.9",
75
- "compromise": "^14.10.1",
76
- "compromise-dates": "^3.4.2",
77
- "fs-extra": "^11.1.1",
78
- "gaxios": "^6.1.1",
75
+ "compromise": "^14.12.0",
76
+ "compromise-dates": "^3.5.0",
77
+ "fs-extra": "^11.2.0",
78
+ "gaxios": "^6.3.0",
79
79
  "graceful-fs": "^4.2.11",
80
80
  "html-escaper": "^3.0.3",
81
81
  "html-to-text": "^9.0.5",
82
82
  "import-meta-resolve": "^4.0.0",
83
83
  "jieba-wasm": "^0.0.2",
84
- "jsdom": "^22.1.0",
84
+ "jsdom": "^24.0.0",
85
85
  "json5": "^2.2.3",
86
86
  "kuromoji": "^0.1.2",
87
- "microsoft-cognitiveservices-speech-sdk": "^1.33.1",
87
+ "microsoft-cognitiveservices-speech-sdk": "^1.36.0",
88
88
  "moving-median": "^1.0.0",
89
89
  "msgpack-lite": "^0.1.26",
90
90
  "ndarray": "^1.0.19",
91
91
  "ndarray-ops": "^1.2.2",
92
- "onnxruntime-node": "^1.16.3",
92
+ "onnxruntime-node": "^1.17.0",
93
93
  "sam-js": "^0.2.1",
94
94
  "strip-ansi": "^7.1.0",
95
95
  "tar": "^6.2.0",
96
96
  "tinyld": "^1.3.4",
97
- "ws": "^8.14.2",
98
- "wtf_wikipedia": "^10.2.1"
97
+
98
+ "ws": "^8.16.0",
99
+ "wtf_wikipedia": "^10.3.0"
99
100
  },
100
101
  "peerDependencies": {
101
102
  "@echogarden/vosk": "^0.3.39-patched.1",
102
103
  "speaker": "^0.5.4",
103
- "winax": "^3.4.1"
104
+ "winax": "^3.4.2"
104
105
  },
105
106
  "peerDependenciesMeta": {
106
107
  "@echogarden/vosk": {
@@ -121,14 +122,14 @@
121
122
  "@types/msgpack-lite": "^0.1.11",
122
123
  "@types/ndarray": "^1.0.14",
123
124
  "@types/ndarray-ops": "^1.2.7",
124
- "@types/node": "^20.10.0",
125
+ "@types/node": "^20.11.25",
125
126
  "@types/recursive-readdir": "^2.2.4",
126
- "@types/tar": "^6.1.10",
127
+ "@types/tar": "^6.1.11",
127
128
  "@types/ws": "^8.5.10",
128
- "@typescript-eslint/eslint-plugin": "^6.12.0",
129
- "@typescript-eslint/parser": "^6.12.0",
130
- "eslint": "^8.54.0",
131
- "ts-json-schema-generator": "^1.4.0",
132
- "typescript": "^5.3.2"
129
+ "@typescript-eslint/eslint-plugin": "^7.1.1",
130
+ "@typescript-eslint/parser": "^7.1.1",
131
+ "eslint": "^8.57.0",
132
+ "ts-json-schema-generator": "^1.5.0",
133
+ "typescript": "^5.4.2"
133
134
  }
134
135
  }
@@ -4,7 +4,7 @@ import { deepClone, extendDeep } from "../utilities/ObjectUtilities.js"
4
4
 
5
5
  import * as FFMpegTranscoder from "../codecs/FFMpegTranscoder.js"
6
6
 
7
- import { clip, convertHtmlToText, sha256AsHex, simplifyPunctuationCharacters, stringifyAndFormatJson, logToStderr, yieldToEventLoop } from "../utilities/Utilities.js"
7
+ import { clip, convertHtmlToText, sha256AsHex, simplifyPunctuationCharacters, stringifyAndFormatJson, logToStderr, yieldToEventLoop, delay, runOperationWithRetries } from "../utilities/Utilities.js"
8
8
  import { RawAudio, concatAudioSegments, downmixToMono, encodeWaveBuffer, getAudioPeakDecibels, getEmptyRawAudio, getRawAudioDuration, normalizeAudioLevel, trimAudioEnd, trimAudioStart } from "../audio/AudioUtilities.js"
9
9
  import { Logger } from "../utilities/Logger.js"
10
10
 
@@ -727,7 +727,10 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
727
727
 
728
728
  logger.end()
729
729
 
730
- const { rawAudio, timeline: segmentTimeline } = await GoogleTranslateTTS.synthesizeLongText(text, language, options.googleTranslate?.tld, options.sentenceEndPause, options.segmentEndPause)
730
+ const { rawAudio, timeline: segmentTimeline } =
731
+ await runOperationWithRetries(
732
+ () => GoogleTranslateTTS.synthesizeLongText(text, language, options.googleTranslate?.tld, options.sentenceEndPause, options.segmentEndPause),
733
+ logger)
731
734
 
732
735
  synthesizedAudio = rawAudio
733
736
 
@@ -777,7 +780,10 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
777
780
 
778
781
  logger.end()
779
782
 
780
- const { rawAudio, timeline: edgeTimeline } = await MicrosoftEdgeTTS.synthesize(text, trustedClientToken, voice, ssmlPitch, ssmlRate)
783
+ const { rawAudio, timeline: edgeTimeline } =
784
+ await runOperationWithRetries(
785
+ () => MicrosoftEdgeTTS.synthesize(text, trustedClientToken!, voice, ssmlPitch, ssmlRate),
786
+ logger)
781
787
 
782
788
  synthesizedAudio = rawAudio
783
789
  timeline = edgeTimeline
@@ -1212,6 +1218,8 @@ export const defaultSynthesisOptions: SynthesisOptions = {
1212
1218
  export async function requestVoiceList(options: VoiceListRequestOptions): Promise<RequestVoiceListResult> {
1213
1219
  options = extendDeep(defaultVoiceListRequestOptions, options)
1214
1220
 
1221
+ const logger = new Logger()
1222
+
1215
1223
  const cacheOptions = options.cache!
1216
1224
 
1217
1225
  let cacheDir = cacheOptions?.path
@@ -1456,7 +1464,10 @@ export async function requestVoiceList(options: VoiceListRequestOptions): Promis
1456
1464
  throw new Error("No trusted client token provided")
1457
1465
  }
1458
1466
 
1459
- const voices = await MicrosoftEdgeTTS.getVoiceList(trustedClientToken)
1467
+ const voices =
1468
+ await runOperationWithRetries(
1469
+ () => MicrosoftEdgeTTS.getVoiceList(trustedClientToken),
1470
+ logger)
1460
1471
 
1461
1472
  voiceList = voices.map((voice: any) => ({
1462
1473
  name: voice.Name,
package/src/cli/CLI.ts CHANGED
@@ -186,6 +186,8 @@ const commandHelp = [
186
186
  ` List installed Echogarden packages\n`,
187
187
  `${executableName} ${chalk.magentaBright('serve')} [options...]`,
188
188
  ` Start a server\n`,
189
+ `Options reference: ${chalk.blueBright('https://bit.ly/echogarden-options')}`
190
+ //` ${chalk.blueBright('https://github.com/echogarden-project/echogarden/blob/main/docs/Options.md')}`
189
191
  ]
190
192
 
191
193
  async function startWithArgs(parsedArgs: CLIArguments) {
@@ -688,24 +688,24 @@ export function softmax(logits: number[], temperature = 1.0) {
688
688
  }
689
689
  }
690
690
 
691
- const temperatureMultiplier = 1 / temperature
691
+ const temperatureReciprocal = 1 / (temperature + 1e-40)
692
692
 
693
693
  const result: number[] = []
694
694
 
695
695
  let sumOfExponentiatedValues = 0.0
696
696
 
697
697
  for (const value of logits) {
698
- const eToValue = Math.exp((value - maxValue) * temperatureMultiplier)
698
+ const eToValue = Math.exp((value - maxValue) * temperatureReciprocal)
699
699
 
700
700
  sumOfExponentiatedValues += eToValue
701
701
 
702
702
  result.push(eToValue)
703
703
  }
704
704
 
705
- const sumOfExponentiatedValuesMultiplier = 1 / (sumOfExponentiatedValues + 1e-40)
705
+ const sumOfExponentiatedValuesReciprocal = 1 / (sumOfExponentiatedValues + 1e-40)
706
706
 
707
707
  for (let i = 0; i < result.length; i++) {
708
- result[i] *= sumOfExponentiatedValuesMultiplier
708
+ result[i] *= sumOfExponentiatedValuesReciprocal
709
709
  }
710
710
 
711
711
  return result
@@ -789,6 +789,7 @@ export function sumExp(values: number[]) {
789
789
 
790
790
  export function logSoftmax(values: number[], minVal = 1e-40) {
791
791
  const softMaxOfValues = softmax(values)
792
+
792
793
  return logOfVector(softMaxOfValues, minVal)
793
794
  }
794
795
 
@@ -4,7 +4,7 @@ import { logToStderr } from '../utilities/Utilities.js'
4
4
  import { OpenPromise } from '../utilities/OpenPromise.js'
5
5
  import { readFile, existsSync } from '../utilities/FileSystem.js'
6
6
  import { extendDeep } from '../utilities/ObjectUtilities.js'
7
- import { sendMessageToWorker, addListenerToWorkerMessages, startNewWorkerThread } from './Worker.js'
7
+ import { sendMessageToWorker, addListenerToWorkerMessages, startNewWorkerThread, startMessageChannel } from './Worker.js'
8
8
  import { Worker } from 'node:worker_threads'
9
9
  import { IncomingMessage, ServerResponse } from 'node:http'
10
10
  import { Logger } from '../utilities/Logger.js'
@@ -87,6 +87,7 @@ export async function startServer(serverOptions: ServerOptions, onStarted: (opti
87
87
  workerThread = await startNewWorkerThread()
88
88
  workerThread.on('message', onWorkerMessage)
89
89
  } else {
90
+ startMessageChannel()
90
91
  addListenerToWorkerMessages(onWorkerMessage)
91
92
  }
92
93
 
@@ -14,29 +14,38 @@ import chalk from "chalk"
14
14
 
15
15
  const log = logToStderr
16
16
 
17
- const messageChannel = new MessageChannel()
18
- messageChannel.port1.start()
19
- messageChannel.port2.start()
17
+ let messageChannel: MessageChannel | undefined = undefined
20
18
 
21
19
  const canceledRequests = new Set<string>()
20
+
22
21
  let cancelCurrentTask = false
23
22
 
24
- export function shouldCancelCurrentTask() {
25
- return cancelCurrentTask
26
- }
23
+ const messageQueue = new Queue<any>()
24
+ let isProcessing = false
27
25
 
28
- addListenerToClientMessages((message) => {
29
- if (message.messageType == "CancelationRequest") {
30
- //log(`CANCEL REQUESTED FOR ${message.requestId}`)
31
- canceledRequests.add(message.requestId)
26
+ export function startMessageChannel() {
27
+ if (messageChannel != null) {
32
28
  return
33
29
  }
34
30
 
35
- enqueueAndProcessIfIdle(message)
36
- })
31
+ messageChannel = new MessageChannel()
32
+ messageChannel.port1.start()
33
+ messageChannel.port2.start()
37
34
 
38
- const messageQueue = new Queue<any>()
39
- let isProcessing = false
35
+ addListenerToClientMessages((message) => {
36
+ if (message.messageType == "CancelationRequest") {
37
+ //log(`CANCEL REQUESTED FOR ${message.requestId}`)
38
+ canceledRequests.add(message.requestId)
39
+ return
40
+ }
41
+
42
+ enqueueAndProcessIfIdle(message)
43
+ })
44
+ }
45
+
46
+ export function shouldCancelCurrentTask() {
47
+ return cancelCurrentTask
48
+ }
40
49
 
41
50
  function enqueueAndProcessIfIdle(message: any) {
42
51
  messageQueue.enqueue(message)
@@ -344,25 +353,39 @@ export interface TextLanguageDetectionResponseMessage extends WorkerMessageBase,
344
353
  // Messaging methods
345
354
  ///////////////////////////////////////////////////////////////////////////////////////////////
346
355
  export function sendMessageToWorker(message: any) {
347
- messageChannel.port1.postMessage(message)
356
+ ensureMessageChannelCreated()
357
+
358
+ messageChannel?.port1.postMessage(message)
348
359
  }
349
360
 
350
361
  export function addListenerToWorkerMessages(handler: MessageFunc) {
351
- messageChannel.port1.addEventListener('message', (event) => {
362
+ ensureMessageChannelCreated()
363
+
364
+ messageChannel?.port1.addEventListener('message', (event) => {
352
365
  handler(event.data)
353
366
  })
354
367
  }
355
368
 
356
369
  function sendMessageToClient(message: any) {
357
- messageChannel.port2.postMessage(message)
370
+ ensureMessageChannelCreated()
371
+
372
+ messageChannel?.port2.postMessage(message)
358
373
  }
359
374
 
360
375
  function addListenerToClientMessages(handler: MessageFunc) {
361
- messageChannel.port2.addEventListener('message', (event) => {
376
+ ensureMessageChannelCreated()
377
+
378
+ messageChannel?.port2.addEventListener('message', (event) => {
362
379
  handler(event.data)
363
380
  })
364
381
  }
365
382
 
383
+ function ensureMessageChannelCreated() {
384
+ if (messageChannel == null) {
385
+ throw new Error(`Message channel has not been created`)
386
+ }
387
+ }
388
+
366
389
  ///////////////////////////////////////////////////////////////////////////////////////////////
367
390
  // Worker thread methods
368
391
  ///////////////////////////////////////////////////////////////////////////////////////////////
@@ -1,5 +1,5 @@
1
1
  import { isMainThread, parentPort } from 'node:worker_threads'
2
- import { sendMessageToWorker, addListenerToWorkerMessages } from './Worker.js'
2
+ import { sendMessageToWorker, addListenerToWorkerMessages, startMessageChannel } from './Worker.js'
3
3
  import { OpenPromise } from '../utilities/OpenPromise.js'
4
4
 
5
5
  async function startIfInWorkerThread() {
@@ -7,6 +7,8 @@ async function startIfInWorkerThread() {
7
7
  return
8
8
  }
9
9
 
10
+ startMessageChannel()
11
+
10
12
  addListenerToWorkerMessages((message) => {
11
13
  parentPort?.postMessage(message)
12
14
  })
@@ -13,7 +13,7 @@ export async function synthesize(
13
13
  subscriptionKey: string,
14
14
  serviceRegion: string,
15
15
  languageCode = "en-US",
16
- voice = "Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)",
16
+ voice = "Microsoft Server Speech Text to Speech Voice (en-US, AvaNeural)",
17
17
  ssmlEnabled = false,
18
18
  ssmlPitchString = "+0Hz",
19
19
  ssmlRateString = "+0%") {
@@ -10,7 +10,8 @@ import * as FFMpegTranscoder from "../codecs/FFMpegTranscoder.js"
10
10
  import { Logger } from "../utilities/Logger.js"
11
11
  import { OpenPromise } from "../utilities/OpenPromise.js"
12
12
  import { getRandomHexString, logToStderr } from "../utilities/Utilities.js"
13
- import { getRawAudioDuration } from "../audio/AudioUtilities.js"
13
+ import { RawAudio, getEmptyRawAudio, getRawAudioDuration } from "../audio/AudioUtilities.js"
14
+ import { Timer } from "../utilities/Timer.js"
14
15
 
15
16
  const traceEnabled = false
16
17
 
@@ -19,7 +20,7 @@ const log: typeof logToStderr = traceEnabled ? logToStderr : () => { }
19
20
  export async function synthesize(
20
21
  text: string,
21
22
  trustedClientToken: string,
22
- voice = "Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)",
23
+ voice = "Microsoft Server Speech Text to Speech Voice (en-US, AvaNeural)",
23
24
  ssmlPitchString = "+0Hz",
24
25
  ssmlRateString = "+0%",
25
26
  ssmlVolumeString = "+0%") {
@@ -30,7 +31,17 @@ export async function synthesize(
30
31
  const { audioData, events } = await requestSynthesis(text, trustedClientToken, voice, ssmlPitchString, ssmlRateString, ssmlVolumeString)
31
32
  logger.end()
32
33
 
33
- const rawAudio = await FFMpegTranscoder.decodeToChannels(audioData, 24000, 1)
34
+ //logToStderr(`Audio length: ${audioData.length}`)
35
+
36
+ let rawAudio: RawAudio
37
+
38
+ try {
39
+ rawAudio = await FFMpegTranscoder.decodeToChannels(audioData, 24000, 1)
40
+ } catch (e) {
41
+ rawAudio = getEmptyRawAudio(1, 24000)
42
+ }
43
+
44
+ //logToStderr(`Raw audio length: ${rawAudio.audioChannels[0].length}`)
34
45
 
35
46
  logger.start("Convert boundary events to timeline")
36
47
  const timeline = AzureCognitiveServicesTTS.boundaryEventsToTimeline(events, getRawAudioDuration(rawAudio))
@@ -51,15 +62,55 @@ async function requestSynthesis(
51
62
 
52
63
  const synthesisOpenPromise = new OpenPromise<SynthesisRequestResult>()
53
64
 
65
+ let connectionFailed = false
66
+ let responseComplete = false
67
+
68
+ let lastReceievedMessageTime = Timer.currentTime
69
+
70
+ function checkForRequestTimeout() {
71
+ if (responseComplete || connectionFailed) {
72
+ clearInterval(timeoutCheckInterval)
73
+
74
+ return
75
+ }
76
+
77
+ if (!webSocket) {
78
+ return
79
+ }
80
+
81
+ if (Timer.currentTime - lastReceievedMessageTime > 10000) {
82
+ clearInterval(timeoutCheckInterval)
83
+
84
+ removeWebSocketHandlers()
85
+ webSocket.close()
86
+
87
+ synthesisOpenPromise.reject(new Error(`WebSocket request timed out after 10 seconds without a message from the server`))
88
+
89
+ return
90
+ }
91
+ }
92
+
93
+ let webSocket: WebSocket = undefined as any
94
+
95
+ const timeoutCheckInterval = setInterval(checkForRequestTimeout, 1000)
96
+
54
97
  const requestId = getRandomHexString()
55
98
 
56
- const webSocket = await initializeWebsocketConnection(trustedClientToken)
99
+ try {
100
+ webSocket = await initializeWebsocketConnection(trustedClientToken)
101
+ } catch(e) {
102
+ connectionFailed = true
103
+
104
+ throw e
105
+ }
57
106
 
58
107
  const receivedBinaryMessages: Buffer[] = []
59
108
  const receivedEventMessages: any[] = []
60
109
  const audioChunks: Buffer[] = []
61
110
 
62
111
  const onMessage = (messageData: Buffer, isBinary: boolean) => {
112
+ lastReceievedMessageTime = Timer.currentTime
113
+
63
114
  if (isBinary) {
64
115
  const [header, audioChunk] = parseBinaryMessage(messageData)
65
116
 
@@ -98,17 +149,22 @@ async function requestSynthesis(
98
149
  else if (header["Path"] == "turn.end") {
99
150
  const result: SynthesisRequestResult = { audioData: Buffer.concat(audioChunks), events: receivedEventMessages }
100
151
 
101
- webSocket.off("message", onMessage)
102
- webSocket.off("error", onError)
103
- webSocket.off("close", onClose)
152
+ removeWebSocketHandlers()
104
153
 
105
154
  synthesisOpenPromise.resolve(result)
155
+ responseComplete = true
106
156
  }
107
157
  }
108
158
  }
109
159
 
110
160
  const onError = (err: Error) => {
111
161
  synthesisOpenPromise.reject(err)
162
+
163
+ removeWebSocketHandlers()
164
+
165
+ if (webSocket) {
166
+ webSocket.close()
167
+ }
112
168
  }
113
169
 
114
170
  const onClose = async (code: number, reason: Buffer) => {
@@ -117,7 +173,15 @@ async function requestSynthesis(
117
173
  log(reason.toString())
118
174
 
119
175
  if (reason.length > 0) {
120
- synthesisOpenPromise.reject(`Websocket closed with code ${code}, reason: ${reason}`)
176
+ synthesisOpenPromise.reject(new Error(`Websocket closed with code ${code}, reason: ${reason}`))
177
+ }
178
+ }
179
+
180
+ function removeWebSocketHandlers() {
181
+ if (webSocket) {
182
+ webSocket.off("message", onMessage)
183
+ webSocket.off("error", onError)
184
+ webSocket.off("close", onClose)
121
185
  }
122
186
  }
123
187
 
@@ -224,7 +288,7 @@ export async function initializeWebsocketConnection(trustedClientToken: string)
224
288
  log(reason.toString())
225
289
 
226
290
  if (reason.length > 0) {
227
- requestOpenPromise.reject(`Websocket closed with code ${code}, reason: ${reason}`)
291
+ requestOpenPromise.reject(new Error(`Websocket closed with code ${code}, reason: ${reason}`))
228
292
  }
229
293
  }
230
294
 
@@ -24,9 +24,20 @@ export class Logger {
24
24
  }
25
25
 
26
26
  writeToStderr(`${titleColor(title)}.. `)
27
+
28
+ this.setAsActiveLogger()
29
+
30
+ this.timer.restart()
31
+ }
32
+
33
+ setAsActiveLogger() {
27
34
  this.active = true
28
35
  currentActiveLogger = this
29
- this.timer.restart()
36
+ }
37
+
38
+ unsetAsActiveLogger() {
39
+ this.active = false
40
+ currentActiveLogger = null
30
41
  }
31
42
 
32
43
  end() {
@@ -4,6 +4,8 @@ import { inspect } from 'node:util'
4
4
 
5
5
  import { RandomGenerator } from './RandomGenerator.js'
6
6
  import { randomUUID, randomBytes } from 'node:crypto'
7
+ import { Logger } from './Logger.js'
8
+ import chalk from 'chalk'
7
9
 
8
10
  const log = logToStderr
9
11
 
@@ -445,9 +447,9 @@ export function splitFilenameOnExtendedExtension(filenameWithExtension: string)
445
447
  splitPoint = i
446
448
 
447
449
  continue
448
- } else {
449
- break
450
- }
450
+ } else {
451
+ break
452
+ }
451
453
  }
452
454
  }
453
455
 
@@ -566,9 +568,40 @@ export function getConsecutiveRepetitionScoreRelativeToFirstSubstring(tokens: st
566
568
  export async function resolveModuleScriptPath(moduleName: string) {
567
569
  const { resolve } = await import('import-meta-resolve')
568
570
 
569
- const scriptPath = await resolve(moduleName, import.meta.url)
571
+ const scriptPath = resolve(moduleName, import.meta.url)
570
572
 
571
573
  const { fileURLToPath } = await import('url')
572
574
 
573
575
  return fileURLToPath(scriptPath)
574
576
  }
577
+
578
+ export async function runOperationWithRetries<R>(
579
+ operationFunc: () => Promise<R>,
580
+ logger: Logger,
581
+ opreationName = "Operation",
582
+ delayBetweenRetries = 2000,
583
+ maxRetries = 1000) {
584
+
585
+ for (let retryIndex = 1; retryIndex <= maxRetries; retryIndex++) {
586
+ try {
587
+ const result = await operationFunc()
588
+
589
+ return result
590
+ } catch (e: any) {
591
+ logger.setAsActiveLogger()
592
+
593
+ logger.logTitledMessage(`Error`, e.message, chalk.redBright)
594
+ logger.log(``)
595
+ logger.logTitledMessage(`${opreationName} failed`, `Trying again in ${delayBetweenRetries}ms..`, chalk.redBright)
596
+
597
+ await delay(delayBetweenRetries)
598
+
599
+ logger.log(``)
600
+ logger.logTitledMessage(`Starting attempt`, `${retryIndex} / ${maxRetries}`)
601
+
602
+ logger.unsetAsActiveLogger()
603
+ }
604
+ }
605
+
606
+ throw new Error(`${opreationName} failed after ${maxRetries} retry attempts`)
607
+ }