mulmocast 2.16.0 → 2.18.0
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 +110 -38
- package/lib/actions/captions.js +1 -1
- package/lib/actions/image_agents.d.ts +1 -0
- package/lib/actions/image_agents.js +2 -2
- package/lib/actions/image_references.d.ts +2 -0
- package/lib/actions/image_references.js +3 -1
- package/lib/actions/images.d.ts +2 -0
- package/lib/actions/images.js +1 -0
- package/lib/actions/pdf.js +23 -2
- package/lib/agents/image_openai_agent.js +4 -3
- package/lib/agents/movie_genai_agent.d.ts +24 -0
- package/lib/agents/movie_genai_agent.js +56 -43
- package/lib/agents/tts_gemini_agent.d.ts +56 -0
- package/lib/agents/tts_gemini_agent.js +43 -38
- package/lib/cli/commands/bundle/builder.d.ts +2 -0
- package/lib/cli/commands/html/builder.d.ts +2 -0
- package/lib/cli/commands/markdown/builder.d.ts +2 -0
- package/lib/cli/commands/viewer/builder.d.ts +2 -0
- package/lib/cli/common.d.ts +2 -0
- package/lib/cli/common.js +5 -0
- package/lib/cli/helpers.js +3 -1
- package/lib/methods/mulmo_studio_context.d.ts +4 -0
- package/lib/methods/mulmo_studio_context.js +7 -0
- package/lib/types/provider2agent.d.ts +3 -4
- package/lib/types/provider2agent.js +38 -26
- package/lib/types/schema.d.ts +1 -1
- package/lib/types/type.d.ts +2 -0
- package/lib/utils/estimate_usage.js +12 -11
- package/lib/utils/gemini_omni_video.d.ts +49 -0
- package/lib/utils/gemini_omni_video.js +36 -0
- package/lib/utils/gemini_tts.d.ts +45 -0
- package/lib/utils/gemini_tts.js +35 -0
- package/lib/utils/html_render.d.ts +6 -5
- package/lib/utils/html_render.js +37 -30
- package/lib/utils/image_plugins/chart.js +2 -1
- package/lib/utils/image_plugins/html_tailwind.js +5 -4
- package/lib/utils/image_plugins/markdown.js +2 -1
- package/lib/utils/image_plugins/mermaid.js +2 -1
- package/lib/utils/image_plugins/slide.js +2 -1
- package/lib/utils/image_plugins/text_slide.js +2 -1
- package/lib/utils/image_plugins/vision.js +7 -0
- package/lib/utils/render_network_guard.d.ts +2 -0
- package/lib/utils/render_network_guard.js +36 -0
- package/lib/utils/render_network_policy.d.ts +15 -0
- package/lib/utils/render_network_policy.js +85 -0
- package/package.json +2 -2
- package/scripts/test/test_genai.json +10 -9
- package/scripts/test/test_genai_movie.json +1 -1
- package/scripts/test/test_image_references.json +3 -2
- package/scripts/test/test_movie_references.json +2 -1
- package/scripts/test/test_veo31_lite.json +3 -3
|
@@ -5,11 +5,14 @@ import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, im
|
|
|
5
5
|
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
|
-
import { getModelDuration, provider2MovieAgent, AUDIO_MODE_NEVER, AUDIO_MODE_ALWAYS, defaultMovieModel,
|
|
8
|
+
import { getModelDuration, provider2MovieAgent, AUDIO_MODE_NEVER, AUDIO_MODE_ALWAYS, defaultMovieModel, unsupportedGoogleMovieModelMessage, } from "../types/provider2agent.js";
|
|
9
|
+
import { GEMINI_OMNI_MAX_TOTAL_SEC, buildGeminiOmniVideoRequest, geminiOmniAspectRatio, geminiOmniExtensionPrompt, geminiOmniSegments, isGeminiOmniVideoModel, videoFromInteraction, } from "../utils/gemini_omni_video.js";
|
|
9
10
|
// Per-request timeout so a stalled GenAI video API call rejects instead of hanging.
|
|
10
11
|
const GENAI_REQUEST_TIMEOUT_MS = 120_000;
|
|
11
12
|
// Wall-clock cap on the long-running video operation poll loop (Veo runs minutes).
|
|
12
13
|
const VIDEO_POLL_TIMEOUT_MS = 1_200_000;
|
|
14
|
+
// A Gemini Omni call answers synchronously with the whole video (10s at 720p took about 45s).
|
|
15
|
+
const GEMINI_OMNI_REQUEST_TIMEOUT_MS = 600_000;
|
|
13
16
|
const pollUntilDone = async (ai, operation) => {
|
|
14
17
|
const response = { operation };
|
|
15
18
|
const deadline = Date.now() + VIDEO_POLL_TIMEOUT_MS;
|
|
@@ -78,47 +81,53 @@ const downloadVideo = async (ai, video, movieFile, isVertexAI, model) => {
|
|
|
78
81
|
const usage = predictSec !== undefined ? { provider: "google", model, predictSec } : undefined;
|
|
79
82
|
return { saved: movieFile, usage };
|
|
80
83
|
};
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
84
|
+
const isOmniImage = (frame) => frame !== undefined;
|
|
85
|
+
const nextOmniRequest = (input, durationSec, previous, frames) => {
|
|
86
|
+
if (!previous)
|
|
87
|
+
return buildGeminiOmniVideoRequest({ ...input, durationSec, frames });
|
|
88
|
+
if (!previous.id)
|
|
89
|
+
throw new Error("Gemini Omni returned no interaction id to extend");
|
|
90
|
+
return buildGeminiOmniVideoRequest({ ...input, prompt: geminiOmniExtensionPrompt(input.prompt), durationSec, previousInteractionId: previous.id });
|
|
91
|
+
};
|
|
92
|
+
// Each call after the first extends the previous interaction's video and answers with the whole video so far.
|
|
93
|
+
export const generateGeminiOmniVideo = async (ai, input) => {
|
|
94
|
+
const segments = geminiOmniSegments(input.requestedSec);
|
|
95
|
+
const frames = [input.firstFrame, segments.length === 1 ? input.lastFrame : undefined].filter(isOmniImage);
|
|
96
|
+
const last = await segments.reduce(async (previousPromise, durationSec) => {
|
|
97
|
+
const request = nextOmniRequest(input, durationSec, await previousPromise, frames);
|
|
98
|
+
return ai.interactions.create(request, { timeout: GEMINI_OMNI_REQUEST_TIMEOUT_MS });
|
|
99
|
+
}, Promise.resolve(undefined));
|
|
100
|
+
return videoFromInteraction(last?.output_video);
|
|
101
|
+
};
|
|
102
|
+
const omniImage = (imagePath) => {
|
|
103
|
+
const { imageBytes, mimeType } = loadImageAsBase64(imagePath);
|
|
104
|
+
return { data: imageBytes, mime_type: mimeType };
|
|
105
|
+
};
|
|
106
|
+
const omniFrames = (input, { imagePath, lastFrameImagePath, referenceImages }) => {
|
|
107
|
+
if (referenceImages && referenceImages.length > 0) {
|
|
108
|
+
GraphAILogger.warn(`movieGenAIAgent: model ${input.model} does not support referenceImages — ignoring`);
|
|
109
|
+
}
|
|
110
|
+
if (lastFrameImagePath && !imagePath) {
|
|
111
|
+
GraphAILogger.warn(`movieGenAIAgent: lastFrame requires a first frame image (imagePrompt or firstFrameImageName) — ignoring lastFrameImageName`);
|
|
112
|
+
}
|
|
113
|
+
else if (lastFrameImagePath && geminiOmniSegments(input.requestedSec).length > 1) {
|
|
114
|
+
GraphAILogger.warn(`movieGenAIAgent: lastFrame applies to a single ${input.model} segment — ignoring lastFrameImageName for a ${input.requestedSec}s beat`);
|
|
115
|
+
}
|
|
116
|
+
const lastFrame = lastFrameImagePath && imagePath ? omniImage(lastFrameImagePath) : undefined;
|
|
117
|
+
return { firstFrame: imagePath ? omniImage(imagePath) : undefined, lastFrame };
|
|
118
|
+
};
|
|
119
|
+
const generateOmniVideoFile = async (ai, isVertexAI, input, sources, movieFile) => {
|
|
120
|
+
if (isVertexAI) {
|
|
121
|
+
throw new Error(`Model ${input.model} is supported on the Gemini API only; use veo-3.1-generate-001 with Vertex AI.`, {
|
|
122
|
+
cause: agentGenerationError("movieGenAIAgent", imageAction, unsupportedModelTarget),
|
|
119
123
|
});
|
|
120
124
|
}
|
|
121
|
-
|
|
125
|
+
if (input.requestedSec > GEMINI_OMNI_MAX_TOTAL_SEC) {
|
|
126
|
+
GraphAILogger.warn(`movieGenAIAgent: ${input.model} makes at most ${GEMINI_OMNI_MAX_TOTAL_SEC}s — the ${input.requestedSec}s beat gets a shorter video`);
|
|
127
|
+
}
|
|
128
|
+
writeFileSync(movieFile, await generateGeminiOmniVideo(ai, { ...input, ...omniFrames(input, sources) }));
|
|
129
|
+
const predictSec = await probeDurationSec(movieFile);
|
|
130
|
+
return { saved: movieFile, usage: predictSec !== undefined ? { provider: "google", model: input.model, predictSec } : undefined };
|
|
122
131
|
};
|
|
123
132
|
const generateStandardVideo = async (ai, model, prompt, aspectRatio, imagePath, lastFrameImagePath, referenceImages, duration, movieFile, isVertexAI) => {
|
|
124
133
|
const capabilities = provider2MovieAgent.google.modelParams[model];
|
|
@@ -173,6 +182,10 @@ export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
173
182
|
const model = params.model ?? defaultMovieModel("google", params);
|
|
174
183
|
const apiKey = config?.apiKey;
|
|
175
184
|
try {
|
|
185
|
+
const unsupportedMessage = unsupportedGoogleMovieModelMessage(model);
|
|
186
|
+
if (unsupportedMessage) {
|
|
187
|
+
throw new Error(unsupportedMessage, { cause: agentGenerationError("movieGenAIAgent", imageAction, unsupportedModelTarget) });
|
|
188
|
+
}
|
|
176
189
|
const requestedDuration = params.duration ?? 8;
|
|
177
190
|
const duration = getModelDuration("google", model, requestedDuration);
|
|
178
191
|
if (duration === undefined) {
|
|
@@ -208,9 +221,9 @@ export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
208
221
|
}
|
|
209
222
|
return new GoogleGenAI({ apiKey, httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS } });
|
|
210
223
|
})();
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
224
|
+
if (isGeminiOmniVideoModel(model)) {
|
|
225
|
+
const omniInput = { model, prompt, aspectRatio: geminiOmniAspectRatio(params.canvasSize), requestedSec: requestedDuration };
|
|
226
|
+
return generateOmniVideoFile(ai, isVertexAI, omniInput, { imagePath, lastFrameImagePath, referenceImages }, movieFile);
|
|
214
227
|
}
|
|
215
228
|
// Standard mode
|
|
216
229
|
return generateStandardVideo(ai, model, prompt, aspectRatio, imagePath, lastFrameImagePath, referenceImages, duration, movieFile, isVertexAI);
|
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
import type { AgentFunction, AgentFunctionInfo } from "graphai";
|
|
2
|
+
import { type GenerateContentConfig } from "@google/genai";
|
|
3
|
+
import { buildGeminiInteractionsTtsRequest, type GeminiInteractionsTtsInput } from "../utils/gemini_tts.js";
|
|
2
4
|
import type { GoogleTTSAgentParams, AgentBufferResult, AgentTextInputs, AgentErrorResult } from "../types/agent.js";
|
|
5
|
+
import type { AgentUsage } from "../types/usage.js";
|
|
6
|
+
type GeminiAudio = {
|
|
7
|
+
rawPcm: Buffer;
|
|
8
|
+
sampleRate: number;
|
|
9
|
+
usage: AgentUsage | undefined;
|
|
10
|
+
};
|
|
11
|
+
type InlineAudioResponse = {
|
|
12
|
+
candidates?: {
|
|
13
|
+
content?: {
|
|
14
|
+
parts?: {
|
|
15
|
+
inlineData?: {
|
|
16
|
+
data?: string;
|
|
17
|
+
mimeType?: string;
|
|
18
|
+
};
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
}[];
|
|
22
|
+
usageMetadata?: {
|
|
23
|
+
promptTokenCount?: number;
|
|
24
|
+
candidatesTokenCount?: number;
|
|
25
|
+
totalTokenCount?: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
type InteractionAudioResponse = {
|
|
29
|
+
output_audio?: {
|
|
30
|
+
data?: string;
|
|
31
|
+
mime_type?: string;
|
|
32
|
+
sample_rate?: number;
|
|
33
|
+
};
|
|
34
|
+
usage?: {
|
|
35
|
+
total_input_tokens?: number;
|
|
36
|
+
total_output_tokens?: number;
|
|
37
|
+
total_tokens?: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export type GeminiTtsClient = {
|
|
41
|
+
models: {
|
|
42
|
+
generateContent: (params: {
|
|
43
|
+
model: string;
|
|
44
|
+
contents: {
|
|
45
|
+
parts: {
|
|
46
|
+
text: string;
|
|
47
|
+
}[];
|
|
48
|
+
}[];
|
|
49
|
+
config: GenerateContentConfig;
|
|
50
|
+
}) => Promise<InlineAudioResponse>;
|
|
51
|
+
};
|
|
52
|
+
interactions: {
|
|
53
|
+
create: (params: ReturnType<typeof buildGeminiInteractionsTtsRequest>, options?: {
|
|
54
|
+
timeout?: number;
|
|
55
|
+
}) => Promise<InteractionAudioResponse>;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export declare const generateGeminiTts: (ai: GeminiTtsClient, input: GeminiInteractionsTtsInput) => Promise<GeminiAudio>;
|
|
3
59
|
export declare const ttsGeminiAgent: AgentFunction<GoogleTTSAgentParams, AgentBufferResult | AgentErrorResult, AgentTextInputs>;
|
|
4
60
|
declare const ttsGeminiAgentInfo: AgentFunctionInfo;
|
|
5
61
|
export default ttsGeminiAgentInfo;
|
|
@@ -3,16 +3,51 @@ 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
|
+
import { GEMINI_TTS_SAMPLE_RATE, buildGeminiInteractionsTtsRequest, geminiDirectorsNotesPrompt, pcmFromInteractionAudio, usesGeminiInteractionsTts, } from "../utils/gemini_tts.js";
|
|
6
7
|
// Per-request timeout so a stalled GenAI TTS call rejects (and GraphAI retry can
|
|
7
8
|
// recover) instead of hanging. Generous vs. normal generation time.
|
|
8
9
|
const GENAI_REQUEST_TIMEOUT_MS = 120_000;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const geminiUsage = (model, inputTokens, outputTokens, totalTokens) => ({
|
|
11
|
+
provider: "gemini",
|
|
12
|
+
model,
|
|
13
|
+
inputTokens,
|
|
14
|
+
outputTokens,
|
|
15
|
+
totalTokens,
|
|
16
|
+
});
|
|
17
|
+
// 2.5 TTS: generateContent, direction embedded in the prompt, headerless PCM with the rate in the mimeType.
|
|
18
|
+
const generateWithContent = async (ai, { model, text, voice, instructions }) => {
|
|
19
|
+
const response = await ai.models.generateContent({
|
|
20
|
+
model,
|
|
21
|
+
contents: [{ parts: [{ text: geminiDirectorsNotesPrompt(text, instructions) }] }],
|
|
22
|
+
config: {
|
|
23
|
+
responseModalities: ["AUDIO"],
|
|
24
|
+
speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } } },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const inlineData = response.candidates?.[0]?.content?.parts?.[0]?.inlineData;
|
|
28
|
+
const pcmBase64 = inlineData?.data;
|
|
29
|
+
if (!pcmBase64 || typeof pcmBase64 !== "string")
|
|
30
|
+
throw new Error("No audio data returned");
|
|
31
|
+
// Extract sample rate from mimeType (e.g., "audio/L16;codec=pcm;rate=24000")
|
|
32
|
+
const rateMatch = inlineData?.mimeType?.match(/rate=(\d+)/);
|
|
33
|
+
const sampleRate = rateMatch ? parseInt(rateMatch[1]) : GEMINI_TTS_SAMPLE_RATE;
|
|
34
|
+
const metadata = response.usageMetadata;
|
|
35
|
+
return {
|
|
36
|
+
rawPcm: Buffer.from(pcmBase64, "base64"),
|
|
37
|
+
sampleRate,
|
|
38
|
+
usage: metadata ? geminiUsage(model, metadata.promptTokenCount, metadata.candidatesTokenCount, metadata.totalTokenCount) : undefined,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const generateWithInteractions = async (ai, input) => {
|
|
42
|
+
// Passed per request too: the Interactions client is built apart from the models client.
|
|
43
|
+
const interaction = await ai.interactions.create(buildGeminiInteractionsTtsRequest(input), { timeout: GENAI_REQUEST_TIMEOUT_MS });
|
|
44
|
+
const usage = interaction.usage;
|
|
45
|
+
return {
|
|
46
|
+
...pcmFromInteractionAudio(interaction.output_audio),
|
|
47
|
+
usage: usage ? geminiUsage(input.model, usage.total_input_tokens, usage.total_output_tokens, usage.total_tokens) : undefined,
|
|
48
|
+
};
|
|
15
49
|
};
|
|
50
|
+
export const generateGeminiTts = (ai, input) => usesGeminiInteractionsTts(input.model) ? generateWithInteractions(ai, input) : generateWithContent(ai, input);
|
|
16
51
|
export const ttsGeminiAgent = async ({ namedInputs, params, config, }) => {
|
|
17
52
|
const { text } = namedInputs;
|
|
18
53
|
const { model, voice, suppressError, instructions } = params;
|
|
@@ -25,38 +60,8 @@ export const ttsGeminiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
25
60
|
const geminiResult = await (async () => {
|
|
26
61
|
try {
|
|
27
62
|
const ai = new GoogleGenAI({ apiKey, httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS } });
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
contents: [{ parts: [{ text: getPrompt(text, instructions) }] }],
|
|
31
|
-
config: {
|
|
32
|
-
responseModalities: ["AUDIO"],
|
|
33
|
-
speechConfig: {
|
|
34
|
-
voiceConfig: {
|
|
35
|
-
prebuiltVoiceConfig: { voiceName: voice ?? provider2TTSAgent.gemini.defaultVoice },
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
const inlineData = response.candidates?.[0]?.content?.parts?.[0]?.inlineData;
|
|
41
|
-
const pcmBase64 = inlineData?.data;
|
|
42
|
-
const mimeType = inlineData?.mimeType;
|
|
43
|
-
if (!pcmBase64 || typeof pcmBase64 !== "string")
|
|
44
|
-
throw new Error("No audio data returned");
|
|
45
|
-
// Extract sample rate from mimeType (e.g., "audio/L16;codec=pcm;rate=24000")
|
|
46
|
-
const rateMatch = mimeType?.match(/rate=(\d+)/);
|
|
47
|
-
const sampleRate = rateMatch ? parseInt(rateMatch[1]) : 24000;
|
|
48
|
-
const rawPcm = Buffer.from(pcmBase64, "base64");
|
|
49
|
-
const usedModel = model ?? provider2TTSAgent.gemini.defaultModel;
|
|
50
|
-
const usage = response.usageMetadata
|
|
51
|
-
? {
|
|
52
|
-
provider: "gemini",
|
|
53
|
-
model: usedModel,
|
|
54
|
-
inputTokens: response.usageMetadata.promptTokenCount,
|
|
55
|
-
outputTokens: response.usageMetadata.candidatesTokenCount,
|
|
56
|
-
totalTokens: response.usageMetadata.totalTokenCount,
|
|
57
|
-
}
|
|
58
|
-
: undefined;
|
|
59
|
-
return { rawPcm, sampleRate, usage };
|
|
63
|
+
const request = { model: model ?? provider2TTSAgent.gemini.defaultModel, text, voice: voice ?? provider2TTSAgent.gemini.defaultVoice, instructions };
|
|
64
|
+
return await generateGeminiTts(ai, request);
|
|
60
65
|
}
|
|
61
66
|
catch (e) {
|
|
62
67
|
if (suppressError) {
|
package/lib/cli/common.d.ts
CHANGED
package/lib/cli/common.js
CHANGED
|
@@ -49,6 +49,11 @@ export const commonOptions = (yargs) => {
|
|
|
49
49
|
describe: "create backup media file",
|
|
50
50
|
type: "boolean",
|
|
51
51
|
default: false,
|
|
52
|
+
})
|
|
53
|
+
.option("strict-network", {
|
|
54
|
+
describe: "Let HTML renderers reach only local files and the CDNs mulmocast's templates use (also MULMO_STRICT_NETWORK=1)",
|
|
55
|
+
type: "boolean",
|
|
56
|
+
default: false,
|
|
52
57
|
})
|
|
53
58
|
.option("p", {
|
|
54
59
|
alias: "presentationStyle",
|
package/lib/cli/helpers.js
CHANGED
|
@@ -10,6 +10,7 @@ import { translate } from "../actions/translate.js";
|
|
|
10
10
|
import { initializeContextFromFiles } from "../utils/context.js";
|
|
11
11
|
import { estimateUsage, actionEstimateProcesses } from "../utils/estimate_usage.js";
|
|
12
12
|
import { formatUsageEstimates } from "../utils/estimate_usage_format.js";
|
|
13
|
+
import { strictNetworkFromEnv } from "../utils/render_network_policy.js";
|
|
13
14
|
export const runTranslateIfNeeded = async (context, includeCaption = false) => {
|
|
14
15
|
if (MulmoStudioContextMethods.needTranslate(context, includeCaption)) {
|
|
15
16
|
GraphAILogger.log("run translate");
|
|
@@ -151,5 +152,6 @@ export const initializeContext = async (argv, raiseError = false) => {
|
|
|
151
152
|
grouped: Boolean(argv.g),
|
|
152
153
|
});
|
|
153
154
|
setGraphAILogger(Boolean(argv.v), { files });
|
|
154
|
-
|
|
155
|
+
const context = await initializeContextFromFiles(files, raiseError, Boolean(argv.f), Boolean(argv.backup), argv.c, argv.l);
|
|
156
|
+
return context && { ...context, strictNetwork: Boolean(argv.strictNetwork) || strictNetworkFromEnv(process.env) };
|
|
155
157
|
};
|
|
@@ -12,6 +12,10 @@ export declare const MulmoStudioContextMethods: {
|
|
|
12
12
|
getImageDirPath(context: MulmoStudioContext): string;
|
|
13
13
|
getImageProjectDirPath(context: MulmoStudioContext): string;
|
|
14
14
|
getOutDirPath(context: MulmoStudioContext): string;
|
|
15
|
+
getRenderNetworkOptions(context: MulmoStudioContext): {
|
|
16
|
+
strictNetwork: boolean;
|
|
17
|
+
allowedFileRoots: string[];
|
|
18
|
+
};
|
|
15
19
|
getFileName(context: MulmoStudioContext): string;
|
|
16
20
|
getCaption(context: MulmoStudioContext): string | undefined;
|
|
17
21
|
setSessionState(context: MulmoStudioContext, sessionType: SessionType, value: boolean, result?: boolean): void;
|
|
@@ -53,6 +53,13 @@ export const MulmoStudioContextMethods = {
|
|
|
53
53
|
getOutDirPath(context) {
|
|
54
54
|
return context.fileDirs.outDirPath;
|
|
55
55
|
},
|
|
56
|
+
// In strict network mode a renderer may read files only from the script's folder and mulmocast's output folders.
|
|
57
|
+
// A script loaded by URL has no local folder: its mulmoFileDirPath is the parent of the base directory.
|
|
58
|
+
getRenderNetworkOptions(context) {
|
|
59
|
+
const { mulmoFileDirPath, outDirPath, imageDirPath, audioDirPath, isHttpPath } = context.fileDirs;
|
|
60
|
+
const scriptRoots = isHttpPath ? [] : [mulmoFileDirPath];
|
|
61
|
+
return { strictNetwork: Boolean(context.strictNetwork), allowedFileRoots: [...scriptRoots, outDirPath, imageDirPath, audioDirPath] };
|
|
62
|
+
},
|
|
56
63
|
getFileName(context) {
|
|
57
64
|
return context.studio.filename;
|
|
58
65
|
},
|
|
@@ -42,6 +42,8 @@ export declare const provider2TTSAgent: {
|
|
|
42
42
|
};
|
|
43
43
|
};
|
|
44
44
|
export declare const gptImages: string[];
|
|
45
|
+
export declare const isGptImage25Model: (model: string) => boolean;
|
|
46
|
+
export declare const gptImageQuality: <Quality extends string>(model: string, quality?: Quality) => Quality | "high" | undefined;
|
|
45
47
|
export declare const deprecatedOpenAIImageModelHints: {
|
|
46
48
|
readonly "dall-e-2": "Use 'gpt-image-2.5-sunburst' or another supported model.";
|
|
47
49
|
readonly "dall-e-3": "Use 'gpt-image-2.5-sunburst' or another supported model.";
|
|
@@ -231,13 +233,10 @@ export declare const ProviderName: {
|
|
|
231
233
|
export declare const isImageProvider: (provider: string | undefined) => provider is keyof typeof provider2ImageAgent;
|
|
232
234
|
export declare const getMaxImageReferenceImages: (provider: string | undefined, model: string) => number | undefined;
|
|
233
235
|
export declare const getModelAudio: (provider: keyof typeof provider2MovieAgent, model: string) => MovieAudioSpec | undefined;
|
|
236
|
+
export declare const unsupportedGoogleMovieModelMessage: (model: string) => string | null;
|
|
234
237
|
export declare const defaultMovieModel: (provider: keyof typeof provider2MovieAgent, movieParams?: {
|
|
235
238
|
vertexai_project?: string;
|
|
236
239
|
}) => string;
|
|
237
|
-
export declare const VEO_EXTENSION_MODEL = "veo-3.1-generate-preview";
|
|
238
|
-
export declare const VEO_SEGMENT_SEC = 8;
|
|
239
|
-
export declare const veoExtensionCount: (requestedSec: number) => number;
|
|
240
|
-
export declare const veoExtendedSeconds: (requestedSec: number) => number;
|
|
241
240
|
export declare const getModelDuration: (provider: keyof typeof provider2MovieAgent, model: string, movieDuration?: number) => number | undefined;
|
|
242
241
|
export type ModelPricing = {
|
|
243
242
|
unit: "tokens" | "chars" | "seconds" | "images";
|
|
@@ -16,9 +16,10 @@ export const provider2TTSAgent = {
|
|
|
16
16
|
gemini: {
|
|
17
17
|
agentName: "ttsGeminiAgent",
|
|
18
18
|
hasLimitedConcurrency: false,
|
|
19
|
-
|
|
19
|
+
// The 2.5 preview TTS models shut down on 2026-11-17; Google names gemini-3.8-flash-tts as their successor.
|
|
20
|
+
defaultModel: "gemini-3.8-flash-tts",
|
|
20
21
|
defaultVoice: "Kore",
|
|
21
|
-
models: ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"],
|
|
22
|
+
models: ["gemini-3.8-flash-tts", "gemini-3.8-flash-lite-tts", "gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"],
|
|
22
23
|
keyName: "GEMINI_API_KEY",
|
|
23
24
|
},
|
|
24
25
|
elevenlabs: {
|
|
@@ -45,6 +46,11 @@ export const provider2TTSAgent = {
|
|
|
45
46
|
},
|
|
46
47
|
};
|
|
47
48
|
export const gptImages = ["gpt-image-2.5-sunburst", "gpt-image-2.5-flare", "gpt-image-2", "gpt-image-1.5", "gpt-image-1", "gpt-image-1-mini"];
|
|
49
|
+
// The 2.5 models render "auto" (their default) at "low", where gpt-image-1 rendered it at "high".
|
|
50
|
+
export const isGptImage25Model = (model) => model.startsWith("gpt-image-2.5");
|
|
51
|
+
// An unset quality on 2.5 is sent as "high" to keep the look scripts had before the default model changed;
|
|
52
|
+
// an explicit "auto" is the script's choice and is sent as is.
|
|
53
|
+
export const gptImageQuality = (model, quality) => quality ?? (isGptImage25Model(model) ? "high" : undefined);
|
|
48
54
|
const supportedOpenAIImageReplacementHint = "Use 'gpt-image-2.5-sunburst' or another supported model.";
|
|
49
55
|
export const deprecatedOpenAIImageModelHints = {
|
|
50
56
|
"dall-e-2": supportedOpenAIImageReplacementHint,
|
|
@@ -377,20 +383,21 @@ export const provider2MovieAgent = {
|
|
|
377
383
|
},
|
|
378
384
|
google: {
|
|
379
385
|
agentName: "movieGenAIAgent",
|
|
380
|
-
//
|
|
381
|
-
defaultModel: "
|
|
382
|
-
models: ["
|
|
386
|
+
// The Veo 3.1 previews shut down on the Gemini API on 2026-10-22 (removed here ahead of that); Gemini Omni is their successor.
|
|
387
|
+
defaultModel: "gemini-omni-1.1-flash",
|
|
388
|
+
models: ["gemini-omni-1.1-flash", "veo-3.1-generate-001"],
|
|
383
389
|
keyName: "GEMINI_API_KEY",
|
|
384
390
|
modelParams: {
|
|
385
|
-
|
|
386
|
-
|
|
391
|
+
// Gemini API only. Longer beats are extended, up to 40 seconds (see gemini_omni_video.ts).
|
|
392
|
+
"gemini-omni-1.1-flash": {
|
|
393
|
+
durations: [3, 4, 5, 6, 7, 8, 9, 10],
|
|
387
394
|
supportsDuration: true,
|
|
388
395
|
supportsLastFrame: true,
|
|
389
396
|
supportsReferenceImages: false,
|
|
390
397
|
supportsPersonGeneration: false,
|
|
391
398
|
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
392
399
|
},
|
|
393
|
-
// Vertex AI only: the GA name of Veo 3.1 there
|
|
400
|
+
// Vertex AI only: the GA name of Veo 3.1 there.
|
|
394
401
|
"veo-3.1-generate-001": {
|
|
395
402
|
durations: [4, 6, 8],
|
|
396
403
|
supportsDuration: true,
|
|
@@ -399,14 +406,6 @@ export const provider2MovieAgent = {
|
|
|
399
406
|
supportsPersonGeneration: false,
|
|
400
407
|
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
401
408
|
},
|
|
402
|
-
"veo-3.1-generate-preview": {
|
|
403
|
-
durations: [4, 6, 8],
|
|
404
|
-
supportsDuration: true,
|
|
405
|
-
supportsLastFrame: true,
|
|
406
|
-
supportsReferenceImages: true,
|
|
407
|
-
supportsPersonGeneration: false,
|
|
408
|
-
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
409
|
-
},
|
|
410
409
|
},
|
|
411
410
|
},
|
|
412
411
|
mock: {
|
|
@@ -579,15 +578,26 @@ export const getModelAudio = (provider, model) => {
|
|
|
579
578
|
return modelParams?.[model]?.audio;
|
|
580
579
|
};
|
|
581
580
|
const GOOGLE_VERTEX_DEFAULT_MOVIE_MODEL = "veo-3.1-generate-001";
|
|
582
|
-
|
|
581
|
+
const removedGoogleMovieModelHint = "Its Gemini API shutdown date is 2026-10-22, so mulmocast no longer offers it. Use 'gemini-omni-1.1-flash' on the Gemini API, or 'veo-3.1-generate-001' with movieParams.vertexai_project on Vertex AI (needed for referenceImages).";
|
|
582
|
+
const removedGoogleMovieModelHints = {
|
|
583
|
+
"veo-3.1-generate-preview": removedGoogleMovieModelHint,
|
|
584
|
+
"veo-3.1-lite-generate-preview": removedGoogleMovieModelHint,
|
|
585
|
+
};
|
|
586
|
+
// An unknown model fails here with a hint, not with a TypeError in the duration lookup.
|
|
587
|
+
export const unsupportedGoogleMovieModelMessage = (model) => {
|
|
588
|
+
if (Object.hasOwn(provider2MovieAgent.google.modelParams, model))
|
|
589
|
+
return null;
|
|
590
|
+
const hint = Object.hasOwn(removedGoogleMovieModelHints, model)
|
|
591
|
+
? removedGoogleMovieModelHints[model]
|
|
592
|
+
: `Supported models: ${provider2MovieAgent.google.models.join(", ")}.`;
|
|
593
|
+
return `Google movie model "${model}" is not supported. ${hint}`;
|
|
594
|
+
};
|
|
595
|
+
// Gemini Omni is a Gemini API model here, so Vertex AI keeps Veo 3.1 as its default.
|
|
583
596
|
export const defaultMovieModel = (provider, movieParams) => provider === "google" && movieParams?.vertexai_project ? GOOGLE_VERTEX_DEFAULT_MOVIE_MODEL : provider2MovieAgent[provider].defaultModel;
|
|
584
|
-
// Veo 3.1 (preview) builds clips longer than one segment by extending an 8-second first segment 8 seconds at a time.
|
|
585
|
-
export const VEO_EXTENSION_MODEL = "veo-3.1-generate-preview";
|
|
586
|
-
export const VEO_SEGMENT_SEC = 8;
|
|
587
|
-
export const veoExtensionCount = (requestedSec) => Math.ceil((requestedSec - VEO_SEGMENT_SEC) / VEO_SEGMENT_SEC);
|
|
588
|
-
export const veoExtendedSeconds = (requestedSec) => VEO_SEGMENT_SEC * (1 + veoExtensionCount(requestedSec));
|
|
589
597
|
export const getModelDuration = (provider, model, movieDuration) => {
|
|
590
598
|
const modelParams = provider2MovieAgent[provider]?.modelParams;
|
|
599
|
+
if (!modelParams || !Object.hasOwn(modelParams, model))
|
|
600
|
+
return undefined;
|
|
591
601
|
const { durations } = modelParams[model];
|
|
592
602
|
if (durations && movieDuration) {
|
|
593
603
|
const largerDurations = durations.filter((d) => d >= movieDuration);
|
|
@@ -641,18 +651,20 @@ export const modelPricing = {
|
|
|
641
651
|
// https://ai.google.dev/gemini-api/docs/pricing
|
|
642
652
|
"gemini-2.5-flash-preview-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
643
653
|
"gemini-2.5-pro-preview-tts": { unit: "tokens", inputPerMTokensUSD: 1, outputPerMTokensUSD: 20, asOf: "2026-07-03" },
|
|
654
|
+
// through 2026-12-31; $1 / $18 (flash) and $1 / $12 (lite) from 2027-01-01
|
|
655
|
+
"gemini-3.8-flash-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 9, asOf: "2026-10-07" },
|
|
656
|
+
"gemini-3.8-flash-lite-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 6, asOf: "2026-10-07" },
|
|
644
657
|
},
|
|
645
658
|
google: {
|
|
646
659
|
// https://ai.google.dev/gemini-api/docs/pricing ($0.039/image ≒ 1290 output tokens at $30/1M)
|
|
647
660
|
"gemini-2.5-flash-image": { unit: "images", inputPerMTokensUSD: 0.3, perImageUSD: 0.039, asOf: "2026-07-03" },
|
|
648
661
|
// $0.0336 per 1K-resolution image
|
|
649
662
|
"gemini-3.1-flash-lite-image": { unit: "images", inputPerMTokensUSD: 0.25, perImageUSD: 0.0336, asOf: "2026-10-07" },
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
"
|
|
663
|
+
// Video per generated second at 720p. Shut-down models (veo-2.0, veo-3.0, the Veo 3.1 previews) intentionally have no price.
|
|
664
|
+
// Gemini Omni bills video output tokens at $17.50/1M, 5,792 tokens per second of 720p video.
|
|
665
|
+
"gemini-omni-1.1-flash": { unit: "seconds", perSecUSD: 0.1014, asOf: "2026-10-07" },
|
|
653
666
|
// Vertex AI: https://cloud.google.com/vertex-ai/generative-ai/pricing (video + audio, 720p/1080p)
|
|
654
667
|
"veo-3.1-generate-001": { unit: "seconds", perSecUSD: 0.4, asOf: "2026-10-07" },
|
|
655
|
-
"veo-3.1-lite-generate-preview": { unit: "seconds", perSecUSD: 0.05, asOf: "2026-07-03" },
|
|
656
668
|
// Google Cloud Text-to-Speech per 1M characters, keyed by voice tier.
|
|
657
669
|
// https://cloud.google.com/text-to-speech/pricing
|
|
658
670
|
"tts-standard": { unit: "chars", perMCharsUSD: 4, asOf: "2026-07-03" },
|
package/lib/types/schema.d.ts
CHANGED
|
@@ -4466,8 +4466,8 @@ export declare const mulmoOpenAIImageModelSchema: z.ZodObject<{
|
|
|
4466
4466
|
[x: string]: string;
|
|
4467
4467
|
}>>;
|
|
4468
4468
|
quality: z.ZodOptional<z.ZodEnum<{
|
|
4469
|
-
low: "low";
|
|
4470
4469
|
high: "high";
|
|
4470
|
+
low: "low";
|
|
4471
4471
|
auto: "auto";
|
|
4472
4472
|
medium: "medium";
|
|
4473
4473
|
}>>;
|
package/lib/types/type.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export type MulmoStudioContext = {
|
|
|
77
77
|
presentationStyle: MulmoPresentationStyle;
|
|
78
78
|
multiLingual: MulmoStudioMultiLingualArray;
|
|
79
79
|
usageCollector?: UsageCollectorAPI;
|
|
80
|
+
strictNetwork?: boolean;
|
|
80
81
|
};
|
|
81
82
|
export type ScriptingParams = {
|
|
82
83
|
urls: string[];
|
|
@@ -162,6 +163,7 @@ export type InitOptions = {
|
|
|
162
163
|
c?: string;
|
|
163
164
|
p?: string;
|
|
164
165
|
g?: boolean;
|
|
166
|
+
strictNetwork?: boolean;
|
|
165
167
|
};
|
|
166
168
|
export type PublicAPIArgs = {
|
|
167
169
|
settings?: Record<string, string>;
|