@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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
};
|
package/lib/requestExecutor.js
CHANGED
|
@@ -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
|
|
327
|
-
// could be
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
249
|
-
|
|
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
|
|
280
|
+
if(completedCount >= totalCount) return;
|
|
254
281
|
|
|
255
|
-
const progress = (
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
291
|
-
throw errorOccurred;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return result;
|
|
295
|
-
}
|
|
297
|
+
const intervalId = setInterval(() => sendProgress(true), 3000);
|
|
296
298
|
|
|
297
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
301
|
+
if (useTS) {
|
|
302
|
+
_promise = processTS;
|
|
303
|
+
} else {
|
|
304
|
+
_promise = processChunk;
|
|
305
|
+
}
|
|
310
306
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
}
|