@aj-archipelago/cortex 1.3.28 → 1.3.29
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 +1 -1
- package/pathways/translate_subtitle.js +51 -24
- package/server/plugins/openAiWhisperPlugin.js +3 -8
- package/tests/subchunk.srt +1459 -0
- package/tests/translate_srt.test.js +386 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.29",
|
|
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": {
|
|
@@ -2,7 +2,7 @@ import { parse, build } from "@aj-archipelago/subvibe";
|
|
|
2
2
|
import logger from "../lib/logger.js";
|
|
3
3
|
import { callPathway } from "../lib/pathwayTools.js";
|
|
4
4
|
|
|
5
|
-
function splitIntoOverlappingChunks(captions, chunkSize = 20, overlap = 3) {
|
|
5
|
+
export function splitIntoOverlappingChunks(captions, chunkSize = 20, overlap = 3) {
|
|
6
6
|
const chunks = [];
|
|
7
7
|
for (let i = 0; i < captions.length; i += (chunkSize - overlap)) {
|
|
8
8
|
const end = Math.min(i + chunkSize, captions.length);
|
|
@@ -17,26 +17,51 @@ function splitIntoOverlappingChunks(captions, chunkSize = 20, overlap = 3) {
|
|
|
17
17
|
return chunks;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function selectBestTranslation(translations, startIndex, endIndex) {
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
export function selectBestTranslation(translations, startIndex, endIndex) {
|
|
21
|
+
try {
|
|
22
|
+
if (!translations || !Array.isArray(translations)) {
|
|
23
|
+
logger.warn(`Invalid translations input: ${JSON.stringify(translations)}`);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (translations.length === 0) {
|
|
28
|
+
logger.warn(`No translations available for selection`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If we only have one translation for this caption, use it
|
|
33
|
+
if (translations.length === 1) return translations[0];
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// Use the first translation as a starting point
|
|
36
|
+
const first = translations[0];
|
|
37
|
+
|
|
38
|
+
// For multiple translations, prefer the one whose identifier is closest to the middle
|
|
39
|
+
// of the requested range
|
|
40
|
+
const targetValue = (Number(startIndex) + Number(endIndex)) / 2;
|
|
41
|
+
|
|
42
|
+
return translations.reduce((best, current) => {
|
|
43
|
+
try {
|
|
44
|
+
// Use identifier for comparison if available, otherwise use index
|
|
45
|
+
const currentValue = Number(current.identifier !== undefined ? current.identifier : current.index || 0);
|
|
46
|
+
const bestValue = Number(best.identifier !== undefined ? best.identifier : best.index || 0);
|
|
47
|
+
|
|
48
|
+
const currentDistance = Math.abs(currentValue - targetValue);
|
|
49
|
+
const bestDistance = Math.abs(bestValue - targetValue);
|
|
50
|
+
|
|
51
|
+
return currentDistance < bestDistance ? current : best;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.warn(`Error comparing translations: ${err.message}`);
|
|
54
|
+
return best; // Fallback to existing best on error
|
|
55
|
+
}
|
|
56
|
+
}, first);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.error(`Error in selectBestTranslation: ${err.message}`);
|
|
59
|
+
// Return the first translation if available, otherwise null
|
|
60
|
+
return translations && translations.length ? translations[0] : null;
|
|
61
|
+
}
|
|
37
62
|
}
|
|
38
63
|
|
|
39
|
-
async function translateChunk(chunk, args, maxRetries = 3) {
|
|
64
|
+
export async function translateChunk(chunk, args, maxRetries = 3) {
|
|
40
65
|
const chunkText = build(chunk.captions, { format: args.format, preserveIndexes: true });
|
|
41
66
|
|
|
42
67
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
@@ -82,7 +107,7 @@ export default {
|
|
|
82
107
|
timeout: 3600,
|
|
83
108
|
executePathway: async ({args}) => {
|
|
84
109
|
try {
|
|
85
|
-
const { text, format = '
|
|
110
|
+
const { text, format = 'vtt' } = args;
|
|
86
111
|
const parsed = parse(text, { format, preserveIndexes: true });
|
|
87
112
|
const captions = parsed.cues;
|
|
88
113
|
|
|
@@ -101,16 +126,18 @@ export default {
|
|
|
101
126
|
// Create a map of caption index to all its translations
|
|
102
127
|
const translationMap = new Map();
|
|
103
128
|
translatedChunks.flat().forEach(caption => {
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
const identifier = caption.identifier || caption.index;
|
|
130
|
+
if (!translationMap.has(identifier)) {
|
|
131
|
+
translationMap.set(identifier, []);
|
|
106
132
|
}
|
|
107
|
-
translationMap.get(
|
|
133
|
+
translationMap.get(identifier).push(caption);
|
|
108
134
|
});
|
|
109
135
|
|
|
110
136
|
// Select best translation for each caption
|
|
111
137
|
const finalCaptions = captions.map(caption => {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
138
|
+
const identifier = caption.identifier || caption.index;
|
|
139
|
+
const translations = translationMap.get(identifier) || [caption];
|
|
140
|
+
const bestTranslation = selectBestTranslation(translations, identifier, identifier);
|
|
114
141
|
const text = bestTranslation?.text || caption?.text;
|
|
115
142
|
return { ...caption, text };
|
|
116
143
|
});
|
|
@@ -72,13 +72,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
72
72
|
if(maxLineWidth) tsparams.max_line_width = maxLineWidth;
|
|
73
73
|
if(maxLineCount) tsparams.max_line_count = maxLineCount;
|
|
74
74
|
if(maxWordsPerLine) tsparams.max_words_per_line = maxWordsPerLine;
|
|
75
|
-
|
|
76
|
-
if(!wordTimestamped) {
|
|
77
|
-
tsparams.word_timestamps = "False";
|
|
78
|
-
}else{
|
|
79
|
-
tsparams.word_timestamps = wordTimestamped;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
75
|
+
tsparams.word_timestamps = !wordTimestamped ? "False" : wordTimestamped;
|
|
82
76
|
|
|
83
77
|
const cortexRequest = new CortexRequest({ pathwayResolver });
|
|
84
78
|
cortexRequest.url = WHISPER_TS_API_URL;
|
|
@@ -157,7 +151,8 @@ async function processURI(uri) {
|
|
|
157
151
|
|
|
158
152
|
const intervalId = setInterval(() => sendProgress(true), 3000);
|
|
159
153
|
|
|
160
|
-
const useTS = WHISPER_TS_API_URL && (wordTimestamped || highlightWords);
|
|
154
|
+
//const useTS = WHISPER_TS_API_URL && (wordTimestamped || highlightWords); // use TS API only for word timestamped
|
|
155
|
+
const useTS = !!WHISPER_TS_API_URL; // use TS API always if URL is set
|
|
161
156
|
|
|
162
157
|
if (useTS) {
|
|
163
158
|
_promise = processTS;
|