@aj-archipelago/cortex 1.1.11 → 1.1.13

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.
@@ -87,32 +87,24 @@ async function splitMediaFile(inputPath, chunkDurationInSeconds = 500) {
87
87
  inputPath = downloadPath;
88
88
  }
89
89
 
90
-
91
90
  const metadata = await ffmpegProbe(inputPath);
92
91
  const duration = metadata.format.duration;
93
92
  const numChunks = Math.ceil((duration - 1) / chunkDurationInSeconds);
94
93
 
95
94
  const chunkPromises = [];
96
-
97
-
95
+ const chunkOffsets = [];
98
96
 
99
97
  for (let i = 0; i < numChunks; i++) {
100
- const outputFileName = path.join(
101
- uniqueOutputPath,
102
- `chunk-${i + 1}-${path.parse(inputPath).name}.mp3`
103
- );
104
-
105
- const chunkPromise = processChunk(
106
- inputPath,
107
- outputFileName,
108
- i * chunkDurationInSeconds,
109
- chunkDurationInSeconds
110
- );
98
+ const outputFileName = path.join(uniqueOutputPath, `chunk-${i + 1}-${path.parse(inputPath).name}.mp3`);
99
+ const offset = i * chunkDurationInSeconds;
111
100
 
101
+ const chunkPromise = processChunk(inputPath, outputFileName, offset, chunkDurationInSeconds);
102
+
112
103
  chunkPromises.push(chunkPromise);
104
+ chunkOffsets.push(offset);
113
105
  }
114
106
 
115
- return { chunkPromises, uniqueOutputPath };
107
+ return { chunkPromises, chunkOffsets, uniqueOutputPath };
116
108
  } catch (err) {
117
109
  const msg = `Error processing media file, check if the file is a valid media file or is accessible`;
118
110
  console.error(msg, err);
@@ -143,7 +143,7 @@ async function main(context, req) {
143
143
  file = await processYoutubeUrl(file);
144
144
  }
145
145
 
146
- const { chunkPromises, uniqueOutputPath } = await splitMediaFile(file);
146
+ const { chunkPromises, chunkOffsets, uniqueOutputPath } = await splitMediaFile(file);
147
147
  folder = uniqueOutputPath;
148
148
 
149
149
  numberOfChunks = chunkPromises.length; // for progress reporting
@@ -158,9 +158,11 @@ async function main(context, req) {
158
158
  }
159
159
 
160
160
  // sequential processing of chunks
161
- for (const chunk of chunks) {
161
+ for (let index = 0; index < chunks.length; index++) {
162
+ const chunk = chunks[index];
162
163
  const blobName = useAzure ? await saveFileToBlob(chunk, requestId) : await moveFileToPublicFolder(chunk, requestId);
163
- result.push(blobName);
164
+ const chunkOffset = chunkOffsets[index];
165
+ result.push({ uri:blobName, offset:chunkOffset });
164
166
  context.log(`Saved chunk as: ${blobName}`);
165
167
  sendProgress();
166
168
  }
@@ -182,7 +184,10 @@ async function main(context, req) {
182
184
  }
183
185
  }
184
186
 
185
- console.log(`result: ${result}`);
187
+ console.log('result:', result.map(item =>
188
+ typeof item === 'object' ? JSON.stringify(item, null, 2) : item
189
+ ).join('\n'));
190
+
186
191
  context.res = {
187
192
  body: result
188
193
  };
@@ -323,8 +323,13 @@ const makeRequest = async (cortexRequest) => {
323
323
  return { response, duration };
324
324
  }
325
325
  } else {
326
- // if there are multiple endpoints, retry everything as it
327
- // could be going to a different host
326
+ // if there are multiple endpoints, retry everything by default
327
+ // as it could be a temporary issue with one endpoint
328
+ // certain errors (e.g. 400) are problems with the request itself
329
+ // and should not be retried
330
+ if (status == 400) {
331
+ return { response, duration };
332
+ }
328
333
  cortexRequest.selectNewEndpoint();
329
334
  }
330
335
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -15,6 +15,7 @@ import { v4 as uuidv4 } from 'uuid';
15
15
  import { promisify } from 'util';
16
16
  import { publishRequestProgress } from '../../lib/redisSubscription.js';
17
17
  import logger from '../../lib/logger.js';
18
+ import CortexRequest from '../../lib/cortexRequest.js';
18
19
  const pipeline = promisify(stream.pipeline);
19
20
 
20
21
  const API_URL = config.get('whisperMediaApiUrl');
@@ -25,7 +26,7 @@ if(WHISPER_TS_API_URL){
25
26
  logger.warn(`WHISPER API URL not set using default OpenAI API Whisper`);
26
27
  }
