@aj-archipelago/cortex 0.0.6 → 0.0.7
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/README.md +108 -72
- package/config.js +20 -0
- package/graphql/graphql.js +53 -9
- package/graphql/pathwayPrompter.js +10 -6
- package/graphql/pathwayResolver.js +27 -36
- package/graphql/plugins/azureTranslatePlugin.js +16 -8
- package/graphql/plugins/modelPlugin.js +54 -22
- package/graphql/plugins/openAiChatPlugin.js +33 -9
- package/graphql/plugins/openAiCompletionPlugin.js +53 -37
- package/graphql/plugins/openAiWhisperPlugin.js +79 -0
- package/graphql/prompt.js +1 -0
- package/graphql/resolver.js +5 -5
- package/graphql/typeDef.js +47 -38
- package/lib/fileChunker.js +152 -0
- package/package.json +5 -2
- package/pathways/bias.js +6 -0
- package/pathways/chat.js +4 -1
- package/pathways/complete.js +4 -0
- package/pathways/edit.js +6 -0
- package/pathways/entities.js +12 -0
- package/pathways/index.js +1 -1
- package/pathways/paraphrase.js +4 -0
- package/pathways/sentiment.js +5 -1
- package/pathways/summary.js +25 -8
- package/pathways/transcribe.js +8 -0
- package/pathways/translate.js +10 -2
- package/tests/main.test.js +0 -13
- package/pathways/topics.js +0 -9
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const ffmpeg = require('fluent-ffmpeg');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const util = require('util');
|
|
7
|
+
const ffmpegProbe = util.promisify(ffmpeg.ffprobe);
|
|
8
|
+
const pipeline = util.promisify(require('stream').pipeline);
|
|
9
|
+
const ytdl = require('ytdl-core');
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async function processChunk(inputPath, outputFileName, start, duration) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
ffmpeg(inputPath)
|
|
15
|
+
.seekInput(start)
|
|
16
|
+
.duration(duration)
|
|
17
|
+
.on('start', (cmd) => {
|
|
18
|
+
console.log(`Started FFmpeg with command: ${cmd}`);
|
|
19
|
+
})
|
|
20
|
+
.on('error', (err) => {
|
|
21
|
+
console.error(`Error occurred while processing chunk:`, err);
|
|
22
|
+
reject(err);
|
|
23
|
+
})
|
|
24
|
+
.on('end', () => {
|
|
25
|
+
console.log(`Finished processing chunk`);
|
|
26
|
+
resolve(outputFileName);
|
|
27
|
+
})
|
|
28
|
+
.save(outputFileName);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const generateUniqueFolderName = () => {
|
|
33
|
+
const uniqueFolderName = uuidv4();
|
|
34
|
+
const tempFolderPath = os.tmpdir(); // Get the system's temporary folder
|
|
35
|
+
const uniqueOutputPath = path.join(tempFolderPath, uniqueFolderName);
|
|
36
|
+
return uniqueOutputPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const generateUniqueTempFileName = () => {
|
|
40
|
+
return path.join(os.tmpdir(), uuidv4());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function splitMediaFile(inputPath, chunkDurationInSeconds = 600) {
|
|
44
|
+
try {
|
|
45
|
+
const metadata = await ffmpegProbe(inputPath);
|
|
46
|
+
const duration = metadata.format.duration;
|
|
47
|
+
const numChunks = Math.ceil((duration - 1) / chunkDurationInSeconds);
|
|
48
|
+
|
|
49
|
+
const chunkPromises = [];
|
|
50
|
+
|
|
51
|
+
const uniqueOutputPath = generateUniqueFolderName();
|
|
52
|
+
|
|
53
|
+
// Create unique folder
|
|
54
|
+
fs.mkdirSync(uniqueOutputPath, { recursive: true });
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < numChunks; i++) {
|
|
58
|
+
const outputFileName = path.join(
|
|
59
|
+
uniqueOutputPath,
|
|
60
|
+
`chunk-${i + 1}-${path.basename(inputPath)}`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const chunkPromise = processChunk(
|
|
64
|
+
inputPath,
|
|
65
|
+
outputFileName,
|
|
66
|
+
i * chunkDurationInSeconds,
|
|
67
|
+
chunkDurationInSeconds
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
chunkPromises.push(chunkPromise);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const chunkedFiles = await Promise.all(chunkPromises);
|
|
74
|
+
console.log('All chunks processed. Chunked file names:', chunkedFiles);
|
|
75
|
+
return { chunks: chunkedFiles, folder: uniqueOutputPath }
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Error occurred during the splitting process:', err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function deleteTempPath(path) {
|
|
82
|
+
try {
|
|
83
|
+
if (!path) return;
|
|
84
|
+
const stats = fs.statSync(path);
|
|
85
|
+
if (stats.isFile()) {
|
|
86
|
+
fs.unlinkSync(path);
|
|
87
|
+
console.log(`Temporary file ${path} deleted successfully.`);
|
|
88
|
+
} else if (stats.isDirectory()) {
|
|
89
|
+
fs.rmdirSync(path, { recursive: true });
|
|
90
|
+
console.log(`Temporary folder ${path} and its contents deleted successfully.`);
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error('Error occurred while deleting the temporary path:', err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
function isValidYoutubeUrl(url) {
|
|
99
|
+
const regex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/;
|
|
100
|
+
return regex.test(url);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function convertYoutubeToMp3Stream(video) {
|
|
104
|
+
// Configure ffmpeg to convert the video to mp3
|
|
105
|
+
const mp3Stream = ffmpeg(video)
|
|
106
|
+
.withAudioCodec('libmp3lame')
|
|
107
|
+
.toFormat('mp3')
|
|
108
|
+
.on('error', (err) => {
|
|
109
|
+
console.error(`An error occurred during conversion: ${err.message}`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return mp3Stream;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function pipeStreamToFile(stream, filePath) {
|
|
116
|
+
try {
|
|
117
|
+
await pipeline(stream, fs.createWriteStream(filePath));
|
|
118
|
+
console.log('Stream piped to file successfully.');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`Error piping stream to file: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
const processYoutubeUrl = async (url) => {
|
|
126
|
+
const info = await ytdl.getInfo(url);
|
|
127
|
+
const audioFormat = ytdl.chooseFormat(info.formats, { quality: 'highestaudio' });
|
|
128
|
+
|
|
129
|
+
if (!audioFormat) {
|
|
130
|
+
throw new Error('No suitable audio format found');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const stream = ytdl.downloadFromInfo(info, { format: audioFormat });
|
|
134
|
+
|
|
135
|
+
const mp3Stream = convertYoutubeToMp3Stream(stream);
|
|
136
|
+
const outputFileName = path.join(os.tmpdir(), `${uuidv4()}.mp3`);
|
|
137
|
+
await pipeStreamToFile(mp3Stream, outputFileName); // You can also pipe the stream to a file
|
|
138
|
+
return outputFileName;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function deleteFile(filePath) {
|
|
142
|
+
try {
|
|
143
|
+
fs.unlinkSync(filePath);
|
|
144
|
+
console.log(`File ${filePath} cleaned successfully.`);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`Error deleting file ${filePath}:`, error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
splitMediaFile, deleteTempPath, processYoutubeUrl, isValidYoutubeUrl
|
|
152
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,13 +40,16 @@
|
|
|
40
40
|
"compromise": "^14.8.1",
|
|
41
41
|
"compromise-paragraphs": "^0.1.0",
|
|
42
42
|
"convict": "^6.2.3",
|
|
43
|
+
"fluent-ffmpeg": "^2.1.2",
|
|
44
|
+
"form-data": "^4.0.0",
|
|
43
45
|
"gpt-3-encoder": "^1.1.4",
|
|
44
46
|
"graphql": "^16.6.0",
|
|
45
47
|
"graphql-subscriptions": "^2.0.0",
|
|
46
48
|
"graphql-ws": "^5.11.2",
|
|
47
49
|
"handlebars": "^4.7.7",
|
|
48
50
|
"keyv": "^4.5.2",
|
|
49
|
-
"ws": "^8.12.0"
|
|
51
|
+
"ws": "^8.12.0",
|
|
52
|
+
"ytdl-core": "^4.11.2"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"dotenv": "^16.0.3",
|
package/pathways/bias.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
// bias.js
|
|
2
|
+
// Objectivity analysis of text
|
|
3
|
+
// This module exports a prompt that analyzes the given text and determines if it's written objectively. It also provides a detailed explanation of the decision.
|
|
4
|
+
|
|
1
5
|
module.exports = {
|
|
6
|
+
// Uncomment the following line to enable caching for this prompt, if desired.
|
|
2
7
|
// enableCache: true,
|
|
8
|
+
|
|
3
9
|
prompt: `{{text}}\n\nIs the above text written objectively? Why or why not, explain with details:\n`
|
|
4
10
|
}
|
package/pathways/chat.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// chat.js
|
|
2
|
+
// Simple context-aware chat bot
|
|
3
|
+
// This is a two prompt implementation of a context aware chat bot. The first prompt generates content that will be stored in the previousResult variable and will be returned to the client. In the optimum implementation, the client will then update their chatContext variable for the next call. The second prompt actually responds to the user. The second prompt *could* use previousResult instead of chatContext, but in this situation previousResult will also include the current turn of the conversation to which it is responding. That can get a little confusing as it tends to overemphasize the current turn in the response.
|
|
4
|
+
|
|
2
5
|
module.exports = {
|
|
3
6
|
prompt:
|
|
4
7
|
[
|
package/pathways/complete.js
CHANGED
package/pathways/edit.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
// edit.js
|
|
2
|
+
// Grammar and spelling correction module
|
|
3
|
+
// This module exports a prompt that takes an input text and corrects all spelling and grammar errors found within the text.
|
|
4
|
+
|
|
1
5
|
module.exports = {
|
|
6
|
+
// Set the temperature to 0 to favor more deterministic output when generating corrections.
|
|
2
7
|
temperature: 0,
|
|
8
|
+
|
|
3
9
|
prompt: `Correct all spelling and grammar errors in the input text.\n\nInput:\n{{text}}\n\nOutput:\n`
|
|
4
10
|
}
|
package/pathways/entities.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
// entities.js
|
|
2
|
+
// Entity extraction module
|
|
3
|
+
// This module exports a prompt that takes an input text and extracts the top entities and their definitions as specified by the count parameter.
|
|
4
|
+
|
|
1
5
|
module.exports = {
|
|
6
|
+
// Set the temperature to 0 to favor more deterministic output when generating entity extraction.
|
|
2
7
|
temperature: 0,
|
|
8
|
+
|
|
3
9
|
prompt: `{{text}}\n\nList the top {{count}} entities and their definitions for the above in the format {{format}}:`,
|
|
10
|
+
|
|
11
|
+
// Define the format for displaying the extracted entities and their definitions.
|
|
4
12
|
format: `(name: definition)`,
|
|
13
|
+
|
|
14
|
+
// Define input parameters for the prompt, such as the number of entities to extract.
|
|
5
15
|
inputParameters: {
|
|
6
16
|
count: 5,
|
|
7
17
|
},
|
|
18
|
+
|
|
19
|
+
// Set the list option to true as the prompt is expected to return a list of entities.
|
|
8
20
|
list: true,
|
|
9
21
|
}
|
package/pathways/index.js
CHANGED
package/pathways/paraphrase.js
CHANGED
package/pathways/sentiment.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// sentiment.js
|
|
2
|
+
// Sentiment detection module
|
|
3
|
+
// This module exports a prompt that takes an input text and asks how it makes the AI feel.
|
|
4
|
+
|
|
1
5
|
module.exports = {
|
|
2
|
-
prompt: `How does
|
|
6
|
+
prompt: `How does the text below make you feel?\n\n{{text}}`,
|
|
3
7
|
}
|
package/pathways/summary.js
CHANGED
|
@@ -1,43 +1,60 @@
|
|
|
1
|
+
// summary.js
|
|
2
|
+
// Text summarization module with custom resolver
|
|
3
|
+
// This module exports a prompt that takes an input text and generates a summary using a custom resolver.
|
|
4
|
+
|
|
5
|
+
// Import required modules
|
|
1
6
|
const { semanticTruncate } = require('../graphql/chunker');
|
|
2
7
|
const { PathwayResolver } = require('../graphql/pathwayResolver');
|
|
3
8
|
|
|
4
9
|
module.exports = {
|
|
10
|
+
// The main prompt function that takes the input text and asks to generate a summary.
|
|
5
11
|
prompt: `{{{text}}}\n\nWrite a summary of the above text:\n\n`,
|
|
6
12
|
|
|
13
|
+
// Define input parameters for the prompt, such as the target length of the summary.
|
|
7
14
|
inputParameters: {
|
|
8
|
-
targetLength:
|
|
15
|
+
targetLength: 0,
|
|
9
16
|
},
|
|
17
|
+
|
|
18
|
+
// Custom resolver to generate summaries by reprompting if they are too long or too short.
|
|
10
19
|
resolver: async (parent, args, contextValue, info) => {
|
|
11
20
|
const { config, pathway, requestState } = contextValue;
|
|
12
21
|
const originalTargetLength = args.targetLength;
|
|
22
|
+
|
|
23
|
+
// If targetLength is not provided, execute the prompt once and return the result.
|
|
24
|
+
if (originalTargetLength === 0) {
|
|
25
|
+
let pathwayResolver = new PathwayResolver({ config, pathway, args, requestState });
|
|
26
|
+
return await pathwayResolver.resolve(args);
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
const errorMargin = 0.2;
|
|
14
30
|
const lowTargetLength = originalTargetLength * (1 - errorMargin);
|
|
15
31
|
const targetWords = Math.round(originalTargetLength / 6.6);
|
|
16
32
|
|
|
17
|
-
//
|
|
33
|
+
// If the text is shorter than the summary length, just return the text.
|
|
18
34
|
if (args.text.length <= originalTargetLength) {
|
|
19
35
|
return args.text;
|
|
20
36
|
}
|
|
21
37
|
|
|
22
38
|
const MAX_ITERATIONS = 5;
|
|
23
39
|
let summary = '';
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
let pathwayResolver = new PathwayResolver({ config, pathway, args, requestState });
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
// Modify the prompt to be words-based instead of characters-based.
|
|
27
44
|
pathwayResolver.pathwayPrompt = `{{{text}}}\n\nWrite a summary of the above text in exactly ${targetWords} words:\n\n`
|
|
28
45
|
|
|
29
46
|
let i = 0;
|
|
30
|
-
//
|
|
47
|
+
// Reprompt if summary is too long or too short.
|
|
31
48
|
while (((summary.length > originalTargetLength) || (summary.length < lowTargetLength)) && i < MAX_ITERATIONS) {
|
|
32
49
|
summary = await pathwayResolver.resolve(args);
|
|
33
50
|
i++;
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
//
|
|
53
|
+
// If the summary is still too long, truncate it.
|
|
37
54
|
if (summary.length > originalTargetLength) {
|
|
38
55
|
return semanticTruncate(summary, originalTargetLength);
|
|
39
56
|
} else {
|
|
40
57
|
return summary;
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
|
-
}
|
|
60
|
+
}
|
package/pathways/translate.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
//
|
|
1
|
+
// translate.js
|
|
2
|
+
// Translation module
|
|
3
|
+
// This module exports a prompt that takes an input text and translates it from one language to another.
|
|
2
4
|
|
|
3
5
|
module.exports = {
|
|
6
|
+
// Set the temperature to 0 to favor more deterministic output when generating translations.
|
|
4
7
|
temperature: 0,
|
|
8
|
+
|
|
5
9
|
prompt: `Translate the following text to {{to}}:\n\nOriginal Language:\n{{{text}}}\n\n{{to}}:\n`,
|
|
10
|
+
|
|
11
|
+
// Define input parameters for the prompt, such as the target language for translation.
|
|
6
12
|
inputParameters: {
|
|
7
13
|
to: `Arabic`,
|
|
8
14
|
},
|
|
9
|
-
|
|
15
|
+
|
|
16
|
+
// Set the timeout for the translation process, in seconds.
|
|
17
|
+
timeout: 300,
|
|
10
18
|
}
|
package/tests/main.test.js
CHANGED
|
@@ -93,19 +93,6 @@ it('validates summary endpoint', async () => {
|
|
|
93
93
|
expect(response.data?.summary.result).toBeDefined();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it('validates topics endpoint with given num of count return', async () => {
|
|
97
|
-
const response = await testServer.executeOperation({
|
|
98
|
-
query: 'query topics($text: String!, $count: Int) { topics(text: $text, count: $count) { result } }',
|
|
99
|
-
variables: { text: 'hello there my dear world!', count: 3 },
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(response.errors).toBeUndefined();
|
|
103
|
-
expect(response.data?.topics.result.length).toBe(3);
|
|
104
|
-
response.data?.topics.result.forEach((topic) => {
|
|
105
|
-
expect(topic).toBeDefined();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
96
|
module.exports = {
|
|
110
97
|
getTestServer,
|
|
111
98
|
};
|
package/pathways/topics.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
prompt: [`{{text}}\n\nList the top {{count}} news categories for the above article (e.g. 1. Finance):`,
|
|
3
|
-
`{{previousResult}}\n\nPick the {{count}} most important news categories from the above:`
|
|
4
|
-
],
|
|
5
|
-
inputParameters: {
|
|
6
|
-
count: 5,
|
|
7
|
-
},
|
|
8
|
-
list: true,
|
|
9
|
-
}
|