mulmocast 2.0.5 → 2.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/lib/agents/image_genai_agent.js +62 -56
- package/lib/agents/image_replicate_agent.js +9 -1
- package/lib/agents/movie_genai_agent.d.ts +0 -4
- package/lib/agents/movie_genai_agent.js +104 -56
- package/lib/agents/test.d.ts +1 -0
- package/lib/agents/test.js +12 -0
- package/lib/agents/tts_elevenlabs_agent.js +42 -32
- package/lib/agents/tts_gemini_agent.js +8 -2
- package/lib/agents/tts_kotodama_agent.d.ts +5 -0
- package/lib/agents/tts_kotodama_agent.js +76 -0
- package/lib/agents/tts_openai_agent.js +1 -1
- package/lib/agents/utils.d.ts +1 -0
- package/lib/agents/utils.js +1 -0
- package/lib/utils/const.d.ts +1 -0
- package/lib/utils/const.js +1 -0
- package/lib/utils/error_cause.d.ts +10 -0
- package/lib/utils/error_cause.js +22 -0
- package/lib/utils/utils.d.ts +4 -0
- package/lib/utils/utils.js +18 -6
- package/package.json +4 -4
- package/scripts/test/README.md +161 -0
- package/scripts/test/test_all_elevenlabs_tts_model.json +111 -0
- package/scripts/test/test_all_gemini_tts_model.json +433 -0
- package/scripts/test/test_all_image.json +40 -0
- package/scripts/test/test_all_image.json~ +45 -0
- package/scripts/test/test_all_movie.json +33 -0
- package/scripts/test/test_all_movie.json~ +37 -0
- package/scripts/test/test_all_tts.json +83 -0
- package/scripts/test/test_all_tts.json~ +83 -0
- package/scripts/test/test_genai_movie.json +26 -0
- package/scripts/test/test_genai_movie.json~ +22 -0
- package/scripts/test/test_kotodama.json~ +0 -0
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { GraphAILogger } from "graphai";
|
|
3
3
|
import { provider2ImageAgent } from "../utils/provider2agent.js";
|
|
4
|
-
import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, imageAction, imageFileTarget, hasCause } from "../utils/error_cause.js";
|
|
4
|
+
import { apiKeyMissingError, agentIncorrectAPIKeyError, agentGenerationError, agentInvalidResponseError, imageAction, imageFileTarget, hasCause, getGenAIErrorReason, resultify, } from "../utils/error_cause.js";
|
|
5
|
+
import { getAspectRatio } from "../utils/utils.js";
|
|
6
|
+
import { ASPECT_RATIOS } from "../utils/const.js";
|
|
5
7
|
import { GoogleGenAI, PersonGeneration } from "@google/genai";
|
|
6
8
|
import { blankImagePath, blankSquareImagePath, blankVerticalImagePath } from "../utils/file.js";
|
|
7
|
-
const getAspectRatio = (canvasSize) => {
|
|
8
|
-
if (canvasSize.width > canvasSize.height) {
|
|
9
|
-
return "16:9";
|
|
10
|
-
}
|
|
11
|
-
else if (canvasSize.width < canvasSize.height) {
|
|
12
|
-
return "9:16";
|
|
13
|
-
}
|
|
14
|
-
return "1:1";
|
|
15
|
-
};
|
|
16
9
|
export const ratio2BlankPath = (aspectRatio) => {
|
|
17
10
|
if (aspectRatio === "9:16") {
|
|
18
11
|
return blankVerticalImagePath();
|
|
@@ -61,9 +54,24 @@ const geminiFlashResult = (response) => {
|
|
|
61
54
|
cause: agentInvalidResponseError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
62
55
|
});
|
|
63
56
|
};
|
|
57
|
+
const errorProcess = (error) => {
|
|
58
|
+
GraphAILogger.info("Failed to generate image:", error);
|
|
59
|
+
if (hasCause(error) && error.cause) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const reasonDetail = getGenAIErrorReason(error);
|
|
63
|
+
if (reasonDetail && reasonDetail.reason && reasonDetail.reason === "API_KEY_INVALID") {
|
|
64
|
+
throw new Error("Failed to generate image: 400 Incorrect API key provided with gemini", {
|
|
65
|
+
cause: agentIncorrectAPIKeyError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
throw new Error("Failed to generate image with Google GenAI", {
|
|
69
|
+
cause: agentGenerationError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
70
|
+
});
|
|
71
|
+
};
|
|
64
72
|
export const imageGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
65
73
|
const { prompt, referenceImages } = namedInputs;
|
|
66
|
-
const aspectRatio = getAspectRatio(params.canvasSize);
|
|
74
|
+
const aspectRatio = getAspectRatio(params.canvasSize, ASPECT_RATIOS);
|
|
67
75
|
const model = params.model ?? provider2ImageAgent["google"].defaultModel;
|
|
68
76
|
const apiKey = config?.apiKey;
|
|
69
77
|
if (!apiKey) {
|
|
@@ -71,61 +79,60 @@ export const imageGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
71
79
|
cause: apiKeyMissingError("imageGenAIAgent", imageAction, "GEMINI_API_KEY"),
|
|
72
80
|
});
|
|
73
81
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
83
|
+
if (model === "gemini-2.5-flash-image" || model === "gemini-3-pro-image-preview") {
|
|
84
|
+
const contentParams = (() => {
|
|
85
|
+
if (model === "gemini-2.5-flash-image") {
|
|
86
|
+
const contents = getGeminiContents(prompt, referenceImages, aspectRatio);
|
|
87
|
+
return { model, contents };
|
|
88
|
+
}
|
|
89
|
+
// gemini-3-pro-image-preview
|
|
82
90
|
const contents = getGeminiContents(prompt, referenceImages);
|
|
83
|
-
const
|
|
91
|
+
const PRO_ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
|
|
92
|
+
return {
|
|
84
93
|
model,
|
|
85
94
|
contents,
|
|
86
95
|
config: {
|
|
87
96
|
imageConfig: {
|
|
88
|
-
|
|
89
|
-
aspectRatio,
|
|
97
|
+
aspectRatio: getAspectRatio(params.canvasSize, PRO_ASPECT_RATIOS),
|
|
90
98
|
},
|
|
91
99
|
},
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
model,
|
|
98
|
-
prompt,
|
|
99
|
-
config: {
|
|
100
|
-
numberOfImages: 1, // default is 4!
|
|
101
|
-
aspectRatio,
|
|
102
|
-
personGeneration: PersonGeneration.ALLOW_ALL,
|
|
103
|
-
// safetyFilterLevel: SafetyFilterLevel.BLOCK_ONLY_HIGH,
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
if (!response.generatedImages || response.generatedImages.length === 0) {
|
|
107
|
-
throw new Error("ERROR: generateImage returned no generated images", {
|
|
108
|
-
cause: agentInvalidResponseError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
const image = response.generatedImages[0].image;
|
|
112
|
-
if (image && image.imageBytes) {
|
|
113
|
-
return { buffer: Buffer.from(image.imageBytes, "base64") };
|
|
114
|
-
}
|
|
115
|
-
throw new Error("ERROR: generateImage returned no image bytes", {
|
|
116
|
-
cause: agentInvalidResponseError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
117
|
-
});
|
|
100
|
+
};
|
|
101
|
+
})();
|
|
102
|
+
const res = await resultify(() => ai.models.generateContent(contentParams));
|
|
103
|
+
if (res.ok) {
|
|
104
|
+
return geminiFlashResult(res.value);
|
|
118
105
|
}
|
|
106
|
+
return errorProcess(res.error);
|
|
119
107
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
108
|
+
// other case,
|
|
109
|
+
const generateParams = {
|
|
110
|
+
model,
|
|
111
|
+
prompt,
|
|
112
|
+
config: {
|
|
113
|
+
numberOfImages: 1, // default is 4!
|
|
114
|
+
aspectRatio,
|
|
115
|
+
personGeneration: PersonGeneration.ALLOW_ALL,
|
|
116
|
+
// safetyFilterLevel: SafetyFilterLevel.BLOCK_ONLY_HIGH,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const res = await resultify(() => ai.models.generateImages(generateParams));
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
return errorProcess(res.error);
|
|
122
|
+
}
|
|
123
|
+
const response = res.value;
|
|
124
|
+
if (!response.generatedImages || response.generatedImages.length === 0) {
|
|
125
|
+
throw new Error("ERROR: generateImage returned no generated images", {
|
|
126
|
+
cause: agentInvalidResponseError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
+
const image = response.generatedImages[0].image;
|
|
130
|
+
if (image && image.imageBytes) {
|
|
131
|
+
return { buffer: Buffer.from(image.imageBytes, "base64") };
|
|
132
|
+
}
|
|
133
|
+
throw new Error("ERROR: generateImage returned no image bytes", {
|
|
134
|
+
cause: agentInvalidResponseError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
135
|
+
});
|
|
129
136
|
};
|
|
130
137
|
const imageGenAIAgentInfo = {
|
|
131
138
|
name: "imageGenAIAgent",
|
|
@@ -136,7 +143,6 @@ const imageGenAIAgentInfo = {
|
|
|
136
143
|
category: ["image"],
|
|
137
144
|
author: "Receptron Team",
|
|
138
145
|
repository: "https://github.com/receptron/mulmocast-cli/",
|
|
139
|
-
// source: "https://github.com/receptron/mulmocast-cli/blob/main/src/agents/image_google_agent.ts",
|
|
140
146
|
license: "MIT",
|
|
141
147
|
environmentVariables: [],
|
|
142
148
|
};
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from "fs";
|
|
|
2
2
|
import { GraphAILogger } from "graphai";
|
|
3
3
|
import Replicate from "replicate";
|
|
4
4
|
import { getAspectRatio } from "./movie_replicate_agent.js";
|
|
5
|
-
import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, imageAction, imageFileTarget, hasCause } from "../utils/error_cause.js";
|
|
5
|
+
import { apiKeyMissingError, agentIncorrectAPIKeyError, agentGenerationError, agentInvalidResponseError, imageAction, imageFileTarget, hasCause, } from "../utils/error_cause.js";
|
|
6
6
|
import { provider2ImageAgent } from "../utils/provider2agent.js";
|
|
7
7
|
export const imageReplicateAgent = async ({ namedInputs, params, config, }) => {
|
|
8
8
|
const { prompt, referenceImages } = namedInputs;
|
|
@@ -51,6 +51,14 @@ export const imageReplicateAgent = async ({ namedInputs, params, config, }) => {
|
|
|
51
51
|
if (hasCause(error) && error.cause) {
|
|
52
52
|
throw error;
|
|
53
53
|
}
|
|
54
|
+
if (typeof error === "object" && error !== null && "response" in error) {
|
|
55
|
+
const errorWithResponse = error;
|
|
56
|
+
if (errorWithResponse.response?.status === 401) {
|
|
57
|
+
throw new Error("Failed to generate image: 401 Incorrect API key provided with replicate", {
|
|
58
|
+
cause: agentIncorrectAPIKeyError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
54
62
|
throw new Error("Failed to generate image with Replicate", {
|
|
55
63
|
cause: agentGenerationError("imageReplicateAgent", imageAction, imageFileTarget),
|
|
56
64
|
});
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { AgentFunction, AgentFunctionInfo } from "graphai";
|
|
2
2
|
import type { AgentBufferResult, GenAIImageAgentConfig, GoogleMovieAgentParams, MovieAgentInputs } from "../types/agent.js";
|
|
3
|
-
export declare const getAspectRatio: (canvasSize: {
|
|
4
|
-
width: number;
|
|
5
|
-
height: number;
|
|
6
|
-
}) => string;
|
|
7
3
|
export declare const movieGenAIAgent: AgentFunction<GoogleMovieAgentParams, AgentBufferResult, MovieAgentInputs, GenAIImageAgentConfig>;
|
|
8
4
|
declare const movieGenAIAgentInfo: AgentFunctionInfo;
|
|
9
5
|
export default movieGenAIAgentInfo;
|
|
@@ -1,22 +1,112 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
2
|
import { GraphAILogger, sleep } from "graphai";
|
|
3
|
-
import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, imageAction, movieFileTarget, videoDurationTarget, hasCause, } from "../utils/error_cause.js";
|
|
4
3
|
import { GoogleGenAI, PersonGeneration } from "@google/genai";
|
|
4
|
+
import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, imageAction, movieFileTarget, videoDurationTarget, hasCause, } from "../utils/error_cause.js";
|
|
5
|
+
import { getAspectRatio } from "../utils/utils.js";
|
|
6
|
+
import { ASPECT_RATIOS } from "../utils/const.js";
|
|
5
7
|
import { getModelDuration, provider2MovieAgent } from "../utils/provider2agent.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
const pollUntilDone = async (ai, operation) => {
|
|
9
|
+
const response = { operation };
|
|
10
|
+
while (!response.operation.done) {
|
|
11
|
+
await sleep(5000);
|
|
12
|
+
response.operation = await ai.operations.getVideosOperation(response);
|
|
13
|
+
}
|
|
14
|
+
return response;
|
|
15
|
+
};
|
|
16
|
+
const getVideoFromResponse = (response, iteration) => {
|
|
17
|
+
const iterationInfo = iteration !== undefined ? ` in iteration ${iteration}` : "";
|
|
18
|
+
if (!response.operation.response?.generatedVideos) {
|
|
19
|
+
throw new Error(`No video${iterationInfo}: ${JSON.stringify(response.operation, null, 2)}`, {
|
|
20
|
+
cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
|
|
21
|
+
});
|
|
9
22
|
}
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
const video = response.operation.response.generatedVideos[0].video;
|
|
24
|
+
if (!video) {
|
|
25
|
+
throw new Error(`No video${iterationInfo}`, {
|
|
26
|
+
cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
|
|
27
|
+
});
|
|
12
28
|
}
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
return video;
|
|
30
|
+
};
|
|
31
|
+
const loadImageAsBase64 = (imagePath) => {
|
|
32
|
+
const buffer = readFileSync(imagePath);
|
|
33
|
+
return {
|
|
34
|
+
imageBytes: buffer.toString("base64"),
|
|
35
|
+
mimeType: "image/png",
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const downloadVideo = async (ai, video, movieFile) => {
|
|
39
|
+
await ai.files.download({
|
|
40
|
+
file: video,
|
|
41
|
+
downloadPath: movieFile,
|
|
42
|
+
});
|
|
43
|
+
await sleep(5000); // HACK: Without this, the file is not ready yet.
|
|
44
|
+
return { saved: movieFile };
|
|
45
|
+
};
|
|
46
|
+
const createVeo31Payload = (model, prompt, aspectRatio, source) => ({
|
|
47
|
+
model,
|
|
48
|
+
prompt,
|
|
49
|
+
config: {
|
|
50
|
+
aspectRatio,
|
|
51
|
+
resolution: "720p",
|
|
52
|
+
numberOfVideos: 1,
|
|
53
|
+
},
|
|
54
|
+
...source,
|
|
55
|
+
});
|
|
56
|
+
const generateExtendedVideo = async (ai, model, prompt, aspectRatio, imagePath, requestedDuration, movieFile) => {
|
|
57
|
+
const initialDuration = 8;
|
|
58
|
+
const maxExtensionDuration = 8;
|
|
59
|
+
const extensionsNeeded = Math.ceil((requestedDuration - initialDuration) / maxExtensionDuration);
|
|
60
|
+
GraphAILogger.info(`Veo 3.1 video extension: ${extensionsNeeded} extensions needed for ${requestedDuration}s target`);
|
|
61
|
+
const generateIteration = async (iteration, accumulatedDuration, previousVideo) => {
|
|
62
|
+
const isInitial = iteration === 0;
|
|
63
|
+
const remainingDuration = requestedDuration - accumulatedDuration;
|
|
64
|
+
const extensionDuration = isInitial ? initialDuration : (getModelDuration("google", model, remainingDuration) ?? maxExtensionDuration);
|
|
65
|
+
const getSource = () => {
|
|
66
|
+
if (isInitial)
|
|
67
|
+
return imagePath ? { image: loadImageAsBase64(imagePath) } : undefined;
|
|
68
|
+
return previousVideo?.uri ? { video: { uri: previousVideo.uri } } : undefined;
|
|
69
|
+
};
|
|
70
|
+
const payload = createVeo31Payload(model, prompt, aspectRatio, getSource());
|
|
71
|
+
GraphAILogger.info(isInitial ? "Generating initial 8s video..." : `Extending video: iteration ${iteration}/${extensionsNeeded} (+${extensionDuration}s)...`);
|
|
72
|
+
const operation = await ai.models.generateVideos(payload);
|
|
73
|
+
const response = await pollUntilDone(ai, operation);
|
|
74
|
+
const video = getVideoFromResponse(response, iteration);
|
|
75
|
+
const totalDuration = accumulatedDuration + extensionDuration;
|
|
76
|
+
GraphAILogger.info(`Video ${isInitial ? "generated" : "extended"}: ~${totalDuration}s total`);
|
|
77
|
+
return { video, duration: totalDuration };
|
|
78
|
+
};
|
|
79
|
+
const result = await Array.from({ length: extensionsNeeded + 1 }).reduce(async (prev, _, index) => {
|
|
80
|
+
const { video, duration } = await prev;
|
|
81
|
+
return generateIteration(index, duration, video);
|
|
82
|
+
}, Promise.resolve({ video: undefined, duration: 0 }));
|
|
83
|
+
if (!result.video) {
|
|
84
|
+
throw new Error("Failed to generate extended video", {
|
|
85
|
+
cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
|
|
86
|
+
});
|
|
15
87
|
}
|
|
88
|
+
return downloadVideo(ai, result.video, movieFile);
|
|
89
|
+
};
|
|
90
|
+
const generateStandardVideo = async (ai, model, prompt, aspectRatio, imagePath, duration, movieFile) => {
|
|
91
|
+
const isVeo3 = model === "veo-3.0-generate-001" || model === "veo-3.1-generate-preview";
|
|
92
|
+
const payload = {
|
|
93
|
+
model,
|
|
94
|
+
prompt,
|
|
95
|
+
config: {
|
|
96
|
+
durationSeconds: isVeo3 ? undefined : duration,
|
|
97
|
+
aspectRatio,
|
|
98
|
+
personGeneration: imagePath ? undefined : PersonGeneration.ALLOW_ALL,
|
|
99
|
+
},
|
|
100
|
+
image: imagePath ? loadImageAsBase64(imagePath) : undefined,
|
|
101
|
+
};
|
|
102
|
+
const operation = await ai.models.generateVideos(payload);
|
|
103
|
+
const response = await pollUntilDone(ai, operation);
|
|
104
|
+
const video = getVideoFromResponse(response);
|
|
105
|
+
return downloadVideo(ai, video, movieFile);
|
|
16
106
|
};
|
|
17
107
|
export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
18
108
|
const { prompt, imagePath, movieFile } = namedInputs;
|
|
19
|
-
const aspectRatio = getAspectRatio(params.canvasSize);
|
|
109
|
+
const aspectRatio = getAspectRatio(params.canvasSize, ASPECT_RATIOS);
|
|
20
110
|
const model = params.model ?? provider2MovieAgent.google.defaultModel;
|
|
21
111
|
const apiKey = config?.apiKey;
|
|
22
112
|
if (!apiKey) {
|
|
@@ -33,54 +123,12 @@ export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
33
123
|
});
|
|
34
124
|
}
|
|
35
125
|
const ai = new GoogleGenAI({ apiKey });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
prompt,
|
|
39
|
-
config: {
|
|
40
|
-
durationSeconds: duration,
|
|
41
|
-
aspectRatio,
|
|
42
|
-
personGeneration: undefined,
|
|
43
|
-
},
|
|
44
|
-
image: undefined,
|
|
45
|
-
};
|
|
46
|
-
if (model === "veo-3.0-generate-001" || model === "veo-3.1-generate-preview") {
|
|
47
|
-
payload.config.durationSeconds = undefined;
|
|
48
|
-
}
|
|
49
|
-
if (imagePath) {
|
|
50
|
-
const buffer = readFileSync(imagePath);
|
|
51
|
-
const imageBytes = buffer.toString("base64");
|
|
52
|
-
payload.image = {
|
|
53
|
-
imageBytes,
|
|
54
|
-
mimeType: "image/png",
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
payload.config.personGeneration = PersonGeneration.ALLOW_ALL;
|
|
59
|
-
}
|
|
60
|
-
const operation = await ai.models.generateVideos(payload);
|
|
61
|
-
const response = { operation };
|
|
62
|
-
// Poll the operation status until the video is ready.
|
|
63
|
-
while (!response.operation.done) {
|
|
64
|
-
await sleep(5000);
|
|
65
|
-
response.operation = await ai.operations.getVideosOperation(response);
|
|
126
|
+
// Veo 3.1: Video extension mode for videos longer than 8s
|
|
127
|
+
if (model === "veo-3.1-generate-preview" && requestedDuration > 8 && params.canvasSize) {
|
|
128
|
+
return generateExtendedVideo(ai, model, prompt, aspectRatio, imagePath, requestedDuration, movieFile);
|
|
66
129
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
const video = response.operation.response.generatedVideos[0].video;
|
|
73
|
-
if (!video) {
|
|
74
|
-
throw new Error(`No video: ${JSON.stringify(response.operation, null, 2)}`, {
|
|
75
|
-
cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
await ai.files.download({
|
|
79
|
-
file: video,
|
|
80
|
-
downloadPath: movieFile,
|
|
81
|
-
});
|
|
82
|
-
await sleep(5000); // HACK: Without this, the file is not ready yet.
|
|
83
|
-
return { saved: movieFile };
|
|
130
|
+
// Standard mode
|
|
131
|
+
return generateStandardVideo(ai, model, prompt, aspectRatio, imagePath, duration, movieFile);
|
|
84
132
|
}
|
|
85
133
|
catch (error) {
|
|
86
134
|
GraphAILogger.info("Failed to generate movie:", error.message);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { ttsKotodamaAgent } from "./tts_kotodama_agent.js";
|
|
3
|
+
const kotodamaApiKey = process.env.KOTODAMA_API_KEY ?? "";
|
|
4
|
+
const main = async () => {
|
|
5
|
+
const result = await ttsKotodamaAgent({
|
|
6
|
+
namedInputs: { text: "こんにちは" },
|
|
7
|
+
params: { voice: "Atla", decoration: "neutral", suppressError: false },
|
|
8
|
+
config: { apiKey: kotodamaApiKey },
|
|
9
|
+
});
|
|
10
|
+
console.log("Result:", result);
|
|
11
|
+
};
|
|
12
|
+
main();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GraphAILogger } from "graphai";
|
|
2
2
|
import { provider2TTSAgent } from "../utils/provider2agent.js";
|
|
3
|
-
import { apiKeyMissingError, agentGenerationError, audioAction, audioFileTarget } from "../utils/error_cause.js";
|
|
3
|
+
import { apiKeyMissingError, agentIncorrectAPIKeyError, agentGenerationError, audioAction, audioFileTarget } from "../utils/error_cause.js";
|
|
4
4
|
export const ttsElevenlabsAgent = async ({ namedInputs, params, config, }) => {
|
|
5
5
|
const { text } = namedInputs;
|
|
6
6
|
const { voice, model, stability, similarityBoost, suppressError } = params;
|
|
@@ -15,45 +15,55 @@ export const ttsElevenlabsAgent = async ({ namedInputs, params, config, }) => {
|
|
|
15
15
|
cause: agentGenerationError("ttsElevenlabsAgent", audioAction, audioFileTarget),
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
const requestBody = {
|
|
19
|
+
text,
|
|
20
|
+
model_id: model ?? provider2TTSAgent.elevenlabs.defaultModel,
|
|
21
|
+
voice_settings: {
|
|
22
|
+
stability: stability ?? 0.5,
|
|
23
|
+
similarity_boost: similarityBoost ?? 0.75,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
GraphAILogger.log("ElevenLabs TTS options", requestBody);
|
|
27
|
+
const response = await (async () => {
|
|
28
|
+
try {
|
|
29
|
+
return await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice}`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
Accept: "audio/mpeg",
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"xi-api-key": apiKey,
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify(requestBody),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (suppressError) {
|
|
41
|
+
return {
|
|
42
|
+
error: e,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
GraphAILogger.info(e);
|
|
46
|
+
throw new Error("TTS Eleven Labs Error", {
|
|
39
47
|
cause: agentGenerationError("ttsElevenlabsAgent", audioAction, audioFileTarget),
|
|
40
48
|
});
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
50
|
+
})();
|
|
51
|
+
if ("error" in response) {
|
|
52
|
+
return response;
|
|
45
53
|
}
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
if (response.status === 401) {
|
|
56
|
+
throw new Error("Failed to generate audio: 401 Incorrect API key provided with ElevenLabs", {
|
|
57
|
+
cause: agentIncorrectAPIKeyError("ttsElevenlabsAgent", audioAction, audioFileTarget),
|
|
58
|
+
});
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
throw new Error("TTS Eleven Labs Error", {
|
|
60
|
+
throw new Error(`Eleven Labs API error: ${response.status} ${response.statusText}`, {
|
|
54
61
|
cause: agentGenerationError("ttsElevenlabsAgent", audioAction, audioFileTarget),
|
|
55
62
|
});
|
|
56
63
|
}
|
|
64
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
65
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
66
|
+
return { buffer };
|
|
57
67
|
};
|
|
58
68
|
const ttsElevenlabsAgentInfo = {
|
|
59
69
|
name: "ttsElevenlabsAgent",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GraphAILogger } from "graphai";
|
|
2
2
|
import { GoogleGenAI } from "@google/genai";
|
|
3
3
|
import { provider2TTSAgent } from "../utils/provider2agent.js";
|
|
4
|
-
import { apiKeyMissingError, agentGenerationError, audioAction, audioFileTarget } from "../utils/error_cause.js";
|
|
4
|
+
import { agentIncorrectAPIKeyError, apiKeyMissingError, agentGenerationError, audioAction, audioFileTarget, getGenAIErrorReason, } from "../utils/error_cause.js";
|
|
5
5
|
import { pcmToMp3 } from "../utils/ffmpeg_utils.js";
|
|
6
6
|
export const ttsGeminiAgent = async ({ namedInputs, params, config, }) => {
|
|
7
7
|
const { text } = namedInputs;
|
|
@@ -29,7 +29,7 @@ export const ttsGeminiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
29
29
|
const inlineData = response.candidates?.[0]?.content?.parts?.[0]?.inlineData;
|
|
30
30
|
const pcmBase64 = inlineData?.data;
|
|
31
31
|
const mimeType = inlineData?.mimeType;
|
|
32
|
-
if (!pcmBase64)
|
|
32
|
+
if (!pcmBase64 || typeof pcmBase64 !== "string")
|
|
33
33
|
throw new Error("No audio data returned");
|
|
34
34
|
// Extract sample rate from mimeType (e.g., "audio/L16;codec=pcm;rate=24000")
|
|
35
35
|
const rateMatch = mimeType?.match(/rate=(\d+)/);
|
|
@@ -44,6 +44,12 @@ export const ttsGeminiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
GraphAILogger.info(e);
|
|
47
|
+
const reasonDetail = getGenAIErrorReason(e);
|
|
48
|
+
if (reasonDetail && reasonDetail.reason && reasonDetail.reason === "API_KEY_INVALID") {
|
|
49
|
+
throw new Error("Failed to generate tts: 400 Incorrect API key provided with gemini", {
|
|
50
|
+
cause: agentIncorrectAPIKeyError("ttsGeminiAgent", audioAction, audioFileTarget),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
47
53
|
throw new Error("TTS Gemini Error", {
|
|
48
54
|
cause: agentGenerationError("ttsGeminiAgent", audioAction, audioFileTarget),
|
|
49
55
|
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AgentFunction, AgentFunctionInfo } from "graphai";
|
|
2
|
+
import type { KotodamaTTSAgentParams, AgentBufferResult, AgentTextInputs, AgentErrorResult, AgentConfig } from "../types/agent.js";
|
|
3
|
+
export declare const ttsKotodamaAgent: AgentFunction<KotodamaTTSAgentParams, AgentBufferResult | AgentErrorResult, AgentTextInputs, AgentConfig>;
|
|
4
|
+
declare const ttsKotodamaAgentInfo: AgentFunctionInfo;
|
|
5
|
+
export default ttsKotodamaAgentInfo;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { GraphAILogger } from "graphai";
|
|
2
|
+
import { provider2TTSAgent } from "../utils/provider2agent.js";
|
|
3
|
+
import { apiKeyMissingError, agentIncorrectAPIKeyError, agentGenerationError, audioAction, audioFileTarget } from "../utils/error_cause.js";
|
|
4
|
+
export const ttsKotodamaAgent = async ({ namedInputs, params, config, }) => {
|
|
5
|
+
const { text } = namedInputs;
|
|
6
|
+
const { voice, decoration, suppressError } = params;
|
|
7
|
+
const { apiKey } = config ?? {};
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
throw new Error("Kotodama API key is required (KOTODAMA_API_KEY)", {
|
|
10
|
+
cause: apiKeyMissingError("ttsKotodamaAgent", audioAction, "KOTODAMA_API_KEY"),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const url = "https://tts3.spiral-ai-app.com/api/tts_generate";
|
|
14
|
+
const body = {
|
|
15
|
+
text,
|
|
16
|
+
speaker_id: voice ?? provider2TTSAgent.kotodama.defaultVoice,
|
|
17
|
+
decoration_id: decoration ?? provider2TTSAgent.kotodama.defaultDecoration,
|
|
18
|
+
audio_format: "mp3",
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"X-API-Key": apiKey,
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
if (response.status === 401) {
|
|
31
|
+
throw new Error("Failed to generate audio: 401 Incorrect API key provided with Kotodama", {
|
|
32
|
+
cause: agentIncorrectAPIKeyError("ttsKotodamaAgent", audioAction, audioFileTarget),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Kotodama API error: ${response.status} ${response.statusText}`, {
|
|
36
|
+
cause: agentGenerationError("ttsKotodamaAgent", audioAction, audioFileTarget),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// Response is JSON with base64-encoded audio in "audios" array
|
|
40
|
+
const json = await response.json();
|
|
41
|
+
if (!json.audios || !json.audios[0]) {
|
|
42
|
+
throw new Error("TTS Kotodama Error: No audio data in response", {
|
|
43
|
+
cause: agentGenerationError("ttsKotodamaAgent", audioAction, audioFileTarget),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const buffer = Buffer.from(json.audios[0], "base64");
|
|
47
|
+
return { buffer };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (suppressError) {
|
|
51
|
+
return {
|
|
52
|
+
error,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
GraphAILogger.error(error);
|
|
56
|
+
if (error && typeof error === "object" && "cause" in error) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
throw new Error("TTS Kotodama Error", {
|
|
60
|
+
cause: agentGenerationError("ttsKotodamaAgent", audioAction, audioFileTarget),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const ttsKotodamaAgentInfo = {
|
|
65
|
+
name: "ttsKotodamaAgent",
|
|
66
|
+
agent: ttsKotodamaAgent,
|
|
67
|
+
mock: ttsKotodamaAgent,
|
|
68
|
+
samples: [],
|
|
69
|
+
description: "Kotodama TTS agent (SpiralAI)",
|
|
70
|
+
category: ["tts"],
|
|
71
|
+
author: "Receptron Team",
|
|
72
|
+
repository: "https://github.com/receptron/mulmocast-cli",
|
|
73
|
+
license: "MIT",
|
|
74
|
+
environmentVariables: ["KOTODAMA_API_KEY"],
|
|
75
|
+
};
|
|
76
|
+
export default ttsKotodamaAgentInfo;
|
|
@@ -34,7 +34,7 @@ export const ttsOpenaiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
34
34
|
}
|
|
35
35
|
GraphAILogger.error(error);
|
|
36
36
|
if (error instanceof AuthenticationError) {
|
|
37
|
-
throw new Error("Failed to generate
|
|
37
|
+
throw new Error("Failed to generate audio: 401 Incorrect API key provided with OpenAI", {
|
|
38
38
|
cause: agentIncorrectAPIKeyError("ttsOpenaiAgent", audioAction, audioFileTarget),
|
|
39
39
|
});
|
|
40
40
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/utils/const.d.ts
CHANGED
package/lib/utils/const.js
CHANGED
|
@@ -197,3 +197,13 @@ export declare const translateApiKeyMissingError: () => {
|
|
|
197
197
|
export declare const hasCause: (err: unknown) => err is Error & {
|
|
198
198
|
cause: unknown;
|
|
199
199
|
};
|
|
200
|
+
type Result<T> = {
|
|
201
|
+
ok: true;
|
|
202
|
+
value: T;
|
|
203
|
+
} | {
|
|
204
|
+
ok: false;
|
|
205
|
+
error: Error;
|
|
206
|
+
};
|
|
207
|
+
export declare function resultify<T>(fn: () => Promise<T>): Promise<Result<T>>;
|
|
208
|
+
export declare const getGenAIErrorReason: (error: Error) => any;
|
|
209
|
+
export {};
|