mulmocast 2.16.0 → 2.17.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 +1 -1
- package/lib/actions/image_references.d.ts +2 -0
- package/lib/actions/image_references.js +3 -1
- package/lib/agents/image_openai_agent.js +4 -3
- package/lib/agents/tts_gemini_agent.d.ts +56 -0
- package/lib/agents/tts_gemini_agent.js +43 -38
- package/lib/types/provider2agent.d.ts +2 -0
- package/lib/types/provider2agent.js +11 -2
- package/lib/types/schema.d.ts +1 -1
- package/lib/utils/estimate_usage.js +7 -7
- package/lib/utils/gemini_tts.d.ts +45 -0
- package/lib/utils/gemini_tts.js +35 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -404,7 +404,7 @@ Payload shape:
|
|
|
404
404
|
"records": 3,
|
|
405
405
|
"byModel": [
|
|
406
406
|
{ "provider": "openai", "model": "gpt-4o-mini-tts", "records": 1, "inputChars": 20 },
|
|
407
|
-
{ "provider": "gemini", "model": "gemini-
|
|
407
|
+
{ "provider": "gemini", "model": "gemini-3.8-flash-tts", "records": 1, "inputTokens": 9, "outputTokens": 59, "totalTokens": 68 },
|
|
408
408
|
{ "provider": "elevenlabs", "model": "eleven_multilingual_v2", "records": 1, "inputChars": 18 }
|
|
409
409
|
],
|
|
410
410
|
"snapshot": [ /* per-call UsageRecord[] */ ]
|
|
@@ -6,10 +6,12 @@ type ImageAgentInfo = ReturnType<typeof MulmoPresentationStyleMethods.getImageAg
|
|
|
6
6
|
type MovieAgentInfo = ReturnType<typeof MulmoPresentationStyleMethods.getMovieAgentInfo>;
|
|
7
7
|
export declare const referenceImageGraphParams: (imageAgentInfo: ImageAgentInfo, canvasSize: MulmoCanvasDimension) => {
|
|
8
8
|
model: string | undefined;
|
|
9
|
+
moderation: string | undefined;
|
|
9
10
|
canvasSize: {
|
|
10
11
|
width: number;
|
|
11
12
|
height: number;
|
|
12
13
|
};
|
|
14
|
+
quality: string | undefined;
|
|
13
15
|
vertexai_project: string | undefined;
|
|
14
16
|
vertexai_location: string | undefined;
|
|
15
17
|
};
|
|
@@ -6,10 +6,12 @@ import { MulmoPresentationStyleMethods, MulmoMediaSourceMethods, MulmoImagePromp
|
|
|
6
6
|
import { ImageMediaType, } from "../types/index.js";
|
|
7
7
|
import { imageOpenaiAgent, mediaMockAgent, imageGenAIAgent, imageReplicateAgent, movieGenAIAgent, movieReplicateAgent } from "../agents/index.js";
|
|
8
8
|
import { agentGenerationError, imageReferenceAction, imageFileTarget, movieFileTarget } from "../utils/error_cause.js";
|
|
9
|
-
// Reference media are generated outside the beat graphs, so they must carry the same
|
|
9
|
+
// Reference media are generated outside the beat graphs, so they must carry the same params the beats do.
|
|
10
10
|
export const referenceImageGraphParams = (imageAgentInfo, canvasSize) => ({
|
|
11
11
|
model: imageAgentInfo.imageParams.model,
|
|
12
|
+
moderation: imageAgentInfo.imageParams.moderation,
|
|
12
13
|
canvasSize,
|
|
14
|
+
quality: imageAgentInfo.imageParams.quality,
|
|
13
15
|
vertexai_project: imageAgentInfo.imageParams.vertexai_project,
|
|
14
16
|
vertexai_location: imageAgentInfo.imageParams.vertexai_location,
|
|
15
17
|
});
|
|
@@ -4,7 +4,7 @@ import { GraphAILogger } from "graphai";
|
|
|
4
4
|
import { toFile, AuthenticationError, RateLimitError, APIError } from "openai";
|
|
5
5
|
import { createOpenAIClient } from "../utils/openai_client.js";
|
|
6
6
|
import { safeFetch, FETCH_DOWNLOAD_TIMEOUT_MS } from "../utils/fetch.js";
|
|
7
|
-
import { provider2ImageAgent, gptImages, deprecatedOpenAIImageModelHints } from "../types/provider2agent.js";
|
|
7
|
+
import { provider2ImageAgent, gptImages, gptImageQuality, deprecatedOpenAIImageModelHints } from "../types/provider2agent.js";
|
|
8
8
|
import { apiKeyMissingError, agentGenerationError, openAIAgentGenerationError, agentIncorrectAPIKeyError, agentAPIRateLimitError, agentInvalidResponseError, imageAction, imageFileTarget, unsupportedModelTarget, } from "../utils/error_cause.js";
|
|
9
9
|
const isDeprecatedOpenAIImageModel = (model) => model in deprecatedOpenAIImageModelHints;
|
|
10
10
|
export const buildDeprecatedModelMessage = (model) => {
|
|
@@ -63,8 +63,9 @@ export const imageOpenaiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
63
63
|
if (gptImages.includes(model)) {
|
|
64
64
|
imageOptions.moderation = moderation || "auto";
|
|
65
65
|
imageOptions.background = "opaque";
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const resolvedQuality = gptImageQuality(model, quality);
|
|
67
|
+
if (resolvedQuality) {
|
|
68
|
+
imageOptions.quality = resolvedQuality;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
const response = await (async () => {
|
|
@@ -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) {
|
|
@@ -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.";
|
|
@@ -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,
|
|
@@ -641,6 +647,9 @@ export const modelPricing = {
|
|
|
641
647
|
// https://ai.google.dev/gemini-api/docs/pricing
|
|
642
648
|
"gemini-2.5-flash-preview-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
643
649
|
"gemini-2.5-pro-preview-tts": { unit: "tokens", inputPerMTokensUSD: 1, outputPerMTokensUSD: 20, asOf: "2026-07-03" },
|
|
650
|
+
// through 2026-12-31; $1 / $18 (flash) and $1 / $12 (lite) from 2027-01-01
|
|
651
|
+
"gemini-3.8-flash-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 9, asOf: "2026-10-07" },
|
|
652
|
+
"gemini-3.8-flash-lite-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 6, asOf: "2026-10-07" },
|
|
644
653
|
},
|
|
645
654
|
google: {
|
|
646
655
|
// https://ai.google.dev/gemini-api/docs/pricing ($0.039/image ≒ 1290 output tokens at $30/1M)
|
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
|
}>>;
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// assume every asset is generated fresh.
|
|
5
5
|
import { Tiktoken } from "js-tiktoken/lite";
|
|
6
6
|
import o200k_base from "js-tiktoken/ranks/o200k_base";
|
|
7
|
-
import { provider2TTSAgent, provider2MovieAgent, gptImageOutputTokens, getModelDuration, defaultMovieModel, veoExtendedSeconds, VEO_EXTENSION_MODEL, VEO_SEGMENT_SEC, getModelPricing, defaultProviders, } from "../types/provider2agent.js";
|
|
7
|
+
import { provider2TTSAgent, provider2MovieAgent, gptImageOutputTokens, isGptImage25Model, getModelDuration, defaultMovieModel, veoExtendedSeconds, VEO_EXTENSION_MODEL, VEO_SEGMENT_SEC, getModelPricing, defaultProviders, } from "../types/provider2agent.js";
|
|
8
8
|
import { text2SpeechProviderSchema, text2MovieProviderSchema } from "../types/schema.js";
|
|
9
9
|
import { MulmoPresentationStyleMethods } from "../methods/mulmo_presentation_style.js";
|
|
10
10
|
import { MulmoBeatMethods } from "../methods/mulmo_beat.js";
|
|
11
|
+
import { geminiTtsInputText } from "./gemini_tts.js";
|
|
11
12
|
import { imagePrompt, htmlImageSystemPrompt, translateSystemPrompt, translatePrompts } from "./prompt.js";
|
|
12
13
|
const MILLION = 1_000_000;
|
|
13
14
|
const LATIN_CHARS_PER_TOKEN = 4;
|
|
@@ -92,15 +93,14 @@ const buildOpenAITts = ({ speaker, text, textIsFinal, beatIndex, lang }) => {
|
|
|
92
93
|
return { process: "tts", beatIndex, lang, provider: "openai", model, inputChars: metric(text.length, textIsFinal), ...tokenMetrics };
|
|
93
94
|
};
|
|
94
95
|
const buildGeminiTts = ({ speaker, speechOptions, text, beatIndex, lang }) => {
|
|
95
|
-
|
|
96
|
-
const prompt = speechOptions?.instruction ? `### DIRECTOR'S NOTES\n${speechOptions.instruction}\n\n#### TRANSCRIPT\n${text}` : text;
|
|
96
|
+
const model = speaker.model ?? provider2TTSAgent.gemini.defaultModel;
|
|
97
97
|
return {
|
|
98
98
|
process: "tts",
|
|
99
99
|
beatIndex,
|
|
100
100
|
lang,
|
|
101
101
|
provider: "gemini",
|
|
102
|
-
model
|
|
103
|
-
inputTokens: estimated(countHeuristicTokens(
|
|
102
|
+
model,
|
|
103
|
+
inputTokens: estimated(countHeuristicTokens(geminiTtsInputText(model, text, speechOptions?.instruction))),
|
|
104
104
|
outputTokens: estimated(estimateSpeechSec(text) * GEMINI_TTS_AUDIO_TOKENS_PER_SEC),
|
|
105
105
|
};
|
|
106
106
|
};
|
|
@@ -162,8 +162,8 @@ const gptImageSize = (canvasSize) => {
|
|
|
162
162
|
const gptImageOutputMetric = (model, canvasSize, quality) => {
|
|
163
163
|
const table = gptImageOutputTokens[gptImageSize(canvasSize)];
|
|
164
164
|
const knownQuality = quality === "low" || quality === "medium" || quality === "high" ? quality : undefined;
|
|
165
|
-
//
|
|
166
|
-
const tokens = table[knownQuality ?? "high"];
|
|
165
|
+
// Anything else renders at "high" (the upper bound), except an explicit "auto" on 2.5, which renders at "low".
|
|
166
|
+
const tokens = table[knownQuality ?? (quality === "auto" && isGptImage25Model(model) ? "low" : "high")];
|
|
167
167
|
return metric(tokens, GPT_IMAGE_FIXED_TOKEN_MODELS.includes(model) && knownQuality !== undefined);
|
|
168
168
|
};
|
|
169
169
|
const buildImageRecord = ({ process, provider, model, prompt, canvasSize, quality, beatIndex, refKey }) => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const GEMINI_INTERACTIONS_TTS_MODELS: ReadonlySet<string>;
|
|
2
|
+
export declare const GEMINI_TTS_SAMPLE_RATE = 24000;
|
|
3
|
+
export declare const geminiDirectorsNotesPrompt: (text: string, instructions?: string) => string;
|
|
4
|
+
export declare const geminiTtsInputText: (model: string, text: string, instructions?: string) => string;
|
|
5
|
+
export declare const usesGeminiInteractionsTts: (model: string) => boolean;
|
|
6
|
+
export type GeminiInteractionsTtsInput = {
|
|
7
|
+
model: string;
|
|
8
|
+
text: string;
|
|
9
|
+
voice: string;
|
|
10
|
+
instructions?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const buildGeminiInteractionsTtsRequest: ({ model, text, voice, instructions }: GeminiInteractionsTtsInput) => {
|
|
13
|
+
model: string;
|
|
14
|
+
input: {
|
|
15
|
+
type: "user_input";
|
|
16
|
+
content: {
|
|
17
|
+
annotations?: {
|
|
18
|
+
type: "speech_metadata";
|
|
19
|
+
style: string;
|
|
20
|
+
}[] | undefined;
|
|
21
|
+
type: "text";
|
|
22
|
+
text: string;
|
|
23
|
+
}[];
|
|
24
|
+
}[];
|
|
25
|
+
response_format: {
|
|
26
|
+
type: "audio";
|
|
27
|
+
mime_type: string;
|
|
28
|
+
sample_rate: number;
|
|
29
|
+
};
|
|
30
|
+
generation_config: {
|
|
31
|
+
speech_config: {
|
|
32
|
+
voice: string;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
type InteractionAudio = {
|
|
37
|
+
data?: string;
|
|
38
|
+
mime_type?: string;
|
|
39
|
+
sample_rate?: number;
|
|
40
|
+
};
|
|
41
|
+
export declare const pcmFromInteractionAudio: (audio: InteractionAudio | undefined) => {
|
|
42
|
+
rawPcm: Buffer;
|
|
43
|
+
sampleRate: number;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Gemini 3.8 TTS takes the Interactions API: the text is read verbatim, so speaker direction goes in a
|
|
2
|
+
// speech_metadata annotation instead of the prompt, and raw PCM must be asked for (the unary default is WAV).
|
|
3
|
+
export const GEMINI_INTERACTIONS_TTS_MODELS = new Set(["gemini-3.8-flash-tts", "gemini-3.8-flash-lite-tts"]);
|
|
4
|
+
export const GEMINI_TTS_SAMPLE_RATE = 24000;
|
|
5
|
+
// 2.5 TTS has no field for direction, so it goes in the prompt: https://ai.google.dev/gemini-api/docs/speech-generation#controllable
|
|
6
|
+
export const geminiDirectorsNotesPrompt = (text, instructions) => instructions ? ["### DIRECTOR'S NOTES", instructions, "", "#### TRANSCRIPT", text].join("\n") : text;
|
|
7
|
+
// The text the model is given for one line: 3.8 gets the transcript plus the style annotation, 2.5 the wrapped prompt.
|
|
8
|
+
export const geminiTtsInputText = (model, text, instructions) => usesGeminiInteractionsTts(model) ? [text, instructions].filter(Boolean).join("\n") : geminiDirectorsNotesPrompt(text, instructions);
|
|
9
|
+
export const usesGeminiInteractionsTts = (model) => GEMINI_INTERACTIONS_TTS_MODELS.has(model);
|
|
10
|
+
export const buildGeminiInteractionsTtsRequest = ({ model, text, voice, instructions }) => ({
|
|
11
|
+
model,
|
|
12
|
+
input: [
|
|
13
|
+
{
|
|
14
|
+
type: "user_input",
|
|
15
|
+
content: [{ type: "text", text, ...(instructions ? { annotations: [{ type: "speech_metadata", style: instructions }] } : {}) }],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
response_format: { type: "audio", mime_type: "audio/l16", sample_rate: GEMINI_TTS_SAMPLE_RATE },
|
|
19
|
+
generation_config: { speech_config: [{ voice }] },
|
|
20
|
+
});
|
|
21
|
+
const sampleRateOf = (audio) => {
|
|
22
|
+
if (typeof audio.sample_rate === "number" && audio.sample_rate > 0)
|
|
23
|
+
return audio.sample_rate;
|
|
24
|
+
const match = audio.mime_type?.match(/rate=(\d+)/);
|
|
25
|
+
return match ? Number(match[1]) : GEMINI_TTS_SAMPLE_RATE;
|
|
26
|
+
};
|
|
27
|
+
// Anything but headerless 16-bit PCM would be fed to the PCM encoder as noise, so it is an error, not a guess.
|
|
28
|
+
export const pcmFromInteractionAudio = (audio) => {
|
|
29
|
+
if (!audio?.data)
|
|
30
|
+
throw new Error("No audio data returned");
|
|
31
|
+
if (!audio.mime_type?.toLowerCase().startsWith("audio/l16")) {
|
|
32
|
+
throw new Error(`Gemini TTS returned ${audio.mime_type ?? "audio of an unknown type"}, expected audio/l16`);
|
|
33
|
+
}
|
|
34
|
+
return { rawPcm: Buffer.from(audio.data, "base64"), sampleRate: sampleRateOf(audio) };
|
|
35
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mulmocast",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.node.js",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"homepage": "https://github.com/receptron/mulmocast-cli#readme",
|
|
100
100
|
"dependencies": {
|
|
101
101
|
"@google-cloud/text-to-speech": "^6.4.1",
|
|
102
|
-
"@google/genai": "^2.
|
|
102
|
+
"@google/genai": "^2.24.0",
|
|
103
103
|
"@graphai/anthropic_agent": "^2.1.0",
|
|
104
104
|
"@graphai/browserless_agent": "^2.0.4",
|
|
105
105
|
"@graphai/gemini_agent": "^2.1.0",
|