mulmocast 2.7.0 → 2.7.1
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/actions/bundle.js +2 -1
- package/lib/actions/pdf.js +4 -1
- package/lib/agents/image_genai_agent.js +5 -2
- package/lib/agents/image_openai_agent.js +2 -1
- package/lib/agents/image_replicate_agent.js +2 -1
- package/lib/agents/lipsync_replicate_agent.js +2 -1
- package/lib/agents/media_mock_agent.js +2 -1
- package/lib/agents/movie_genai_agent.js +17 -3
- package/lib/agents/movie_replicate_agent.js +2 -1
- package/lib/agents/sound_effect_replicate_agent.js +6 -2
- package/lib/agents/tts_elevenlabs_agent.js +4 -3
- package/lib/agents/tts_gemini_agent.js +4 -1
- package/lib/agents/tts_kotodama_agent.js +7 -3
- package/lib/methods/mulmo_media_source.js +9 -13
- package/lib/utils/fetch.d.ts +28 -0
- package/lib/utils/fetch.js +52 -0
- package/lib/utils/file.js +2 -1
- package/lib/utils/image_plugins/bg_image_util.js +7 -20
- package/lib/utils/openai_client.js +6 -0
- package/lib/utils/replicate_usage.d.ts +4 -1
- package/lib/utils/replicate_usage.js +25 -5
- package/lib/utils/sdk_timeout.d.ts +3 -0
- package/lib/utils/sdk_timeout.js +7 -0
- package/lib/utils/zip.js +2 -2
- package/package.json +7 -7
package/lib/actions/bundle.js
CHANGED
|
@@ -7,8 +7,9 @@ import { ZipBuilder } from "../utils/zip.js";
|
|
|
7
7
|
import { bundleTargetLang } from "../types/const.js";
|
|
8
8
|
import { createSilentAudio } from "../utils/ffmpeg_utils.js";
|
|
9
9
|
import { silentMp3 } from "../utils/context.js";
|
|
10
|
+
import { safeFetch, FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
10
11
|
const downloadFile = async (url, destPath) => {
|
|
11
|
-
const response = await
|
|
12
|
+
const response = await safeFetch(url, {}, FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS);
|
|
12
13
|
if (!response.ok) {
|
|
13
14
|
throw new Error(`Failed to download file from ${url}: ${response.statusText}`);
|
|
14
15
|
}
|
package/lib/actions/pdf.js
CHANGED
|
@@ -6,6 +6,7 @@ import { MulmoPresentationStyleMethods } from "../methods/index.js";
|
|
|
6
6
|
import { localizedText, isHttp } from "../utils/utils.js";
|
|
7
7
|
import { getOutputPdfFilePath, writingMessage, getHTMLFile, mulmoCreditPath } from "../utils/file.js";
|
|
8
8
|
import { interpolate } from "../utils/html_render.js";
|
|
9
|
+
import { safeFetch, FETCH_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
9
10
|
import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
|
|
10
11
|
const isCI = process.env.CI === "true";
|
|
11
12
|
const getPdfSize = (pdfSize) => {
|
|
@@ -13,7 +14,9 @@ const getPdfSize = (pdfSize) => {
|
|
|
13
14
|
};
|
|
14
15
|
const loadImage = async (imagePath, index) => {
|
|
15
16
|
try {
|
|
16
|
-
const imageData = isHttp(imagePath)
|
|
17
|
+
const imageData = isHttp(imagePath)
|
|
18
|
+
? Buffer.from(await (await safeFetch(imagePath, {}, FETCH_DOWNLOAD_TIMEOUT_MS)).arrayBuffer())
|
|
19
|
+
: fs.readFileSync(imagePath);
|
|
17
20
|
const ext = path.extname(imagePath).toLowerCase().replace(".", "");
|
|
18
21
|
const mimeType = ext === "jpg" ? "jpeg" : ext;
|
|
19
22
|
return `data:image/${mimeType};base64,${imageData.toString("base64")}`;
|
|
@@ -5,6 +5,9 @@ import { apiKeyMissingError, agentIncorrectAPIKeyError, agentGenerationError, ag
|
|
|
5
5
|
import { getAspectRatio } from "../utils/utils.js";
|
|
6
6
|
import { ASPECT_RATIOS, PRO_ASPECT_RATIOS } from "../types/const.js";
|
|
7
7
|
import { GoogleGenAI, PersonGeneration } from "@google/genai";
|
|
8
|
+
// Per-request timeout so a stalled GenAI image call rejects (and GraphAI retry
|
|
9
|
+
// can recover) instead of hanging. Generous vs. normal generation time.
|
|
10
|
+
const GENAI_REQUEST_TIMEOUT_MS = 120_000;
|
|
8
11
|
const isDeprecatedGoogleImageModel = (model) => model in deprecatedGoogleImageModelHints;
|
|
9
12
|
export const buildDeprecatedGoogleImageModelMessage = (model) => {
|
|
10
13
|
if (!isDeprecatedGoogleImageModel(model))
|
|
@@ -86,7 +89,7 @@ export const imageGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
86
89
|
if (vertexAIGlobalOnlyImageModels.has(model) && location !== "global") {
|
|
87
90
|
GraphAILogger.warn(`imageGenAIAgent: model "${model}" on Vertex AI is only available in location "global", but got "${location}". Set imageParams.vertexai_location to "global".`);
|
|
88
91
|
}
|
|
89
|
-
return new GoogleGenAI({ vertexai: true, project: params.vertexai_project, location });
|
|
92
|
+
return new GoogleGenAI({ vertexai: true, project: params.vertexai_project, location, httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS } });
|
|
90
93
|
})()
|
|
91
94
|
: (() => {
|
|
92
95
|
if (!apiKey) {
|
|
@@ -94,7 +97,7 @@ export const imageGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
94
97
|
cause: apiKeyMissingError("imageGenAIAgent", imageAction, "GEMINI_API_KEY"),
|
|
95
98
|
});
|
|
96
99
|
}
|
|
97
|
-
return new GoogleGenAI({ apiKey });
|
|
100
|
+
return new GoogleGenAI({ apiKey, httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS } });
|
|
98
101
|
})();
|
|
99
102
|
if (model === "gemini-2.5-flash-image" || model === "gemini-3.1-flash-image-preview" || model === "gemini-3-pro-image-preview") {
|
|
100
103
|
const contentParams = (() => {
|
|
@@ -3,6 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { GraphAILogger } from "graphai";
|
|
4
4
|
import { toFile, AuthenticationError, RateLimitError, APIError } from "openai";
|
|
5
5
|
import { createOpenAIClient } from "../utils/openai_client.js";
|
|
6
|
+
import { safeFetch, FETCH_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
6
7
|
import { provider2ImageAgent, gptImages, deprecatedOpenAIImageModelHints } from "../types/provider2agent.js";
|
|
7
8
|
import { apiKeyMissingError, agentGenerationError, openAIAgentGenerationError, agentIncorrectAPIKeyError, agentAPIRateLimitError, agentInvalidResponseError, imageAction, imageFileTarget, unsupportedModelTarget, } from "../utils/error_cause.js";
|
|
8
9
|
const isDeprecatedOpenAIImageModel = (model) => model in deprecatedOpenAIImageModelHints;
|
|
@@ -142,7 +143,7 @@ export const imageOpenaiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
142
143
|
return { buffer: Buffer.from(image_base64, "base64"), usage };
|
|
143
144
|
}
|
|
144
145
|
// URL response handling (legacy OpenAI image API response format)
|
|
145
|
-
const res = await
|
|
146
|
+
const res = await safeFetch(url, {}, FETCH_DOWNLOAD_TIMEOUT_MS);
|
|
146
147
|
if (!res.ok) {
|
|
147
148
|
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`, {
|
|
148
149
|
cause: agentGenerationError("imageOpenaiAgent", imageAction, imageFileTarget),
|
|
@@ -5,6 +5,7 @@ import { getAspectRatio } from "./movie_replicate_agent.js";
|
|
|
5
5
|
import { apiKeyMissingError, agentIncorrectAPIKeyError, agentGenerationError, agentInvalidResponseError, imageAction, imageFileTarget, hasCause, } from "../utils/error_cause.js";
|
|
6
6
|
import { provider2ImageAgent } from "../types/provider2agent.js";
|
|
7
7
|
import { runReplicateWithMetrics } from "../utils/replicate_usage.js";
|
|
8
|
+
import { safeFetch, FETCH_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
8
9
|
// Replicate image models return one of: FileOutput (object with url() method),
|
|
9
10
|
// Array<FileOutput>, string URL, or { url: string }. Normalize to URL string/URL.
|
|
10
11
|
const extractImageUrl = (output) => {
|
|
@@ -52,7 +53,7 @@ export const imageReplicateAgent = async ({ namedInputs, params, config, }) => {
|
|
|
52
53
|
cause: agentInvalidResponseError("imageReplicateAgent", imageAction, imageFileTarget),
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
|
-
const imageResponse = await
|
|
56
|
+
const imageResponse = await safeFetch(imageUrl, {}, FETCH_DOWNLOAD_TIMEOUT_MS);
|
|
56
57
|
if (!imageResponse.ok) {
|
|
57
58
|
throw new Error(`Error downloading image: ${imageResponse.status} - ${imageResponse.statusText}`, {
|
|
58
59
|
cause: agentGenerationError("imageReplicateAgent", imageAction, imageFileTarget),
|
|
@@ -4,6 +4,7 @@ import Replicate from "replicate";
|
|
|
4
4
|
import { provider2LipSyncAgent } from "../types/provider2agent.js";
|
|
5
5
|
import { apiKeyMissingError, agentGenerationError, agentFileNotExistError, imageAction, movieFileTarget, audioFileTarget, hasCause, } from "../utils/error_cause.js";
|
|
6
6
|
import { runReplicateWithMetrics } from "../utils/replicate_usage.js";
|
|
7
|
+
import { safeFetch, FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
7
8
|
export const lipSyncReplicateAgent = async ({ namedInputs, params, config, }) => {
|
|
8
9
|
const { movieFile, audioFile, imageFile } = namedInputs;
|
|
9
10
|
const apiKey = config?.apiKey;
|
|
@@ -64,7 +65,7 @@ export const lipSyncReplicateAgent = async ({ namedInputs, params, config, }) =>
|
|
|
64
65
|
const { output, predictSec } = await runReplicateWithMetrics(replicate, model_identifier, input);
|
|
65
66
|
if (output && typeof output === "object" && "url" in output) {
|
|
66
67
|
const videoUrl = output.url();
|
|
67
|
-
const videoResponse = await
|
|
68
|
+
const videoResponse = await safeFetch(videoUrl, {}, FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS);
|
|
68
69
|
if (!videoResponse.ok) {
|
|
69
70
|
throw new Error(`Error downloading video: ${videoResponse.status} - ${videoResponse.statusText}`, {
|
|
70
71
|
cause: agentGenerationError("lipSyncReplicateAgent", imageAction, movieFileTarget),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GraphAILogger } from "graphai";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import { silent60secPath, mulmoCreditPath } from "../utils/file.js";
|
|
4
|
+
import { safeFetch, FETCH_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
4
5
|
export const mediaMockAgent = async ({ namedInputs }) => {
|
|
5
6
|
if (namedInputs.media === "audio") {
|
|
6
7
|
const buffer = fs.readFileSync(silent60secPath());
|
|
@@ -12,7 +13,7 @@ export const mediaMockAgent = async ({ namedInputs }) => {
|
|
|
12
13
|
}
|
|
13
14
|
if (namedInputs.media === "movie") {
|
|
14
15
|
const url = "https://github.com/receptron/mulmocast-media/raw/refs/heads/main/test/pingpong.mov";
|
|
15
|
-
const res = await
|
|
16
|
+
const res = await safeFetch(url, {}, FETCH_DOWNLOAD_TIMEOUT_MS);
|
|
16
17
|
if (!res.ok) {
|
|
17
18
|
throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);
|
|
18
19
|
}
|
|
@@ -6,9 +6,19 @@ import { getAspectRatio } from "../utils/utils.js";
|
|
|
6
6
|
import { ffmpegGetMediaDuration } from "../utils/ffmpeg_utils.js";
|
|
7
7
|
import { ASPECT_RATIOS } from "../types/const.js";
|
|
8
8
|
import { getModelDuration, provider2MovieAgent, AUDIO_MODE_NEVER, AUDIO_MODE_ALWAYS } from "../types/provider2agent.js";
|
|
9
|
+
// Per-request timeout so a stalled GenAI video API call rejects instead of hanging.
|
|
10
|
+
const GENAI_REQUEST_TIMEOUT_MS = 120_000;
|
|
11
|
+
// Wall-clock cap on the long-running video operation poll loop (Veo runs minutes).
|
|
12
|
+
const VIDEO_POLL_TIMEOUT_MS = 1_200_000;
|
|
9
13
|
const pollUntilDone = async (ai, operation) => {
|
|
10
14
|
const response = { operation };
|
|
15
|
+
const deadline = Date.now() + VIDEO_POLL_TIMEOUT_MS;
|
|
11
16
|
while (!response.operation.done) {
|
|
17
|
+
if (Date.now() > deadline) {
|
|
18
|
+
throw new Error(`Video generation did not complete within ${VIDEO_POLL_TIMEOUT_MS}ms`, {
|
|
19
|
+
cause: agentGenerationError("movieGenAIAgent", imageAction, movieFileTarget),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
12
22
|
await sleep(5000);
|
|
13
23
|
response.operation = await ai.operations.getVideosOperation(response);
|
|
14
24
|
}
|
|
@@ -190,6 +200,7 @@ export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
190
200
|
vertexai: true,
|
|
191
201
|
project: params.vertexai_project,
|
|
192
202
|
location: params.vertexai_location ?? "us-central1",
|
|
203
|
+
httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS },
|
|
193
204
|
})
|
|
194
205
|
: (() => {
|
|
195
206
|
if (!apiKey) {
|
|
@@ -197,7 +208,7 @@ export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
197
208
|
cause: apiKeyMissingError("movieGenAIAgent", imageAction, "GEMINI_API_KEY"),
|
|
198
209
|
});
|
|
199
210
|
}
|
|
200
|
-
return new GoogleGenAI({ apiKey });
|
|
211
|
+
return new GoogleGenAI({ apiKey, httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS } });
|
|
201
212
|
})();
|
|
202
213
|
// Veo 3.1: Video extension mode for videos longer than 8s
|
|
203
214
|
if (model === "veo-3.1-generate-preview" && requestedDuration > 8 && params.canvasSize) {
|
|
@@ -207,11 +218,14 @@ export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
207
218
|
return generateStandardVideo(ai, model, prompt, aspectRatio, imagePath, lastFrameImagePath, referenceImages, duration, movieFile, isVertexAI);
|
|
208
219
|
}
|
|
209
220
|
catch (error) {
|
|
210
|
-
GraphAILogger.info("Failed to generate movie:", error.message);
|
|
211
221
|
if (hasCause(error) && error.cause) {
|
|
212
222
|
throw error;
|
|
213
223
|
}
|
|
214
|
-
|
|
224
|
+
GraphAILogger.info("Failed to generate movie:", error.message);
|
|
225
|
+
// Preserve the underlying message (e.g. a timeout/abort deadline) instead of
|
|
226
|
+
// collapsing every failure to a static label. (Same template as #1452.)
|
|
227
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
228
|
+
throw new Error(`Failed to generate movie with Google GenAI: ${detail}`, {
|
|
215
229
|
cause: agentGenerationError("movieGenAIAgent", imageAction, movieFileTarget),
|
|
216
230
|
});
|
|
217
231
|
}
|
|
@@ -4,6 +4,7 @@ import Replicate from "replicate";
|
|
|
4
4
|
import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, hasCause, imageAction, movieFileTarget, videoDurationTarget, unsupportedModelTarget, } from "../utils/error_cause.js";
|
|
5
5
|
import { provider2MovieAgent, getModelDuration, AUDIO_MODE_OPTIONAL, AUDIO_MODE_NEVER, AUDIO_MODE_ALWAYS } from "../types/provider2agent.js";
|
|
6
6
|
import { runReplicateWithMetrics } from "../utils/replicate_usage.js";
|
|
7
|
+
import { safeFetch, FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
7
8
|
function replicate_get_videoUrl(output) {
|
|
8
9
|
if (typeof output === "string")
|
|
9
10
|
return output;
|
|
@@ -99,7 +100,7 @@ async function generateMovie(model, apiKey, prompt, imagePath, lastFrameImagePat
|
|
|
99
100
|
// Some models return a FileOutput object with a url() method; others return a plain string URL.
|
|
100
101
|
const videoUrl = replicate_get_videoUrl(output);
|
|
101
102
|
if (videoUrl) {
|
|
102
|
-
const videoResponse = await
|
|
103
|
+
const videoResponse = await safeFetch(videoUrl, {}, FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS);
|
|
103
104
|
if (!videoResponse.ok) {
|
|
104
105
|
throw new Error(`Error downloading video: ${videoResponse.status} - ${videoResponse.statusText}`, {
|
|
105
106
|
cause: agentGenerationError("movieReplicateAgent", imageAction, movieFileTarget),
|
|
@@ -4,6 +4,7 @@ import Replicate from "replicate";
|
|
|
4
4
|
import { provider2SoundEffectAgent } from "../types/provider2agent.js";
|
|
5
5
|
import { apiKeyMissingError, agentGenerationError, imageAction, movieFileTarget, hasCause } from "../utils/error_cause.js";
|
|
6
6
|
import { runReplicateWithMetrics } from "../utils/replicate_usage.js";
|
|
7
|
+
import { safeFetch, FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
7
8
|
export const soundEffectReplicateAgent = async ({ namedInputs, params, config }) => {
|
|
8
9
|
const { prompt, movieFile } = namedInputs;
|
|
9
10
|
const apiKey = config?.apiKey;
|
|
@@ -32,7 +33,7 @@ export const soundEffectReplicateAgent = async ({ namedInputs, params, config })
|
|
|
32
33
|
const { output, predictSec } = await runReplicateWithMetrics(replicate, model_identifier, input);
|
|
33
34
|
if (output && typeof output === "object" && "url" in output) {
|
|
34
35
|
const videoUrl = output.url();
|
|
35
|
-
const videoResponse = await
|
|
36
|
+
const videoResponse = await safeFetch(videoUrl, {}, FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS);
|
|
36
37
|
if (!videoResponse.ok) {
|
|
37
38
|
throw new Error(`Error downloading video: ${videoResponse.status} - ${videoResponse.statusText}`, {
|
|
38
39
|
cause: agentGenerationError("soundEffectReplicateAgent", imageAction, movieFileTarget),
|
|
@@ -49,7 +50,10 @@ export const soundEffectReplicateAgent = async ({ namedInputs, params, config })
|
|
|
49
50
|
if (hasCause(error) && error.cause) {
|
|
50
51
|
throw error;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
// Preserve the underlying message (e.g. a safeFetch timeout) rather than
|
|
54
|
+
// collapsing every failure to a static label. (Same template as #1452.)
|
|
55
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
56
|
+
throw new Error(`Failed to generate sound effect with Replicate: ${detail}`, {
|
|
53
57
|
cause: agentGenerationError("soundEffectReplicateAgent", imageAction, movieFileTarget),
|
|
54
58
|
});
|
|
55
59
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GraphAILogger } from "graphai";
|
|
2
2
|
import { provider2TTSAgent } from "../types/provider2agent.js";
|
|
3
3
|
import { apiKeyMissingError, agentVoiceLimitReachedError, agentIncorrectAPIKeyError, agentGenerationError, audioAction, audioFileTarget, } from "../utils/error_cause.js";
|
|
4
|
+
import { safeFetch, FETCH_API_TIMEOUT_MS } from "../utils/fetch.js";
|
|
4
5
|
export const ttsElevenlabsAgent = async ({ namedInputs, params, config, }) => {
|
|
5
6
|
const { text } = namedInputs;
|
|
6
7
|
const { voice, model, stability, similarityBoost, speed, suppressError } = params;
|
|
@@ -27,7 +28,7 @@ export const ttsElevenlabsAgent = async ({ namedInputs, params, config, }) => {
|
|
|
27
28
|
GraphAILogger.log("ElevenLabs TTS options", requestBody);
|
|
28
29
|
const response = await (async () => {
|
|
29
30
|
try {
|
|
30
|
-
return await
|
|
31
|
+
return await safeFetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice}`, {
|
|
31
32
|
method: "POST",
|
|
32
33
|
headers: {
|
|
33
34
|
Accept: "audio/mpeg",
|
|
@@ -35,7 +36,7 @@ export const ttsElevenlabsAgent = async ({ namedInputs, params, config, }) => {
|
|
|
35
36
|
"xi-api-key": apiKey,
|
|
36
37
|
},
|
|
37
38
|
body: JSON.stringify(requestBody),
|
|
38
|
-
});
|
|
39
|
+
}, FETCH_API_TIMEOUT_MS);
|
|
39
40
|
}
|
|
40
41
|
catch (e) {
|
|
41
42
|
if (suppressError) {
|
|
@@ -64,7 +65,7 @@ export const ttsElevenlabsAgent = async ({ namedInputs, params, config, }) => {
|
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
const errorDetail = await response.json();
|
|
67
|
-
if (errorDetail.detail
|
|
68
|
+
if (errorDetail.detail?.status === "voice_limit_reached") {
|
|
68
69
|
throw new Error("Failed to generate audio: 400 You have reached your maximum amount of custom voices with ElevenLabs", {
|
|
69
70
|
cause: agentVoiceLimitReachedError("ttsElevenlabsAgent", audioAction, audioFileTarget),
|
|
70
71
|
});
|
|
@@ -3,6 +3,9 @@ import { GoogleGenAI } from "@google/genai";
|
|
|
3
3
|
import { provider2TTSAgent } from "../types/provider2agent.js";
|
|
4
4
|
import { agentIncorrectAPIKeyError, apiKeyMissingError, agentGenerationError, audioAction, audioFileTarget, getGenAIErrorReason, } from "../utils/error_cause.js";
|
|
5
5
|
import { pcmToMp3 } from "../utils/ffmpeg_utils.js";
|
|
6
|
+
// Per-request timeout so a stalled GenAI TTS call rejects (and GraphAI retry can
|
|
7
|
+
// recover) instead of hanging. Generous vs. normal generation time.
|
|
8
|
+
const GENAI_REQUEST_TIMEOUT_MS = 120_000;
|
|
6
9
|
const getPrompt = (text, instructions) => {
|
|
7
10
|
// https://ai.google.dev/gemini-api/docs/speech-generation?hl=ja#controllable
|
|
8
11
|
if (instructions) {
|
|
@@ -21,7 +24,7 @@ export const ttsGeminiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
21
24
|
}
|
|
22
25
|
const geminiResult = await (async () => {
|
|
23
26
|
try {
|
|
24
|
-
const ai = new GoogleGenAI({ apiKey });
|
|
27
|
+
const ai = new GoogleGenAI({ apiKey, httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS } });
|
|
25
28
|
const response = await ai.models.generateContent({
|
|
26
29
|
model: model ?? provider2TTSAgent.gemini.defaultModel,
|
|
27
30
|
contents: [{ parts: [{ text: getPrompt(text, instructions) }] }],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GraphAILogger } from "graphai";
|
|
2
2
|
import { provider2TTSAgent } from "../types/provider2agent.js";
|
|
3
3
|
import { apiKeyMissingError, agentIncorrectAPIKeyError, agentGenerationError, audioAction, audioFileTarget } from "../utils/error_cause.js";
|
|
4
|
+
import { safeFetch, FETCH_API_TIMEOUT_MS } from "../utils/fetch.js";
|
|
4
5
|
export const ttsKotodamaAgent = async ({ namedInputs, params, config, }) => {
|
|
5
6
|
const { text } = namedInputs;
|
|
6
7
|
const { voice, decoration, suppressError } = params;
|
|
@@ -18,14 +19,14 @@ export const ttsKotodamaAgent = async ({ namedInputs, params, config, }) => {
|
|
|
18
19
|
audio_format: "mp3",
|
|
19
20
|
};
|
|
20
21
|
try {
|
|
21
|
-
const response = await
|
|
22
|
+
const response = await safeFetch(url, {
|
|
22
23
|
method: "POST",
|
|
23
24
|
headers: {
|
|
24
25
|
"Content-Type": "application/json",
|
|
25
26
|
"X-API-Key": apiKey,
|
|
26
27
|
},
|
|
27
28
|
body: JSON.stringify(body),
|
|
28
|
-
});
|
|
29
|
+
}, FETCH_API_TIMEOUT_MS);
|
|
29
30
|
if (!response.ok) {
|
|
30
31
|
if (response.status === 401) {
|
|
31
32
|
throw new Error("Failed to generate audio: 401 Incorrect API key provided with Kotodama", {
|
|
@@ -64,7 +65,10 @@ export const ttsKotodamaAgent = async ({ namedInputs, params, config, }) => {
|
|
|
64
65
|
if (error && typeof error === "object" && "cause" in error) {
|
|
65
66
|
throw error;
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
// Preserve the underlying message (e.g. a safeFetch timeout) rather than
|
|
69
|
+
// collapsing every failure to a static label. (Same template as #1452.)
|
|
70
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
71
|
+
throw new Error(`TTS Kotodama Error: ${detail}`, {
|
|
68
72
|
cause: agentGenerationError("ttsKotodamaAgent", audioAction, audioFileTarget),
|
|
69
73
|
});
|
|
70
74
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { GraphAILogger, assert } from "graphai";
|
|
3
3
|
import { getFullPath, getReferenceImagePath, resolveAssetPath } from "../utils/file.js";
|
|
4
|
+
import { safeFetch, DEFAULT_FETCH_TIMEOUT_MS } from "../utils/fetch.js";
|
|
4
5
|
import { downLoadReferenceImageError, getTextError, imageReferenceUnknownMediaError, downloadImagePluginError, imagePluginUnknownMediaError, mediaSourceToDataUrlError, mediaSourceFileNotFoundError, mediaSourceUnknownKindError, } from "../utils/error_cause.js";
|
|
5
6
|
// for image reference
|
|
6
7
|
export const getExtention = (contentType, url) => {
|
|
@@ -18,7 +19,7 @@ export const getExtention = (contentType, url) => {
|
|
|
18
19
|
return "png"; // default
|
|
19
20
|
};
|
|
20
21
|
const downLoadReferenceImage = async (context, key, url) => {
|
|
21
|
-
const response = await
|
|
22
|
+
const response = await safeFetch(url);
|
|
22
23
|
assert(response.ok, `Failed to download reference image: ${url}`, false, downLoadReferenceImageError(key, url));
|
|
23
24
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
24
25
|
// Detect file extension from Content-Type header or URL
|
|
@@ -38,26 +39,21 @@ function pluginSourceFixExtention(path, imageType) {
|
|
|
38
39
|
return path;
|
|
39
40
|
}
|
|
40
41
|
// end of util
|
|
41
|
-
const DEFAULT_FETCH_TIMEOUT_MS = 30000;
|
|
42
42
|
// Convert URL to data URL (base64 encoded)
|
|
43
43
|
const urlToDataUrl = async (url, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) => {
|
|
44
|
-
const controller = new AbortController();
|
|
45
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
46
44
|
try {
|
|
47
|
-
const response = await
|
|
45
|
+
const response = await safeFetch(url, {}, timeoutMs);
|
|
48
46
|
assert(response.ok, `Failed to fetch: ${url}`, false, mediaSourceToDataUrlError(url));
|
|
49
47
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
50
48
|
const contentType = response.headers.get("content-type") || "image/png";
|
|
51
49
|
return `data:${contentType};base64,${buffer.toString("base64")}`;
|
|
52
50
|
}
|
|
53
51
|
catch (error) {
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
// assert() failures already carry the structured cause — keep it as-is.
|
|
53
|
+
if (error instanceof Error && error.cause) {
|
|
54
|
+
throw error;
|
|
56
55
|
}
|
|
57
|
-
throw new Error(`Fetch failed: ${url}`, { cause: mediaSourceToDataUrlError(url) });
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
clearTimeout(timeoutId);
|
|
56
|
+
throw new Error(`Fetch failed: ${url}: ${error instanceof Error ? error.message : String(error)}`, { cause: mediaSourceToDataUrlError(url) });
|
|
61
57
|
}
|
|
62
58
|
};
|
|
63
59
|
/** Map file extension to MIME type for data URLs */
|
|
@@ -92,7 +88,7 @@ export const MulmoMediaSourceMethods = {
|
|
|
92
88
|
return mediaSource.text;
|
|
93
89
|
}
|
|
94
90
|
if (mediaSource.kind === "url") {
|
|
95
|
-
const response = await
|
|
91
|
+
const response = await safeFetch(mediaSource.url);
|
|
96
92
|
assert(response.ok, `Failed to download mermaid code text: ${mediaSource.url}`, false, getTextError(mediaSource.url)); // TODO: index
|
|
97
93
|
return await response.text();
|
|
98
94
|
}
|
|
@@ -126,7 +122,7 @@ export const MulmoMediaSourceMethods = {
|
|
|
126
122
|
},
|
|
127
123
|
async imagePluginSource(mediaSource, context, expectImagePath, imageType) {
|
|
128
124
|
if (mediaSource.kind === "url") {
|
|
129
|
-
const response = await
|
|
125
|
+
const response = await safeFetch(mediaSource.url);
|
|
130
126
|
assert(response.ok, `Failed to download image plugin: ${imageType} ${mediaSource.url}`, false, downloadImagePluginError(mediaSource.url, imageType)); // TODO: key, id, index
|
|
131
127
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
132
128
|
// Detect file extension from Content-Type header or URL
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const DEFAULT_FETCH_TIMEOUT_MS = 30000;
|
|
2
|
+
export declare const FETCH_DOWNLOAD_TIMEOUT_MS = 60000;
|
|
3
|
+
export declare const FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS = 180000;
|
|
4
|
+
export declare const FETCH_API_TIMEOUT_MS = 120000;
|
|
5
|
+
export type SafeFetchResponse = {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
status: number;
|
|
8
|
+
statusText: string;
|
|
9
|
+
headers: {
|
|
10
|
+
get(name: string): string | null;
|
|
11
|
+
};
|
|
12
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
13
|
+
text(): Promise<string>;
|
|
14
|
+
json<T = unknown>(): Promise<T>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* fetch() with an AbortController timeout that covers the whole exchange —
|
|
18
|
+
* headers AND body.
|
|
19
|
+
*
|
|
20
|
+
* A stalled connection otherwise yields a promise that never resolves or
|
|
21
|
+
* rejects; callers relying on GraphAI `retry` never recover because retry only
|
|
22
|
+
* fires on rejection. The body is read once under the same deadline (a server
|
|
23
|
+
* that sends headers and then stalls mid-body would otherwise hang the caller's
|
|
24
|
+
* body read), and the bytes are handed back through a lightweight result so the
|
|
25
|
+
* caller does not buffer them a second time. An optional caller `signal` is
|
|
26
|
+
* composed with the timeout so upstream cancellation still works.
|
|
27
|
+
*/
|
|
28
|
+
export declare const safeFetch: (url: string | URL, init?: Parameters<typeof fetch>[1], timeoutMs?: number) => Promise<SafeFetchResponse>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Timeout budgets for the different kinds of network waits. Generous safety
|
|
2
|
+
// nets against a stalled socket — NOT tight deadlines — so legitimate slow
|
|
3
|
+
// responses still succeed while a genuine hang becomes a rejection.
|
|
4
|
+
export const DEFAULT_FETCH_TIMEOUT_MS = 30_000; // small assets (reference/background images, JSON)
|
|
5
|
+
export const FETCH_DOWNLOAD_TIMEOUT_MS = 60_000; // generated image download
|
|
6
|
+
export const FETCH_MEDIA_DOWNLOAD_TIMEOUT_MS = 180_000; // large video/audio download
|
|
7
|
+
export const FETCH_API_TIMEOUT_MS = 120_000; // generation API POST (e.g. TTS text -> audio)
|
|
8
|
+
/**
|
|
9
|
+
* fetch() with an AbortController timeout that covers the whole exchange —
|
|
10
|
+
* headers AND body.
|
|
11
|
+
*
|
|
12
|
+
* A stalled connection otherwise yields a promise that never resolves or
|
|
13
|
+
* rejects; callers relying on GraphAI `retry` never recover because retry only
|
|
14
|
+
* fires on rejection. The body is read once under the same deadline (a server
|
|
15
|
+
* that sends headers and then stalls mid-body would otherwise hang the caller's
|
|
16
|
+
* body read), and the bytes are handed back through a lightweight result so the
|
|
17
|
+
* caller does not buffer them a second time. An optional caller `signal` is
|
|
18
|
+
* composed with the timeout so upstream cancellation still works.
|
|
19
|
+
*/
|
|
20
|
+
export const safeFetch = async (url, init = {}, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) => {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
23
|
+
const externalSignal = init?.signal ?? undefined;
|
|
24
|
+
const forwardAbort = () => controller.abort();
|
|
25
|
+
externalSignal?.addEventListener("abort", forwardAbort, { once: true });
|
|
26
|
+
if (externalSignal?.aborted)
|
|
27
|
+
controller.abort();
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
30
|
+
const body = await response.arrayBuffer();
|
|
31
|
+
const decode = () => new TextDecoder().decode(body);
|
|
32
|
+
return {
|
|
33
|
+
ok: response.ok,
|
|
34
|
+
status: response.status,
|
|
35
|
+
statusText: response.statusText,
|
|
36
|
+
headers: response.headers,
|
|
37
|
+
arrayBuffer: async () => body,
|
|
38
|
+
text: async () => decode(),
|
|
39
|
+
json: async () => JSON.parse(decode()),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
44
|
+
throw new Error(`Fetch timeout after ${timeoutMs}ms: ${url}`);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timeoutId);
|
|
50
|
+
externalSignal?.removeEventListener("abort", forwardAbort);
|
|
51
|
+
}
|
|
52
|
+
};
|
package/lib/utils/file.js
CHANGED
|
@@ -8,6 +8,7 @@ import { MulmoStudioContextMethods } from "../methods/index.js";
|
|
|
8
8
|
import { mulmoPromptTemplateSchema } from "../types/schema.js";
|
|
9
9
|
import { getMulmoScriptTemplateSystemPrompt } from "./prompt.js";
|
|
10
10
|
import { resolveAssetFile } from "./asset_import.js";
|
|
11
|
+
import { safeFetch } from "./fetch.js";
|
|
11
12
|
const promptTemplateDirName = "./assets/templates";
|
|
12
13
|
const scriptTemplateDirName = "./scripts/templates";
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -43,7 +44,7 @@ export function readMulmoScriptFile(arg2, errorMessage) {
|
|
|
43
44
|
}
|
|
44
45
|
export const fetchMulmoScriptFile = async (url) => {
|
|
45
46
|
try {
|
|
46
|
-
const res = await
|
|
47
|
+
const res = await safeFetch(url);
|
|
47
48
|
if (!res.ok) {
|
|
48
49
|
return { result: false, status: res.status };
|
|
49
50
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MulmoMediaSourceMethods } from "../../methods/mulmo_media_source.js";
|
|
2
2
|
import { resolveStyle } from "./utils.js";
|
|
3
|
-
|
|
3
|
+
import { safeFetch, DEFAULT_FETCH_TIMEOUT_MS } from "../fetch.js";
|
|
4
4
|
/**
|
|
5
5
|
* Resolve background image from beat level and global level settings.
|
|
6
6
|
* Beat level takes precedence over global level.
|
|
@@ -25,26 +25,13 @@ export const resolveBackgroundImage = (beatBackgroundImage, globalBackgroundImag
|
|
|
25
25
|
* Fetch URL and convert to data URL with timeout
|
|
26
26
|
*/
|
|
27
27
|
const fetchUrlAsDataUrl = async (url, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
32
|
-
if (!response.ok) {
|
|
33
|
-
throw new Error(`Failed to fetch background image: ${url} (${response.status})`);
|
|
34
|
-
}
|
|
35
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
36
|
-
const contentType = response.headers.get("content-type") || "image/png";
|
|
37
|
-
return `data:${contentType};base64,${buffer.toString("base64")}`;
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
41
|
-
throw new Error(`Fetch timeout for background image: ${url}`);
|
|
42
|
-
}
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
clearTimeout(timeoutId);
|
|
28
|
+
const response = await safeFetch(url, {}, timeoutMs);
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Failed to fetch background image: ${url} (${response.status})`);
|
|
47
31
|
}
|
|
32
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
33
|
+
const contentType = response.headers.get("content-type") || "image/png";
|
|
34
|
+
return `data:${contentType};base64,${buffer.toString("base64")}`;
|
|
48
35
|
};
|
|
49
36
|
/**
|
|
50
37
|
* Convert BackgroundImage to CSS string
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import OpenAI, { AzureOpenAI } from "openai";
|
|
2
|
+
// Explicit per-request timeout so a stalled connection rejects (and the SDK's
|
|
3
|
+
// default maxRetries=2 re-issues it) instead of hanging on the SDK's 10-minute
|
|
4
|
+
// default. gpt-image-1 generation is typically well under this.
|
|
5
|
+
const OPENAI_REQUEST_TIMEOUT_MS = 120_000;
|
|
2
6
|
/**
|
|
3
7
|
* Detects if the given URL is an Azure OpenAI endpoint
|
|
4
8
|
* Safely parses the URL and checks if the hostname ends with ".openai.azure.com"
|
|
@@ -26,10 +30,12 @@ export const createOpenAIClient = (options) => {
|
|
|
26
30
|
apiKey,
|
|
27
31
|
endpoint: baseURL,
|
|
28
32
|
apiVersion: apiVersion ?? "2025-04-01-preview",
|
|
33
|
+
timeout: OPENAI_REQUEST_TIMEOUT_MS,
|
|
29
34
|
});
|
|
30
35
|
}
|
|
31
36
|
return new OpenAI({
|
|
32
37
|
apiKey,
|
|
33
38
|
baseURL,
|
|
39
|
+
timeout: OPENAI_REQUEST_TIMEOUT_MS,
|
|
34
40
|
});
|
|
35
41
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type Replicate from "replicate";
|
|
2
|
-
export declare const runReplicateWithMetrics: (replicate: Replicate, identifier: `${string}/${string}` | `${string}/${string}:${string}`, input: object,
|
|
2
|
+
export declare const runReplicateWithMetrics: (replicate: Replicate, identifier: `${string}/${string}` | `${string}/${string}:${string}`, input: object, options?: {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}) => Promise<{
|
|
3
6
|
output: unknown;
|
|
4
7
|
predictSec?: number;
|
|
5
8
|
}>;
|
|
@@ -1,11 +1,31 @@
|
|
|
1
|
+
// Generous safety net against a stalled run — video models (seedance/kling)
|
|
2
|
+
// legitimately run for minutes — not a tight deadline.
|
|
3
|
+
const REPLICATE_RUN_TIMEOUT_MS = 600_000;
|
|
1
4
|
// Capture Replicate's exact billed seconds (`metrics.predict_time`) via the
|
|
2
5
|
// progress callback on `replicate.run()`. Avoids switching the four
|
|
3
6
|
// replicate agents from `.run()` to `predictions.create()` + poll, which
|
|
4
7
|
// would be a much bigger blast radius.
|
|
5
|
-
|
|
8
|
+
//
|
|
9
|
+
// A timeout aborts the run so a stalled job rejects (and the caller's GraphAI
|
|
10
|
+
// `retry` can recover) instead of hanging forever. An optional caller `signal`
|
|
11
|
+
// is composed with the timeout so upstream cancellation still works.
|
|
12
|
+
export const runReplicateWithMetrics = async (replicate, identifier, input, options = {}) => {
|
|
13
|
+
const { timeoutMs = REPLICATE_RUN_TIMEOUT_MS, signal: externalSignal } = options;
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
+
const forwardAbort = () => controller.abort();
|
|
17
|
+
externalSignal?.addEventListener("abort", forwardAbort, { once: true });
|
|
18
|
+
if (externalSignal?.aborted)
|
|
19
|
+
controller.abort();
|
|
6
20
|
let lastPrediction;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
21
|
+
try {
|
|
22
|
+
const output = await replicate.run(identifier, { input, signal: controller.signal }, (prediction) => {
|
|
23
|
+
lastPrediction = prediction;
|
|
24
|
+
});
|
|
25
|
+
return { output, predictSec: lastPrediction?.metrics?.predict_time };
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
clearTimeout(timeoutId);
|
|
29
|
+
externalSignal?.removeEventListener("abort", forwardAbort);
|
|
30
|
+
}
|
|
11
31
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Timeout budgets for provider SDK calls (OpenAI has its own in openai_client.ts).
|
|
2
|
+
// Generous safety nets against a stalled connection or a never-completing
|
|
3
|
+
// operation — NOT tight deadlines. A stall becomes a rejection so the caller's
|
|
4
|
+
// GraphAI `retry` can recover instead of hanging forever.
|
|
5
|
+
export const GENAI_REQUEST_TIMEOUT_MS = 120_000; // per @google/genai API call (kick-off / each poll)
|
|
6
|
+
export const REPLICATE_RUN_TIMEOUT_MS = 600_000; // whole replicate.run() job (video models run minutes)
|
|
7
|
+
export const VIDEO_POLL_TIMEOUT_MS = 1_200_000; // wall-clock cap for a long-running Veo video operation
|
package/lib/utils/zip.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import
|
|
2
|
+
import { ZipArchive } from "archiver";
|
|
3
3
|
import path from "path";
|
|
4
4
|
/**
|
|
5
5
|
* Promise-based ZIP archive builder
|
|
@@ -18,7 +18,7 @@ export class ZipBuilder {
|
|
|
18
18
|
constructor(outputPath) {
|
|
19
19
|
this.outputPath = outputPath;
|
|
20
20
|
this.output = fs.createWriteStream(outputPath);
|
|
21
|
-
this.archive =
|
|
21
|
+
this.archive = new ZipArchive({
|
|
22
22
|
zlib: { level: 9 }, // Maximum compression level
|
|
23
23
|
});
|
|
24
24
|
// Pipe archive data to the file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mulmocast",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.node.js",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"@mozilla/readability": "^0.6.0",
|
|
107
107
|
"@mulmocast/deck": "^1.1.0",
|
|
108
108
|
"@tavily/core": "^0.7.6",
|
|
109
|
-
"archiver": "^
|
|
109
|
+
"archiver": "^8.0.0",
|
|
110
110
|
"clipboardy": "^5.3.1",
|
|
111
111
|
"dotenv": "^17.4.2",
|
|
112
112
|
"graphai": "^2.0.18",
|
|
@@ -124,20 +124,20 @@
|
|
|
124
124
|
"devDependencies": {
|
|
125
125
|
"@eslint/js": "^10.0.1",
|
|
126
126
|
"@receptron/test_utils": "^2.0.3",
|
|
127
|
-
"@types/archiver": "^
|
|
127
|
+
"@types/archiver": "^8.0.0",
|
|
128
128
|
"@types/jsdom": "^28.0.3",
|
|
129
129
|
"@types/yargs": "^17.0.35",
|
|
130
130
|
"cross-env": "^10.1.0",
|
|
131
|
-
"eslint": "^10.
|
|
131
|
+
"eslint": "^10.7.0",
|
|
132
132
|
"eslint-config-prettier": "^10.1.8",
|
|
133
133
|
"eslint-plugin-import": "^2.32.0",
|
|
134
134
|
"eslint-plugin-prettier": "^5.5.6",
|
|
135
135
|
"eslint-plugin-sonarjs": "^4.1.0",
|
|
136
136
|
"globals": "^17.7.0",
|
|
137
|
-
"prettier": "^3.9.
|
|
138
|
-
"tsx": "^4.
|
|
137
|
+
"prettier": "^3.9.5",
|
|
138
|
+
"tsx": "^4.23.0",
|
|
139
139
|
"typescript": "6.0.3",
|
|
140
|
-
"typescript-eslint": "^8.
|
|
140
|
+
"typescript-eslint": "^8.63.0"
|
|
141
141
|
},
|
|
142
142
|
"engines": {
|
|
143
143
|
"node": ">=22.0.0"
|