@aj-archipelago/cortex 1.1.13 → 1.1.15
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/lib/cortexRequest.js +12 -2
- package/package.json +1 -1
- package/server/plugins/claude3VertexPlugin.js +1 -1
- package/server/plugins/modelPlugin.js +3 -1
- package/server/plugins/openAiDallE3Plugin.js +6 -2
- package/server/plugins/openAiWhisperPlugin.js +1 -1
- package/server/plugins/openAiWhisperPlugin_parallel.js +0 -358
package/lib/cortexRequest.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { selectEndpoint } from './requestExecutor.js';
|
|
2
2
|
|
|
3
3
|
class CortexRequest {
|
|
4
|
-
constructor( { url, data, params, headers, cache, model, pathwayResolver, selectedEndpoint, stream } = {}) {
|
|
4
|
+
constructor( { url, urlSuffix, data, params, headers, cache, model, pathwayResolver, selectedEndpoint, stream } = {}) {
|
|
5
5
|
this._url = url || '';
|
|
6
|
+
this._urlSuffix = urlSuffix || '';
|
|
6
7
|
this._data = data || {};
|
|
7
8
|
this._params = params || {};
|
|
8
9
|
this._headers = headers || {};
|
|
@@ -35,13 +36,22 @@ class CortexRequest {
|
|
|
35
36
|
|
|
36
37
|
// url getter and setter
|
|
37
38
|
get url() {
|
|
38
|
-
return this._url;
|
|
39
|
+
return this._url + this._urlSuffix;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
set url(value) {
|
|
42
43
|
this._url = value;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// urlSuffix getter and setter
|
|
47
|
+
get urlSuffix() {
|
|
48
|
+
return this._urlSuffix;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
set urlSuffix(value) {
|
|
52
|
+
this._urlSuffix = value;
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
// method getter and setter
|
|
46
56
|
get method() {
|
|
47
57
|
return this._method;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
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": {
|
|
@@ -79,7 +79,7 @@ class Claude3VertexPlugin extends OpenAIVisionPlugin {
|
|
|
79
79
|
cortexRequest.data = { ...(cortexRequest.data || {}), ...requestParameters };
|
|
80
80
|
cortexRequest.params = {}; // query params
|
|
81
81
|
cortexRequest.stream = stream;
|
|
82
|
-
cortexRequest.
|
|
82
|
+
cortexRequest.urlSuffix = cortexRequest.stream ? ':streamRawPredict' : ':rawPredict';
|
|
83
83
|
|
|
84
84
|
const gcpAuthTokenHelper = this.config.get('gcpAuthTokenHelper');
|
|
85
85
|
const authToken = await gcpAuthTokenHelper.getAccessToken();
|
|
@@ -276,7 +276,9 @@ class ModelPlugin {
|
|
|
276
276
|
|
|
277
277
|
const errorData = Array.isArray(responseData) ? responseData[0] : responseData;
|
|
278
278
|
if (errorData && errorData.error) {
|
|
279
|
-
|
|
279
|
+
const newError = new Error(errorData.error.message);
|
|
280
|
+
newError.data = errorData;
|
|
281
|
+
throw newError;
|
|
280
282
|
}
|
|
281
283
|
|
|
282
284
|
this.logAIRequestFinished(requestDuration);
|
|
@@ -55,8 +55,12 @@ class OpenAIDallE3Plugin extends ModelPlugin {
|
|
|
55
55
|
.catch((error) => handleResponse(error));
|
|
56
56
|
|
|
57
57
|
function handleResponse(response) {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
let status = "succeeded";
|
|
59
|
+
let data = JSON.stringify(response);
|
|
60
|
+
if (response.data.error) {
|
|
61
|
+
status = "failed";
|
|
62
|
+
data = JSON.stringify(response.data);
|
|
63
|
+
}
|
|
60
64
|
|
|
61
65
|
const requestProgress = {
|
|
62
66
|
requestId,
|
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
// openAiWhisperPlugin.js
|
|
2
|
-
import ModelPlugin from './modelPlugin.js';
|
|
3
|
-
import { config } from '../../config.js';
|
|
4
|
-
import subsrt from 'subsrt';
|
|
5
|
-
import FormData from 'form-data';
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import { axios } from '../../lib/requestExecutor.js';
|
|
8
|
-
import stream from 'stream';
|
|
9
|
-
import os from 'os';
|
|
10
|
-
import path from 'path';
|
|
11
|
-
import http from 'http';
|
|
12
|
-
import https from 'https';
|
|
13
|
-
import { URL } from 'url';
|
|
14
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
15
|
-
import { promisify } from 'util';
|
|
16
|
-
import { publishRequestProgress } from '../../lib/redisSubscription.js';
|
|
17
|
-
import logger from '../../lib/logger.js';
|
|
18
|
-
const pipeline = promisify(stream.pipeline);
|
|
19
|
-
|
|
20
|
-
const API_URL = config.get('whisperMediaApiUrl');
|
|
21
|
-
const WHISPER_TS_API_URL = config.get('whisperTSApiUrl');
|
|
22
|
-
if(WHISPER_TS_API_URL){
|
|
23
|
-
logger.info(`WHISPER API URL using ${WHISPER_TS_API_URL}`);
|
|
24
|
-
}else{
|
|
25
|
-
logger.warn(`WHISPER API URL not set using default OpenAI API Whisper`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const OFFSET_CHUNK = 1000 * 60 * 10; // 10 minutes for each chunk
|
|
29
|
-
|
|
30
|
-
async function deleteTempPath(path) {
|
|
31
|
-
try {
|
|
32
|
-
if (!path) {
|
|
33
|
-
logger.warn('Temporary path is not defined.');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (!fs.existsSync(path)) {
|
|
37
|
-
logger.warn(`Temporary path ${path} does not exist.`);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const stats = fs.statSync(path);
|
|
41
|
-
if (stats.isFile()) {
|
|
42
|
-
fs.unlinkSync(path);
|
|
43
|
-
logger.info(`Temporary file ${path} deleted successfully.`);
|
|
44
|
-
} else if (stats.isDirectory()) {
|
|
45
|
-
fs.rmSync(path, { recursive: true });
|
|
46
|
-
logger.info(`Temporary folder ${path} and its contents deleted successfully.`);
|
|
47
|
-
}
|
|
48
|
-
} catch (err) {
|
|
49
|
-
logger.error(`Error occurred while deleting the temporary path: ${err}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function generateUniqueFilename(extension) {
|
|
54
|
-
return `${uuidv4()}.${extension}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const downloadFile = async (fileUrl) => {
|
|
58
|
-
const fileExtension = path.extname(fileUrl).slice(1);
|
|
59
|
-
const uniqueFilename = generateUniqueFilename(fileExtension);
|
|
60
|
-
const tempDir = os.tmpdir();
|
|
61
|
-
const localFilePath = `${tempDir}/${uniqueFilename}`;
|
|
62
|
-
|
|
63
|
-
// eslint-disable-next-line no-async-promise-executor
|
|
64
|
-
return new Promise(async (resolve, reject) => {
|
|
65
|
-
try {
|
|
66
|
-
const parsedUrl = new URL(fileUrl);
|
|
67
|
-
const protocol = parsedUrl.protocol === 'https:' ? https : http;
|
|
68
|
-
|
|
69
|
-
const response = await new Promise((resolve, reject) => {
|
|
70
|
-
protocol.get(parsedUrl, (res) => {
|
|
71
|
-
if (res.statusCode === 200) {
|
|
72
|
-
resolve(res);
|
|
73
|
-
} else {
|
|
74
|
-
reject(new Error(`HTTP request failed with status code ${res.statusCode}`));
|
|
75
|
-
}
|
|
76
|
-
}).on('error', reject);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
await pipeline(response, fs.createWriteStream(localFilePath));
|
|
80
|
-
logger.info(`Downloaded file to ${localFilePath}`);
|
|
81
|
-
resolve(localFilePath);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
fs.unlink(localFilePath, () => {
|
|
84
|
-
reject(error);
|
|
85
|
-
});
|
|
86
|
-
//throw error;
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// convert srt format to text
|
|
92
|
-
function convertToText(str) {
|
|
93
|
-
return str
|
|
94
|
-
.split('\n')
|
|
95
|
-
.filter(line => !line.match(/^\d+$/) && !line.match(/^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}$/) && line !== '')
|
|
96
|
-
.join(' ');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function alignSubtitles(subtitles, format) {
|
|
100
|
-
const result = [];
|
|
101
|
-
|
|
102
|
-
function preprocessStr(str) {
|
|
103
|
-
try{
|
|
104
|
-
if(!str) return '';
|
|
105
|
-
return str.trim().replace(/(\n\n)(?!\n)/g, '\n\n\n');
|
|
106
|
-
}catch(e){
|
|
107
|
-
logger.error(`An error occurred in content text preprocessing: ${e}`);
|
|
108
|
-
return '';
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function shiftSubtitles(subtitle, shiftOffset) {
|
|
113
|
-
const captions = subsrt.parse(preprocessStr(subtitle));
|
|
114
|
-
const resynced = subsrt.resync(captions, { offset: shiftOffset });
|
|
115
|
-
return resynced;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
for (let i = 0; i < subtitles.length; i++) {
|
|
119
|
-
result.push(...shiftSubtitles(subtitles[i], i * OFFSET_CHUNK));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
//if content has needed html style tags, keep them
|
|
124
|
-
for(const obj of result) {
|
|
125
|
-
if(obj && obj.content){
|
|
126
|
-
obj.text = obj.content;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
} catch (error) {
|
|
130
|
-
logger.error(`An error occurred in content text parsing: ${error}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return subsrt.build(result, { format: format === 'vtt' ? 'vtt' : 'srt' });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class OpenAIWhisperPlugin extends ModelPlugin {
|
|
138
|
-
constructor(pathway, model) {
|
|
139
|
-
super(pathway, model);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async getMediaChunks(file, requestId) {
|
|
143
|
-
try {
|
|
144
|
-
if (API_URL) {
|
|
145
|
-
//call helper api and get list of file uris
|
|
146
|
-
const res = await axios.get(API_URL, { params: { uri: file, requestId } });
|
|
147
|
-
return res.data;
|
|
148
|
-
} else {
|
|
149
|
-
logger.info(`No API_URL set, returning file as chunk`);
|
|
150
|
-
return [file];
|
|
151
|
-
}
|
|
152
|
-
} catch (err) {
|
|
153
|
-
logger.error(`Error getting media chunks list from api: ${err}`);
|
|
154
|
-
throw err;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async markCompletedForCleanUp(requestId) {
|
|
159
|
-
try {
|
|
160
|
-
if (API_URL) {
|
|
161
|
-
//call helper api to mark processing as completed
|
|
162
|
-
const res = await axios.delete(API_URL, { params: { requestId } });
|
|
163
|
-
logger.info(`Marked request ${requestId} as completed:`, res.data);
|
|
164
|
-
return res.data;
|
|
165
|
-
}
|
|
166
|
-
} catch (err) {
|
|
167
|
-
logger.error(`Error marking request ${requestId} as completed: ${err}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Execute the request to the OpenAI Whisper API
|
|
172
|
-
async execute(text, parameters, prompt, cortexRequest) {
|
|
173
|
-
const { pathwayResolver } = cortexRequest;
|
|
174
|
-
const { responseFormat, wordTimestamped, highlightWords, maxLineWidth, maxLineCount, maxWordsPerLine } = parameters;
|
|
175
|
-
cortexRequest.url = this.requestUrl(text);
|
|
176
|
-
|
|
177
|
-
const chunks = [];
|
|
178
|
-
const processChunk = async (uri) => {
|
|
179
|
-
try {
|
|
180
|
-
const chunk = await downloadFile(uri);
|
|
181
|
-
chunks.push(chunk);
|
|
182
|
-
|
|
183
|
-
const { language, responseFormat } = parameters;
|
|
184
|
-
cortexRequest.url = this.requestUrl(text);
|
|
185
|
-
const params = {};
|
|
186
|
-
const { modelPromptText } = this.getCompiledPrompt(text, parameters, prompt);
|
|
187
|
-
const response_format = responseFormat || 'text';
|
|
188
|
-
|
|
189
|
-
const formData = new FormData();
|
|
190
|
-
formData.append('file', fs.createReadStream(chunk));
|
|
191
|
-
formData.append('model', this.model.params.model);
|
|
192
|
-
formData.append('response_format', response_format);
|
|
193
|
-
language && formData.append('language', language);
|
|
194
|
-
modelPromptText && formData.append('prompt', modelPromptText);
|
|
195
|
-
|
|
196
|
-
cortexRequest.data = formData;
|
|
197
|
-
cortexRequest.params = params;
|
|
198
|
-
cortexRequest.headers = { ...cortexRequest.headers, ...formData.getHeaders() };
|
|
199
|
-
|
|
200
|
-
return this.executeRequest(cortexRequest);
|
|
201
|
-
} catch (err) {
|
|
202
|
-
logger.error(`Error getting word timestamped data from api: ${err}`);
|
|
203
|
-
throw err;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const processTS = async (uri) => {
|
|
208
|
-
try {
|
|
209
|
-
const tsparams = { fileurl:uri };
|
|
210
|
-
|
|
211
|
-
const { language } = parameters;
|
|
212
|
-
if(language) tsparams.language = language;
|
|
213
|
-
if(highlightWords) tsparams.highlight_words = highlightWords ? "True" : "False";
|
|
214
|
-
if(maxLineWidth) tsparams.max_line_width = maxLineWidth;
|
|
215
|
-
if(maxLineCount) tsparams.max_line_count = maxLineCount;
|
|
216
|
-
if(maxWordsPerLine) tsparams.max_words_per_line = maxWordsPerLine;
|
|
217
|
-
if(wordTimestamped!=null) {
|
|
218
|
-
if(!wordTimestamped) {
|
|
219
|
-
tsparams.word_timestamps = "False";
|
|
220
|
-
}else{
|
|
221
|
-
tsparams.word_timestamps = wordTimestamped;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
cortexRequest.url = WHISPER_TS_API_URL;
|
|
226
|
-
cortexRequest.data = tsparams;
|
|
227
|
-
|
|
228
|
-
const res = await this.executeRequest(cortexRequest);
|
|
229
|
-
|
|
230
|
-
if(!wordTimestamped && !responseFormat){
|
|
231
|
-
//if no response format, convert to text
|
|
232
|
-
return convertToText(res);
|
|
233
|
-
}
|
|
234
|
-
return res;
|
|
235
|
-
} catch (err) {
|
|
236
|
-
logger.error(`Error getting word timestamped data from api: ${err}`);
|
|
237
|
-
throw err;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
let result = [];
|
|
242
|
-
let { file } = parameters;
|
|
243
|
-
let totalCount = 0;
|
|
244
|
-
let completedCount = 0;
|
|
245
|
-
let partialCount = 0;
|
|
246
|
-
const { requestId } = pathwayResolver;
|
|
247
|
-
|
|
248
|
-
const MAXPARTIALCOUNT = 60;
|
|
249
|
-
const sendProgress = (partial=false) => {
|
|
250
|
-
if(partial){
|
|
251
|
-
partialCount = Math.min(partialCount + 1, MAXPARTIALCOUNT-1);
|
|
252
|
-
}else {
|
|
253
|
-
partialCount = 0;
|
|
254
|
-
completedCount++;
|
|
255
|
-
}
|
|
256
|
-
if (completedCount >= totalCount) return;
|
|
257
|
-
|
|
258
|
-
const progress = (partialCount / MAXPARTIALCOUNT + completedCount) / totalCount;
|
|
259
|
-
logger.info(`Progress for ${requestId}: ${progress}`);
|
|
260
|
-
|
|
261
|
-
publishRequestProgress({
|
|
262
|
-
requestId,
|
|
263
|
-
progress,
|
|
264
|
-
data: null,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async function processInBatches(tasks, batchSize) {
|
|
269
|
-
const batches = chunkArray(tasks,batchSize);
|
|
270
|
-
const result = [];
|
|
271
|
-
for(let i=0; i < batches.length; i++){
|
|
272
|
-
let batch = batches[i];
|
|
273
|
-
// execute all tasks in current batch and wait for them to finish
|
|
274
|
-
const curBatchResults = await Promise.all(batch.map(task => task()));
|
|
275
|
-
// accumulate the results
|
|
276
|
-
result.push(...curBatchResults);
|
|
277
|
-
}
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// helper function to chunk an array into smaller arrays of size n
|
|
282
|
-
function chunkArray(array, chunkSize) {
|
|
283
|
-
const results = [];
|
|
284
|
-
while (array.length) {
|
|
285
|
-
results.push(array.splice(0, chunkSize));
|
|
286
|
-
}
|
|
287
|
-
return results;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function processURI(uri) {
|
|
291
|
-
let result = null;
|
|
292
|
-
let _promise = null;
|
|
293
|
-
if(WHISPER_TS_API_URL){
|
|
294
|
-
_promise = processTS
|
|
295
|
-
}else {
|
|
296
|
-
_promise = processChunk;
|
|
297
|
-
}
|
|
298
|
-
_promise(uri).then((ts) => { result = ts;});
|
|
299
|
-
|
|
300
|
-
//send updates while waiting for result
|
|
301
|
-
while(!result) {
|
|
302
|
-
sendProgress(true);
|
|
303
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
304
|
-
}
|
|
305
|
-
return result;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
const uris = await this.getMediaChunks(file, requestId); // array of remote file uris
|
|
310
|
-
if (!uris || !uris.length) {
|
|
311
|
-
throw new Error(`Error in getting chunks from media helper for file ${file}`);
|
|
312
|
-
}
|
|
313
|
-
totalCount = uris.length + 1; // total number of chunks that will be processed
|
|
314
|
-
|
|
315
|
-
// parallel process of chunks with limit
|
|
316
|
-
const tasks = uris.map(uri => () => processURI(uri)); // map each uri to a function that returns a Promise
|
|
317
|
-
result = await processInBatches(tasks, 2); // wait for all Promises to resolve, 2 at a time
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
} catch (error) {
|
|
321
|
-
const errMsg = `Transcribe error: ${error?.response?.data || error?.message || error}`;
|
|
322
|
-
logger.error(errMsg);
|
|
323
|
-
return errMsg;
|
|
324
|
-
}
|
|
325
|
-
finally {
|
|
326
|
-
try {
|
|
327
|
-
for (const chunk of chunks) {
|
|
328
|
-
try {
|
|
329
|
-
await deleteTempPath(chunk);
|
|
330
|
-
} catch (error) {
|
|
331
|
-
//ignore error
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
await this.markCompletedForCleanUp(requestId);
|
|
336
|
-
|
|
337
|
-
//check cleanup for whisper temp uploaded files url
|
|
338
|
-
const regex = /whispertempfiles\/([a-z0-9-]+)/;
|
|
339
|
-
const match = file.match(regex);
|
|
340
|
-
if (match && match[1]) {
|
|
341
|
-
const extractedValue = match[1];
|
|
342
|
-
await this.markCompletedForCleanUp(extractedValue);
|
|
343
|
-
logger.info(`Cleaned temp whisper file ${file} with request id ${extractedValue}`);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
} catch (error) {
|
|
347
|
-
logger.error(`An error occurred while deleting: ${error}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (['srt','vtt'].includes(responseFormat) || wordTimestamped) { // align subtitles for formats
|
|
352
|
-
return alignSubtitles(result, responseFormat);
|
|
353
|
-
}
|
|
354
|
-
return result.join(` `);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export default OpenAIWhisperPlugin;
|