27
28
 
28
- const OFFSET_CHUNK = 1000 * 500; // 500 seconds chunk offset
29
+ const OFFSET_CHUNK = 500; //seconds of each chunk offset, only used if helper does not provide
29
30
 
30
31
  async function deleteTempPath(path) {
31
32
  try {
@@ -96,7 +97,7 @@ function convertToText(str) {
96
97
  .join(' ');
97
98
  }
98
99
 
99
- function alignSubtitles(subtitles, format) {
100
+ function alignSubtitles(subtitles, format, offsets) {
100
101
  const result = [];
101
102
 
102
103
  function preprocessStr(str) {
@@ -116,7 +117,7 @@ function alignSubtitles(subtitles, format) {
116
117
  }
117
118
 
118
119
  for (let i = 0; i < subtitles.length; i++) {
119
- result.push(...shiftSubtitles(subtitles[i], i * OFFSET_CHUNK));
120
+ result.push(...shiftSubtitles(subtitles[i], offsets[i]*1000)); // convert to milliseconds
120
121
  }
121
122
 
122
123
  try {
@@ -171,12 +172,14 @@ class OpenAIWhisperPlugin extends ModelPlugin {
171
172
  // Execute the request to the OpenAI Whisper API
172
173
  async execute(text, parameters, prompt, cortexRequest) {
173
174
  const { pathwayResolver } = cortexRequest;
175
+
174
176
  const { responseFormat, wordTimestamped, highlightWords, maxLineWidth, maxLineCount, maxWordsPerLine } = parameters;
175
- cortexRequest.url = this.requestUrl(text);
176
177
 
177
178
  const chunks = [];
178
179
  const processChunk = async (uri) => {
179
180
  try {
181
+ const cortexRequest = new CortexRequest({ pathwayResolver });
182
+
180
183
  const chunk = await downloadFile(uri);
181
184
  chunks.push(chunk);
182
185
 
@@ -205,6 +208,8 @@ class OpenAIWhisperPlugin extends ModelPlugin {
205
208
  }
206
209
 
207
210
  const processTS = async (uri) => {
211
+ const cortexRequest = new CortexRequest({ pathwayResolver });
212
+
208
213
  const tsparams = { fileurl:uri };
209
214
  const { language } = parameters;
210
215
  if(language) tsparams.language = language;
@@ -223,7 +228,24 @@ class OpenAIWhisperPlugin extends ModelPlugin {
223
228
  cortexRequest.url = WHISPER_TS_API_URL;
224
229
  cortexRequest.data = tsparams;
225
230
 
226
- const res = await this.executeRequest(cortexRequest);
231
+ const MAX_RETRIES = 3;
232
+ let attempt = 0;
233
+ let res = null;
234
+ while(attempt < MAX_RETRIES){
235
+ sendProgress(true, true);
236
+ try {
237
+ res = await this.executeRequest(cortexRequest);
238
+ if(res.statusCode && res.statusCode >= 400){
239
+ throw new Error(res.message || 'An error occurred.');
240
+ }
241
+ break;
242
+ }
243
+ catch(err){
244
+ logger.warn(`Error calling timestamped API: ${err}. Retrying ${attempt+1} of ${MAX_RETRIES}...`);
245
+ attempt++;
246
+ }
247
+ }
248
+
227
249
  if (res.statusCode && res.statusCode >= 400) {
228
250
  throw new Error(res.message || 'An error occurred.');
229
251
  }
@@ -241,18 +263,23 @@ class OpenAIWhisperPlugin extends ModelPlugin {
241
263
  let completedCount = 0;
242
264
  let partialCount = 0;
243
265
  const { requestId } = pathwayResolver;
266
+ let partialRatio = 0;
267
+
268
+ const sendProgress = (partial=false, resetCount=false) => {
269
+ partialCount = resetCount ? 0 : partialCount;
244
270
 
245
- const MAXPARTIALCOUNT = 60;
246
- const sendProgress = (partial=false) => {
247
271
  if(partial){
248
- partialCount = Math.min(partialCount + 1, MAXPARTIALCOUNT-1);
249
- }else {
272
+ partialCount++;
273
+ const increment = 0.02 / Math.log2(partialCount + 1); // logarithmic diminishing increment
274
+ partialRatio = Math.min(partialRatio + increment, 0.99); // limit to 0.99
275
+ }else{
250
276
  partialCount = 0;
277
+ partialRatio = 0;
251
278
  completedCount++;
252
279
  }
253
- if (completedCount >= totalCount) return;
280
+ if(completedCount >= totalCount) return;
254
281
 
255
- const progress = (partialCount / MAXPARTIALCOUNT + completedCount) / totalCount;
282
+ const progress = (completedCount + partialRatio) / totalCount;
256
283
  logger.info(`Progress for ${requestId}: ${progress}`);
257
284
 
258
285
  publishRequestProgress({
@@ -262,57 +289,70 @@ class OpenAIWhisperPlugin extends ModelPlugin {
262
289
  });
263
290
  }
264
291
 
265
- async function processURI(uri) {
266
- let result = null;
267
- let _promise = null;
268
- let errorOccurred = false;
269
-
270
- const useTS = WHISPER_TS_API_URL && (wordTimestamped || highlightWords);
271
-
272
- if (useTS) {
273
- _promise = processTS;
274
- } else {
275
- _promise = processChunk;
276
- }
277
-
278
- _promise(uri).then((ts) => {
279
- result = ts;
280
- }).catch((err) => {
281
- logger.error(`Error occurred while processing URI: ${err}`);
282
- errorOccurred = err;
283
- });
284
-
285
- while(result === null && !errorOccurred) {
286
- sendProgress(true);
287
- await new Promise(r => setTimeout(r, 3000));
288
- }
292
+ async function processURI(uri) {
293
+ let result = null;
294
+ let _promise = null;
295
+ let errorOccurred = false;
289
296
 
290
- if(errorOccurred) {
291
- throw errorOccurred;
292
- }
293
-
294
- return result;
295
- }
297
+ const intervalId = setInterval(() => sendProgress(true), 3000);
296
298
 
297
- try {
298
- const uris = await this.getMediaChunks(file, requestId); // array of remote file uris
299
- if (!uris || !uris.length) {
300
- throw new Error(`Error in getting chunks from media helper for file ${file}`);
301
- }
302
- totalCount = uris.length + 1; // total number of chunks that will be processed
299
+ const useTS = WHISPER_TS_API_URL && (wordTimestamped || highlightWords);
303
300
 
304
- // sequential process of chunks
305
- for (const uri of uris) {
306
- sendProgress();
307
- const ts = await processURI(uri);
308
- result.push(ts);
309
- }
301
+ if (useTS) {
302
+ _promise = processTS;
303
+ } else {
304
+ _promise = processChunk;
305
+ }
310
306
 
311
- } catch (error) {
312
- const errMsg = `Transcribe error: ${error?.response?.data || error?.message || error}`;
313
- logger.error(errMsg);
314
- return errMsg;
307
+ await _promise(uri).then((ts) => {
308
+ result = ts;
309
+ }).catch((err) => {
310
+ errorOccurred = err;
311
+ }).finally(() => {
312
+ clearInterval(intervalId);
313
+ sendProgress();
314
+ });
315
+
316
+ if(errorOccurred) {
317
+ throw errorOccurred;
318
+ }
319
+
320
+ return result;
321
+ }
322
+
323
+ let offsets = [];
324
+ let uris = []
325
+
326
+ try {
327
+ const mediaChunks = await this.getMediaChunks(file, requestId);
328
+
329
+ if (!mediaChunks || !mediaChunks.length) {
330
+ throw new Error(`Error in getting chunks from media helper for file ${file}`);
331
+ }
332
+
333
+ uris = mediaChunks.map((chunk) => chunk?.uri || chunk);
334
+ offsets = mediaChunks.map((chunk, index) => chunk?.offset || index * OFFSET_CHUNK);
335
+
336
+ totalCount = mediaChunks.length + 1; // total number of chunks that will be processed
337
+
338
+ const batchSize = 2;
339
+ sendProgress();
340
+
341
+ for (let i = 0; i < uris.length; i += batchSize) {
342
+ const currentBatchURIs = uris.slice(i, i + batchSize);
343
+ const promisesToProcess = currentBatchURIs.map(uri => processURI(uri));
344
+ const results = await Promise.all(promisesToProcess);
345
+
346
+ for(const res of results) {
347
+ result.push(res);
315
348
  }
349
+ }
350
+
351
+ } catch (error) {
352
+ const errMsg = `Transcribe error: ${error?.response?.data || error?.message || error}`;
353
+ logger.error(errMsg);
354
+ return errMsg;
355
+ }
316
356
  finally {
317
357
  try {
318
358
  for (const chunk of chunks) {
@@ -340,7 +380,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
340
380
  }
341
381
 
342
382
  if (['srt','vtt'].includes(responseFormat) || wordTimestamped) { // align subtitles for formats
343
- return alignSubtitles(result, responseFormat);
383
+ return alignSubtitles(result, responseFormat, offsets);
344
384
  }
345
385
  return result.join(` `);
346
386
  }