mulmocast 2.6.20 → 2.6.21
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 +8 -0
- package/lib/actions/audio.js +3 -0
- package/lib/actions/image_references.js +3 -0
- package/lib/actions/images.js +3 -0
- package/lib/actions/translate.d.ts +2 -0
- package/lib/actions/translate.js +5 -0
- package/lib/agents/image_genai_agent.js +12 -3
- package/lib/agents/image_openai_agent.js +12 -2
- package/lib/agents/image_replicate_agent.js +4 -2
- package/lib/agents/lipsync_replicate_agent.js +4 -4
- package/lib/agents/movie_genai_agent.js +22 -4
- package/lib/agents/movie_replicate_agent.js +7 -5
- package/lib/agents/sound_effect_replicate_agent.js +4 -4
- package/lib/agents/tts_elevenlabs_agent.js +12 -1
- package/lib/agents/tts_gemini_agent.js +11 -1
- package/lib/agents/tts_google_agent.js +9 -1
- package/lib/agents/tts_kotodama_agent.js +9 -1
- package/lib/agents/tts_openai_agent.js +9 -1
- package/lib/cli/commands/audio/handler.js +2 -1
- package/lib/cli/commands/bundle/handler.js +2 -1
- package/lib/cli/commands/html/handler.js +2 -1
- package/lib/cli/commands/image/handler.js +2 -1
- package/lib/cli/commands/markdown/handler.js +2 -1
- package/lib/cli/commands/movie/handler.js +2 -1
- package/lib/cli/commands/pdf/handler.js +2 -1
- package/lib/cli/commands/translate/handler.js +2 -1
- package/lib/cli/helpers.d.ts +1 -0
- package/lib/cli/helpers.js +46 -0
- package/lib/tools/create_mulmo_script_from_url.d.ts +2 -2
- package/lib/tools/create_mulmo_script_from_url.js +5 -2
- package/lib/tools/create_mulmo_script_interactively.d.ts +1 -1
- package/lib/tools/create_mulmo_script_interactively.js +6 -3
- package/lib/tools/deep_research.d.ts +2 -1
- package/lib/tools/deep_research.js +3 -1
- package/lib/tools/story_to_script.d.ts +3 -1
- package/lib/tools/story_to_script.js +3 -1
- package/lib/types/agent.d.ts +2 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/type.d.ts +3 -0
- package/lib/types/usage.d.ts +32 -0
- package/lib/types/usage.js +1 -0
- package/lib/utils/context.d.ts +2 -0
- package/lib/utils/context.js +2 -0
- package/lib/utils/filters.js +3 -6
- package/lib/utils/replicate_usage.d.ts +5 -0
- package/lib/utils/replicate_usage.js +11 -0
- package/lib/utils/usage_callback.d.ts +4 -0
- package/lib/utils/usage_callback.js +93 -0
- package/lib/utils/usage_collector.d.ts +12 -0
- package/lib/utils/usage_collector.js +18 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -137,6 +137,14 @@ ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
|
|
137
137
|
BROWSERLESS_API_TOKEN=your_browserless_api_token # to access web in mulmo tool
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
#### (Optional) Local usage tracking
|
|
141
|
+
```bash
|
|
142
|
+
MULMOCAST_DUMP_USAGE=1 # print JSON usage dump to stdout after each CLI action
|
|
143
|
+
MULMOCAST_DUMP_USAGE=/tmp/usage.json # ...or write it to a file
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Token / per-second / per-char usage per provider:model. See [docs/api.md](./docs/api.md#usage-tracking) for the full reference.
|
|
147
|
+
|
|
140
148
|
### Google Vertex AI
|
|
141
149
|
|
|
142
150
|
For enterprise/production environments, or to use Veo movie models that are only available on Vertex AI, use Vertex AI with Application Default Credentials (ADC):
|
package/lib/actions/audio.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as agents from "@graphai/vanilla";
|
|
|
4
4
|
import { fileWriteAgent } from "@graphai/vanilla_node_agents";
|
|
5
5
|
import { ttsOpenaiAgent, ttsGoogleAgent, ttsGeminiAgent, ttsElevenlabsAgent, ttsKotodamaAgent, addBGMAgent, combineAudioFilesAgent, mediaMockAgent, } from "../agents/index.js";
|
|
6
6
|
import { audioGraphOption } from "./graph_option.js";
|
|
7
|
+
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
7
8
|
import { getAudioArtifactFilePath, getAudioFilePath, getGroupedAudioFilePath, getOutputStudioFilePath, resolveDirPath, defaultBGMPath, mkdir, writingMessage, } from "../utils/file.js";
|
|
8
9
|
import { localizedText } from "../utils/utils.js";
|
|
9
10
|
import { text2hash } from "../utils/utils_node.js";
|
|
@@ -232,6 +233,7 @@ export const generateBeatAudio = async (index, context, args) => {
|
|
|
232
233
|
mkdir(audioSegmentDirPath);
|
|
233
234
|
const graph = new GraphAI(langs ? graph_tts_map : graph_tts, audioAgents, await audioGraphOption(context, settings));
|
|
234
235
|
callbacks?.forEach((callback) => graph.registerCallback(callback));
|
|
236
|
+
graph.registerCallback(createUsageCallback(context));
|
|
235
237
|
graph.injectValue("__mapIndex", index);
|
|
236
238
|
graph.injectValue("beat", context.studio.script.beats[index]);
|
|
237
239
|
graph.injectValue("studioBeat", context.studio.beats[index]);
|
|
@@ -268,6 +270,7 @@ export const audio = async (context, args) => {
|
|
|
268
270
|
mkdir(audioSegmentDirPath);
|
|
269
271
|
const graph = new GraphAI(audio_graph_data, audioAgents, await audioGraphOption(context, settings));
|
|
270
272
|
callbacks?.forEach((callback) => graph.registerCallback(callback));
|
|
273
|
+
graph.registerCallback(createUsageCallback(context));
|
|
271
274
|
graph.injectValue("context", context);
|
|
272
275
|
graph.injectValue("audioArtifactFilePath", audioArtifactFilePath);
|
|
273
276
|
graph.injectValue("audioCombinedFilePath", audioCombinedFilePath);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GraphAI, GraphAILogger } from "graphai";
|
|
2
2
|
import { getReferenceImagePath } from "../utils/file.js";
|
|
3
3
|
import { imageGraphOption } from "./graph_option.js";
|
|
4
|
+
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
4
5
|
import { MulmoPresentationStyleMethods, MulmoMediaSourceMethods } from "../methods/index.js";
|
|
5
6
|
import { imageOpenaiAgent, mediaMockAgent, imageGenAIAgent, imageReplicateAgent, movieGenAIAgent, movieReplicateAgent } from "../agents/index.js";
|
|
6
7
|
import { agentGenerationError, imageReferenceAction, imageFileTarget, movieFileTarget } from "../utils/error_cause.js";
|
|
@@ -43,6 +44,7 @@ export const generateReferenceImage = async (inputs) => {
|
|
|
43
44
|
try {
|
|
44
45
|
const options = await imageGraphOption(context);
|
|
45
46
|
const graph = new GraphAI(image_graph_data, { imageGenAIAgent, imageOpenaiAgent, mediaMockAgent, imageReplicateAgent }, options);
|
|
47
|
+
graph.registerCallback(createUsageCallback(context));
|
|
46
48
|
await graph.run();
|
|
47
49
|
return imagePath;
|
|
48
50
|
}
|
|
@@ -129,6 +131,7 @@ const generateReferenceMovie = async (inputs) => {
|
|
|
129
131
|
try {
|
|
130
132
|
const options = await imageGraphOption(context);
|
|
131
133
|
const graph = new GraphAI(movie_graph_data, { movieGenAIAgent, movieReplicateAgent, mediaMockAgent }, options);
|
|
134
|
+
graph.registerCallback(createUsageCallback(context));
|
|
132
135
|
await graph.run();
|
|
133
136
|
return moviePath;
|
|
134
137
|
}
|
package/lib/actions/images.js
CHANGED
|
@@ -15,6 +15,7 @@ import { extractImageFromMovie, ffmpegGetMediaDuration, trimMusic } from "../uti
|
|
|
15
15
|
import { getMediaRefs, resolveBeatLocalRefs } from "./image_references.js";
|
|
16
16
|
import { imagePreprocessAgent, imagePluginAgent, htmlImageGeneratorAgent } from "./image_agents.js";
|
|
17
17
|
import { imageGraphOption } from "./graph_option.js";
|
|
18
|
+
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
18
19
|
const vanillaAgents = vanilla.default ?? vanilla;
|
|
19
20
|
const imageAgents = {
|
|
20
21
|
imageGenAIAgent,
|
|
@@ -468,6 +469,7 @@ const generateImages = async (context, args) => {
|
|
|
468
469
|
graph.registerCallback(callback);
|
|
469
470
|
});
|
|
470
471
|
}
|
|
472
|
+
graph.registerCallback(createUsageCallback(context));
|
|
471
473
|
const res = await graph.run();
|
|
472
474
|
return res.mergeResult;
|
|
473
475
|
};
|
|
@@ -519,6 +521,7 @@ export const generateBeatImage = async (inputs) => {
|
|
|
519
521
|
graph.registerCallback(callback);
|
|
520
522
|
});
|
|
521
523
|
}
|
|
524
|
+
graph.registerCallback(createUsageCallback(context));
|
|
522
525
|
await graph.run();
|
|
523
526
|
}
|
|
524
527
|
catch (error) {
|
package/lib/actions/translate.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as agents from "@graphai/vanilla";
|
|
|
5
5
|
import { openAIAgent } from "@graphai/openai_agent";
|
|
6
6
|
import { fileWriteAgent } from "@graphai/vanilla_node_agents";
|
|
7
7
|
import { splitText } from "../utils/string.js";
|
|
8
|
+
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
8
9
|
import { settings2GraphAIConfig, beatId, multiLingualObjectToArray } from "../utils/utils.js";
|
|
9
10
|
import { getMultiLingual } from "../utils/context.js";
|
|
10
11
|
import { currentMulmoScriptVersion } from "../types/const.js";
|
|
@@ -36,6 +37,8 @@ export const translateTextGraph = {
|
|
|
36
37
|
},
|
|
37
38
|
output: {
|
|
38
39
|
text: ".text",
|
|
40
|
+
usage: ".usage",
|
|
41
|
+
model: ".model",
|
|
39
42
|
},
|
|
40
43
|
// return { lang, text } <- localizedText
|
|
41
44
|
agent: "openAIAgent",
|
|
@@ -253,6 +256,7 @@ export const translateBeat = async (index, context, targetLangs, args) => {
|
|
|
253
256
|
graph.registerCallback(callback);
|
|
254
257
|
});
|
|
255
258
|
}
|
|
259
|
+
graph.registerCallback(createUsageCallback(context));
|
|
256
260
|
const results = await graph.run();
|
|
257
261
|
const multiLingual = getMultiLingual(outputMultilingualFilePath, context.studio.beats);
|
|
258
262
|
const key = beatId(context.studio.script.beats[index]?.id, index);
|
|
@@ -294,6 +298,7 @@ export const translate = async (context, args) => {
|
|
|
294
298
|
graph.registerCallback(callback);
|
|
295
299
|
});
|
|
296
300
|
}
|
|
301
|
+
graph.registerCallback(createUsageCallback(context));
|
|
297
302
|
const results = await graph.run();
|
|
298
303
|
writingMessage(outputMultilingualFilePath);
|
|
299
304
|
if (results.mergeStudioResult) {
|
|
@@ -21,12 +21,21 @@ const getGeminiContents = (prompt, referenceImages) => {
|
|
|
21
21
|
});
|
|
22
22
|
return contents;
|
|
23
23
|
};
|
|
24
|
-
const geminiFlashResult = (response) => {
|
|
24
|
+
const geminiFlashResult = (response, model) => {
|
|
25
25
|
if (!response.candidates?.[0]?.content?.parts) {
|
|
26
26
|
throw new Error("ERROR: generateContent returned no candidates", {
|
|
27
27
|
cause: agentInvalidResponseError("imageGenAIAgent", imageAction, imageFileTarget),
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
+
const usage = response.usageMetadata
|
|
31
|
+
? {
|
|
32
|
+
provider: "google",
|
|
33
|
+
model,
|
|
34
|
+
inputTokens: response.usageMetadata.promptTokenCount,
|
|
35
|
+
outputTokens: response.usageMetadata.candidatesTokenCount,
|
|
36
|
+
totalTokens: response.usageMetadata.totalTokenCount,
|
|
37
|
+
}
|
|
38
|
+
: undefined;
|
|
30
39
|
for (const part of response.candidates[0].content.parts) {
|
|
31
40
|
if (part.text) {
|
|
32
41
|
GraphAILogger.info("Gemini image generation response:", part.text);
|
|
@@ -39,7 +48,7 @@ const geminiFlashResult = (response) => {
|
|
|
39
48
|
});
|
|
40
49
|
}
|
|
41
50
|
const buffer = Buffer.from(imageData, "base64");
|
|
42
|
-
return { buffer };
|
|
51
|
+
return { buffer, usage };
|
|
43
52
|
}
|
|
44
53
|
}
|
|
45
54
|
throw new Error("ERROR: generateContent returned no image data", {
|
|
@@ -102,7 +111,7 @@ export const imageGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
102
111
|
})();
|
|
103
112
|
const res = await resultify(() => ai.models.generateContent(contentParams));
|
|
104
113
|
if (res.ok) {
|
|
105
|
-
return geminiFlashResult(res.value);
|
|
114
|
+
return geminiFlashResult(res.value, model);
|
|
106
115
|
}
|
|
107
116
|
return errorProcess(res.error);
|
|
108
117
|
}
|
|
@@ -115,6 +115,16 @@ export const imageOpenaiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
115
115
|
cause: agentInvalidResponseError("imageOpenaiAgent", imageAction, imageFileTarget),
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
+
// gpt-image-1 surfaces token usage; legacy DALL·E response shapes don't.
|
|
119
|
+
const usage = response.usage
|
|
120
|
+
? {
|
|
121
|
+
provider: "openai",
|
|
122
|
+
model,
|
|
123
|
+
inputTokens: response.usage.input_tokens,
|
|
124
|
+
outputTokens: response.usage.output_tokens,
|
|
125
|
+
totalTokens: response.usage.total_tokens,
|
|
126
|
+
}
|
|
127
|
+
: undefined;
|
|
118
128
|
const url = response.data[0].url;
|
|
119
129
|
if (!url) {
|
|
120
130
|
// For gpt-image-1
|
|
@@ -124,7 +134,7 @@ export const imageOpenaiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
124
134
|
cause: agentInvalidResponseError("imageOpenaiAgent", imageAction, imageFileTarget),
|
|
125
135
|
});
|
|
126
136
|
}
|
|
127
|
-
return { buffer: Buffer.from(image_base64, "base64") };
|
|
137
|
+
return { buffer: Buffer.from(image_base64, "base64"), usage };
|
|
128
138
|
}
|
|
129
139
|
// URL response handling (legacy OpenAI image API response format)
|
|
130
140
|
const res = await fetch(url);
|
|
@@ -136,7 +146,7 @@ export const imageOpenaiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
136
146
|
// 2. Read the response as an ArrayBuffer
|
|
137
147
|
const arrayBuffer = await res.arrayBuffer();
|
|
138
148
|
// 3. Convert the ArrayBuffer to a Node.js Buffer and return it along with url
|
|
139
|
-
return { buffer: Buffer.from(arrayBuffer) };
|
|
149
|
+
return { buffer: Buffer.from(arrayBuffer), usage };
|
|
140
150
|
};
|
|
141
151
|
const imageOpenaiAgentInfo = {
|
|
142
152
|
name: "imageOpenaiAgent",
|
|
@@ -4,6 +4,7 @@ import Replicate from "replicate";
|
|
|
4
4
|
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
|
+
import { runReplicateWithMetrics } from "../utils/replicate_usage.js";
|
|
7
8
|
// Replicate image models return one of: FileOutput (object with url() method),
|
|
8
9
|
// Array<FileOutput>, string URL, or { url: string }. Normalize to URL string/URL.
|
|
9
10
|
const extractImageUrl = (output) => {
|
|
@@ -44,7 +45,7 @@ export const imageReplicateAgent = async ({ namedInputs, params, config, }) => {
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
try {
|
|
47
|
-
const output = await replicate
|
|
48
|
+
const { output, predictSec } = await runReplicateWithMetrics(replicate, model, input);
|
|
48
49
|
const imageUrl = extractImageUrl(output);
|
|
49
50
|
if (!imageUrl) {
|
|
50
51
|
throw new Error("ERROR: generateImage returned undefined", {
|
|
@@ -59,7 +60,8 @@ export const imageReplicateAgent = async ({ namedInputs, params, config, }) => {
|
|
|
59
60
|
}
|
|
60
61
|
const arrayBuffer = await imageResponse.arrayBuffer();
|
|
61
62
|
const buffer = Buffer.from(arrayBuffer);
|
|
62
|
-
|
|
63
|
+
const usage = predictSec !== undefined ? { provider: "replicate", model, predictSec } : undefined;
|
|
64
|
+
return { buffer, usage };
|
|
63
65
|
}
|
|
64
66
|
catch (error) {
|
|
65
67
|
GraphAILogger.info("Replicate generation error:", error);
|
|
@@ -3,6 +3,7 @@ import { GraphAILogger } from "graphai";
|
|
|
3
3
|
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
|
+
import { runReplicateWithMetrics } from "../utils/replicate_usage.js";
|
|
6
7
|
export const lipSyncReplicateAgent = async ({ namedInputs, params, config, }) => {
|
|
7
8
|
const { movieFile, audioFile, imageFile } = namedInputs;
|
|
8
9
|
const apiKey = config?.apiKey;
|
|
@@ -60,9 +61,7 @@ export const lipSyncReplicateAgent = async ({ namedInputs, params, config, }) =>
|
|
|
60
61
|
}
|
|
61
62
|
const model_identifier = provider2LipSyncAgent.replicate.modelParams[model]?.identifier ?? model;
|
|
62
63
|
try {
|
|
63
|
-
const output = await replicate
|
|
64
|
-
input,
|
|
65
|
-
});
|
|
64
|
+
const { output, predictSec } = await runReplicateWithMetrics(replicate, model_identifier, input);
|
|
66
65
|
if (output && typeof output === "object" && "url" in output) {
|
|
67
66
|
const videoUrl = output.url();
|
|
68
67
|
const videoResponse = await fetch(videoUrl);
|
|
@@ -72,7 +71,8 @@ export const lipSyncReplicateAgent = async ({ namedInputs, params, config, }) =>
|
|
|
72
71
|
});
|
|
73
72
|
}
|
|
74
73
|
const arrayBuffer = await videoResponse.arrayBuffer();
|
|
75
|
-
|
|
74
|
+
const usage = predictSec !== undefined ? { provider: "replicate", model, predictSec } : undefined;
|
|
75
|
+
return { buffer: Buffer.from(arrayBuffer), usage };
|
|
76
76
|
}
|
|
77
77
|
return undefined;
|
|
78
78
|
}
|
|
@@ -3,6 +3,7 @@ import { GraphAILogger, sleep } from "graphai";
|
|
|
3
3
|
import { GoogleGenAI, PersonGeneration } from "@google/genai";
|
|
4
4
|
import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, imageAction, movieFileTarget, videoDurationTarget, unsupportedModelTarget, hasCause, } from "../utils/error_cause.js";
|
|
5
5
|
import { getAspectRatio } from "../utils/utils.js";
|
|
6
|
+
import { ffmpegGetMediaDuration } from "../utils/ffmpeg_utils.js";
|
|
6
7
|
import { ASPECT_RATIOS } from "../types/const.js";
|
|
7
8
|
import { getModelDuration, provider2MovieAgent, AUDIO_MODE_NEVER, AUDIO_MODE_ALWAYS } from "../types/provider2agent.js";
|
|
8
9
|
const pollUntilDone = async (ai, operation) => {
|
|
@@ -35,7 +36,22 @@ const loadImageAsBase64 = (imagePath) => {
|
|
|
35
36
|
mimeType: "image/png",
|
|
36
37
|
};
|
|
37
38
|
};
|
|
38
|
-
|
|
39
|
+
// Veo bills per second of generated video. The SDK response carries no usage
|
|
40
|
+
// metadata (GenerateVideosResponse only has generatedVideos and RAI flags), so
|
|
41
|
+
// we ffprobe the downloaded file to get the actual duration. Falls back to
|
|
42
|
+
// undefined if ffprobe fails, which lets the billing layer use the requested
|
|
43
|
+
// duration as a fallback.
|
|
44
|
+
const probeDurationSec = async (movieFile) => {
|
|
45
|
+
try {
|
|
46
|
+
const { duration } = await ffmpegGetMediaDuration(movieFile);
|
|
47
|
+
return duration > 0 ? duration : undefined;
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
GraphAILogger.warn("movieGenAIAgent: ffprobe failed, predictSec will be omitted", e);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const downloadVideo = async (ai, video, movieFile, isVertexAI, model) => {
|
|
39
55
|
if (isVertexAI) {
|
|
40
56
|
// Vertex AI returns videoBytes directly
|
|
41
57
|
writeFileSync(movieFile, Buffer.from(video.videoBytes, "base64"));
|
|
@@ -48,7 +64,9 @@ const downloadVideo = async (ai, video, movieFile, isVertexAI) => {
|
|
|
48
64
|
});
|
|
49
65
|
await sleep(5000); // HACK: Without this, the file is not ready yet.
|
|
50
66
|
}
|
|
51
|
-
|
|
67
|
+
const predictSec = await probeDurationSec(movieFile);
|
|
68
|
+
const usage = predictSec !== undefined ? { provider: "google", model, predictSec } : undefined;
|
|
69
|
+
return { saved: movieFile, usage };
|
|
52
70
|
};
|
|
53
71
|
const createVeo31Payload = (model, prompt, aspectRatio, source) => ({
|
|
54
72
|
model,
|
|
@@ -92,7 +110,7 @@ const generateExtendedVideo = async (ai, model, prompt, aspectRatio, imagePath,
|
|
|
92
110
|
cause: agentInvalidResponseError("movieGenAIAgent", imageAction, movieFileTarget),
|
|
93
111
|
});
|
|
94
112
|
}
|
|
95
|
-
return downloadVideo(ai, result.video, movieFile, isVertexAI);
|
|
113
|
+
return downloadVideo(ai, result.video, movieFile, isVertexAI, model);
|
|
96
114
|
};
|
|
97
115
|
const generateStandardVideo = async (ai, model, prompt, aspectRatio, imagePath, lastFrameImagePath, referenceImages, duration, movieFile, isVertexAI) => {
|
|
98
116
|
const capabilities = provider2MovieAgent.google.modelParams[model];
|
|
@@ -139,7 +157,7 @@ const generateStandardVideo = async (ai, model, prompt, aspectRatio, imagePath,
|
|
|
139
157
|
const operation = await ai.models.generateVideos(payload);
|
|
140
158
|
const response = await pollUntilDone(ai, operation);
|
|
141
159
|
const video = getVideoFromResponse(response);
|
|
142
|
-
return downloadVideo(ai, video, movieFile, isVertexAI);
|
|
160
|
+
return downloadVideo(ai, video, movieFile, isVertexAI, model);
|
|
143
161
|
};
|
|
144
162
|
export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
145
163
|
const { prompt, imagePath, lastFrameImagePath, referenceImages, movieFile } = namedInputs;
|
|
@@ -3,6 +3,7 @@ import { GraphAILogger } from "graphai";
|
|
|
3
3
|
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
|
+
import { runReplicateWithMetrics } from "../utils/replicate_usage.js";
|
|
6
7
|
function replicate_get_videoUrl(output) {
|
|
7
8
|
if (typeof output === "string")
|
|
8
9
|
return output;
|
|
@@ -93,7 +94,7 @@ async function generateMovie(model, apiKey, prompt, imagePath, lastFrameImagePat
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
try {
|
|
96
|
-
const output = await replicate
|
|
97
|
+
const { output, predictSec } = await runReplicateWithMetrics(replicate, model, input);
|
|
97
98
|
// Download the generated video
|
|
98
99
|
// Some models return a FileOutput object with a url() method; others return a plain string URL.
|
|
99
100
|
const videoUrl = replicate_get_videoUrl(output);
|
|
@@ -105,7 +106,7 @@ async function generateMovie(model, apiKey, prompt, imagePath, lastFrameImagePat
|
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
const arrayBuffer = await videoResponse.arrayBuffer();
|
|
108
|
-
return Buffer.from(arrayBuffer);
|
|
109
|
+
return { buffer: Buffer.from(arrayBuffer), predictSec };
|
|
109
110
|
}
|
|
110
111
|
return undefined;
|
|
111
112
|
}
|
|
@@ -154,9 +155,10 @@ export const movieReplicateAgent = async ({ namedInputs, params, config, }) => {
|
|
|
154
155
|
});
|
|
155
156
|
}
|
|
156
157
|
try {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
159
|
-
|
|
158
|
+
const result = await generateMovie(model, apiKey, prompt, imagePath, lastFrameImagePath, referenceImages, aspectRatio, duration, params.generateAudio);
|
|
159
|
+
if (result) {
|
|
160
|
+
const usage = result.predictSec !== undefined ? { provider: "replicate", model, predictSec: result.predictSec } : undefined;
|
|
161
|
+
return { buffer: result.buffer, usage };
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
catch (error) {
|
|
@@ -3,6 +3,7 @@ import { GraphAILogger } from "graphai";
|
|
|
3
3
|
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
|
+
import { runReplicateWithMetrics } from "../utils/replicate_usage.js";
|
|
6
7
|
export const soundEffectReplicateAgent = async ({ namedInputs, params, config }) => {
|
|
7
8
|
const { prompt, movieFile } = namedInputs;
|
|
8
9
|
const apiKey = config?.apiKey;
|
|
@@ -28,9 +29,7 @@ export const soundEffectReplicateAgent = async ({ namedInputs, params, config })
|
|
|
28
29
|
};
|
|
29
30
|
try {
|
|
30
31
|
const model_identifier = provider2SoundEffectAgent.replicate.modelParams[model]?.identifier ?? model;
|
|
31
|
-
const output = await replicate
|
|
32
|
-
input,
|
|
33
|
-
});
|
|
32
|
+
const { output, predictSec } = await runReplicateWithMetrics(replicate, model_identifier, input);
|
|
34
33
|
if (output && typeof output === "object" && "url" in output) {
|
|
35
34
|
const videoUrl = output.url();
|
|
36
35
|
const videoResponse = await fetch(videoUrl);
|
|
@@ -40,7 +39,8 @@ export const soundEffectReplicateAgent = async ({ namedInputs, params, config })
|
|
|
40
39
|
});
|
|
41
40
|
}
|
|
42
41
|
const arrayBuffer = await videoResponse.arrayBuffer();
|
|
43
|
-
|
|
42
|
+
const usage = predictSec !== undefined ? { provider: "replicate", model, predictSec } : undefined;
|
|
43
|
+
return { buffer: Buffer.from(arrayBuffer), usage };
|
|
44
44
|
}
|
|
45
45
|
return undefined;
|
|
46
46
|
}
|
|
@@ -68,9 +68,20 @@ export const ttsElevenlabsAgent = async ({ namedInputs, params, config, }) => {
|
|
|
68
68
|
cause: agentGenerationError("ttsElevenlabsAgent", audioAction, audioFileTarget),
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
// ElevenLabs returns the exact billed character count in the `character-cost`
|
|
72
|
+
// header (already discounted for V2 Flash/Turbo etc. — see #1420 research).
|
|
73
|
+
// We read it BEFORE consuming the body so the header is still accessible.
|
|
74
|
+
const billedHeader = response.headers.get("character-cost");
|
|
75
|
+
const billedChars = billedHeader ? Number(billedHeader) : undefined;
|
|
71
76
|
const arrayBuffer = await response.arrayBuffer();
|
|
72
77
|
const buffer = Buffer.from(arrayBuffer);
|
|
73
|
-
|
|
78
|
+
const usage = {
|
|
79
|
+
provider: "elevenlabs",
|
|
80
|
+
model: requestBody.model_id,
|
|
81
|
+
// Use upstream-billed amount when available (exact); fall back to raw text length.
|
|
82
|
+
inputChars: Number.isFinite(billedChars) ? billedChars : text.length,
|
|
83
|
+
};
|
|
84
|
+
return { buffer, usage };
|
|
74
85
|
};
|
|
75
86
|
const ttsElevenlabsAgentInfo = {
|
|
76
87
|
name: "ttsElevenlabsAgent",
|
|
@@ -42,7 +42,17 @@ export const ttsGeminiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
42
42
|
const rateMatch = mimeType?.match(/rate=(\d+)/);
|
|
43
43
|
const sampleRate = rateMatch ? parseInt(rateMatch[1]) : 24000;
|
|
44
44
|
const rawPcm = Buffer.from(pcmBase64, "base64");
|
|
45
|
-
|
|
45
|
+
const usedModel = model ?? provider2TTSAgent.gemini.defaultModel;
|
|
46
|
+
const usage = response.usageMetadata
|
|
47
|
+
? {
|
|
48
|
+
provider: "gemini",
|
|
49
|
+
model: usedModel,
|
|
50
|
+
inputTokens: response.usageMetadata.promptTokenCount,
|
|
51
|
+
outputTokens: response.usageMetadata.candidatesTokenCount,
|
|
52
|
+
totalTokens: response.usageMetadata.totalTokenCount,
|
|
53
|
+
}
|
|
54
|
+
: undefined;
|
|
55
|
+
return { buffer: await pcmToMp3(rawPcm, sampleRate), usage };
|
|
46
56
|
}
|
|
47
57
|
catch (e) {
|
|
48
58
|
if (suppressError) {
|
|
@@ -42,7 +42,15 @@ export const ttsGoogleAgent = async ({ namedInputs, params }) => {
|
|
|
42
42
|
try {
|
|
43
43
|
// Call the Text-to-Speech API
|
|
44
44
|
const [response] = await client.synthesizeSpeech(request, { timeout: SYNTHESIZE_TIMEOUT_MS });
|
|
45
|
-
|
|
45
|
+
// Google Cloud TTS bills per character; SynthesizeSpeechResponse exposes
|
|
46
|
+
// no usage info. Record inputChars so the billing layer can compute cost
|
|
47
|
+
// from `model` (voice tier) + char count.
|
|
48
|
+
const usage = {
|
|
49
|
+
provider: "google",
|
|
50
|
+
model: model ?? voice ?? "default",
|
|
51
|
+
inputChars: text.length,
|
|
52
|
+
};
|
|
53
|
+
return { buffer: response.audioContent, usage };
|
|
46
54
|
}
|
|
47
55
|
catch (e) {
|
|
48
56
|
if (suppressError) {
|
|
@@ -44,7 +44,15 @@ export const ttsKotodamaAgent = async ({ namedInputs, params, config, }) => {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
const buffer = Buffer.from(json.audios[0], "base64");
|
|
47
|
-
|
|
47
|
+
// Kotodama is subscription-billed externally (no public per-char rate API
|
|
48
|
+
// surface). Report inputChars + voice/decoration so the billing layer can
|
|
49
|
+
// do its own accounting.
|
|
50
|
+
const usage = {
|
|
51
|
+
provider: "kotodama",
|
|
52
|
+
model: body.speaker_id,
|
|
53
|
+
inputChars: text.length,
|
|
54
|
+
};
|
|
55
|
+
return { buffer, usage };
|
|
48
56
|
}
|
|
49
57
|
catch (error) {
|
|
50
58
|
if (suppressError) {
|
|
@@ -28,7 +28,15 @@ export const ttsOpenaiAgent = async ({ namedInputs, params, config, }) => {
|
|
|
28
28
|
GraphAILogger.log("ttsOptions", tts_options);
|
|
29
29
|
const response = await openai.audio.speech.create(tts_options);
|
|
30
30
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
31
|
-
|
|
31
|
+
// tts-1 / tts-1-hd bill per character. gpt-4o-mini-tts is token-based and
|
|
32
|
+
// is handled by #1428; for now we report inputChars uniformly and let the
|
|
33
|
+
// billing layer pick the right unit from `model`.
|
|
34
|
+
const usage = {
|
|
35
|
+
provider: "openai",
|
|
36
|
+
model: tts_options.model,
|
|
37
|
+
inputChars: text.length,
|
|
38
|
+
};
|
|
39
|
+
return { buffer, usage };
|
|
32
40
|
}
|
|
33
41
|
catch (error) {
|
|
34
42
|
if (suppressError) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { audio } from "../../../actions/index.js";
|
|
2
|
-
import { initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
3
3
|
export const handler = async (argv) => {
|
|
4
4
|
const context = await initializeContext(argv);
|
|
5
5
|
if (!context) {
|
|
@@ -7,4 +7,5 @@ export const handler = async (argv) => {
|
|
|
7
7
|
}
|
|
8
8
|
await runTranslateIfNeeded(context);
|
|
9
9
|
await audio(context);
|
|
10
|
+
dumpUsageIfRequested(context);
|
|
10
11
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mulmoViewerBundle, audio, images, translate } from "../../../actions/index.js";
|
|
2
|
-
import { initializeContext } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext } from "../../helpers.js";
|
|
3
3
|
import { bundleTargetLang } from "../../../types/const.js";
|
|
4
4
|
export const handler = async (argv) => {
|
|
5
5
|
const context = await initializeContext(argv);
|
|
@@ -11,4 +11,5 @@ export const handler = async (argv) => {
|
|
|
11
11
|
await audio({ ...context, lang });
|
|
12
12
|
}
|
|
13
13
|
await audio(context).then(images).then(mulmoViewerBundle);
|
|
14
|
+
dumpUsageIfRequested(context);
|
|
14
15
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { images, html } from "../../../actions/index.js";
|
|
2
|
-
import { initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
3
3
|
export const handler = async (argv) => {
|
|
4
4
|
const context = await initializeContext(argv);
|
|
5
5
|
if (!context) {
|
|
@@ -8,4 +8,5 @@ export const handler = async (argv) => {
|
|
|
8
8
|
await runTranslateIfNeeded(context);
|
|
9
9
|
await images(context);
|
|
10
10
|
await html(context, argv.image_width);
|
|
11
|
+
dumpUsageIfRequested(context);
|
|
11
12
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { images } from "../../../actions/index.js";
|
|
2
|
-
import { initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
3
3
|
export const handler = async (argv) => {
|
|
4
4
|
const context = await initializeContext(argv);
|
|
5
5
|
if (!context) {
|
|
@@ -7,4 +7,5 @@ export const handler = async (argv) => {
|
|
|
7
7
|
}
|
|
8
8
|
await runTranslateIfNeeded(context);
|
|
9
9
|
await images(context);
|
|
10
|
+
dumpUsageIfRequested(context);
|
|
10
11
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { images, markdown } from "../../../actions/index.js";
|
|
2
|
-
import { initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
3
3
|
export const handler = async (argv) => {
|
|
4
4
|
const context = await initializeContext(argv);
|
|
5
5
|
if (!context) {
|
|
@@ -8,4 +8,5 @@ export const handler = async (argv) => {
|
|
|
8
8
|
await runTranslateIfNeeded(context);
|
|
9
9
|
await images(context);
|
|
10
10
|
await markdown(context, argv.image_width);
|
|
11
|
+
dumpUsageIfRequested(context);
|
|
11
12
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { audio, images, movie, captions } from "../../../actions/index.js";
|
|
2
|
-
import { initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
3
3
|
export const handler = async (argv) => {
|
|
4
4
|
const context = await initializeContext(argv);
|
|
5
5
|
if (!context) {
|
|
@@ -7,4 +7,5 @@ export const handler = async (argv) => {
|
|
|
7
7
|
}
|
|
8
8
|
await runTranslateIfNeeded(context, true);
|
|
9
9
|
await audio(context).then(images).then(captions).then(movie);
|
|
10
|
+
dumpUsageIfRequested(context);
|
|
10
11
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { images, pdf } from "../../../actions/index.js";
|
|
2
|
-
import { initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
3
3
|
export const handler = async (argv) => {
|
|
4
4
|
const context = await initializeContext(argv);
|
|
5
5
|
if (!context) {
|
|
@@ -8,4 +8,5 @@ export const handler = async (argv) => {
|
|
|
8
8
|
await runTranslateIfNeeded(context);
|
|
9
9
|
await images(context);
|
|
10
10
|
await pdf(context, argv.pdf_mode, argv.pdf_size);
|
|
11
|
+
dumpUsageIfRequested(context);
|
|
11
12
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { translate } from "../../../actions/index.js";
|
|
2
|
-
import { initializeContext } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext } from "../../helpers.js";
|
|
3
3
|
export const handler = async (argv) => {
|
|
4
4
|
const context = await initializeContext(argv);
|
|
5
5
|
if (!context) {
|
|
6
6
|
process.exit(1);
|
|
7
7
|
}
|
|
8
8
|
await translate(context);
|
|
9
|
+
dumpUsageIfRequested(context);
|
|
9
10
|
};
|
package/lib/cli/helpers.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CliArgs } from "../types/cli_types.js";
|
|
2
2
|
import { FileObject, InitOptions, MulmoStudioContext } from "../types/index.js";
|
|
3
3
|
export declare const runTranslateIfNeeded: (context: MulmoStudioContext, includeCaption?: boolean) => Promise<void>;
|
|
4
|
+
export declare const dumpUsageIfRequested: (context: MulmoStudioContext) => void;
|
|
4
5
|
export declare const setGraphAILogger: (verbose: boolean | undefined, logValues?: Record<string, unknown>) => void;
|
|
5
6
|
export declare const getFileObject: (args: {
|
|
6
7
|
basedir?: string;
|
package/lib/cli/helpers.js
CHANGED
|
@@ -14,6 +14,52 @@ export const runTranslateIfNeeded = async (context, includeCaption = false) => {
|
|
|
14
14
|
await translate(context);
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
|
+
const summarizeUsage = (context) => {
|
|
18
|
+
const snapshot = context.usageCollector?.snapshot() ?? [];
|
|
19
|
+
const groups = new Map();
|
|
20
|
+
for (const record of snapshot) {
|
|
21
|
+
const key = `${record.provider}:${record.model}`;
|
|
22
|
+
const group = groups.get(key) ?? {
|
|
23
|
+
provider: record.provider,
|
|
24
|
+
model: record.model,
|
|
25
|
+
records: 0,
|
|
26
|
+
inputTokens: 0,
|
|
27
|
+
outputTokens: 0,
|
|
28
|
+
totalTokens: 0,
|
|
29
|
+
predictSec: 0,
|
|
30
|
+
inputChars: 0,
|
|
31
|
+
};
|
|
32
|
+
group.records += 1;
|
|
33
|
+
group.inputTokens += record.inputTokens ?? 0;
|
|
34
|
+
group.outputTokens += record.outputTokens ?? 0;
|
|
35
|
+
group.totalTokens += record.totalTokens ?? 0;
|
|
36
|
+
group.predictSec += record.predictSec ?? 0;
|
|
37
|
+
group.inputChars += record.inputChars ?? 0;
|
|
38
|
+
groups.set(key, group);
|
|
39
|
+
}
|
|
40
|
+
return { records: snapshot.length, byModel: Array.from(groups.values()), snapshot };
|
|
41
|
+
};
|
|
42
|
+
// Opt-in usage dump after a CLI action completes. Controlled by
|
|
43
|
+
// MULMOCAST_DUMP_USAGE:
|
|
44
|
+
// - unset / empty → no-op (zero overhead)
|
|
45
|
+
// - "1" / "true" / "stdout" → print JSON to stdout via GraphAILogger.info
|
|
46
|
+
// - any other value → treated as a file path and the JSON is written there
|
|
47
|
+
//
|
|
48
|
+
// The API/billing layer reads context.usageCollector directly; this is for
|
|
49
|
+
// local CLI verification only.
|
|
50
|
+
export const dumpUsageIfRequested = (context) => {
|
|
51
|
+
const setting = process.env.MULMOCAST_DUMP_USAGE;
|
|
52
|
+
if (!setting)
|
|
53
|
+
return;
|
|
54
|
+
const payload = summarizeUsage(context);
|
|
55
|
+
const json = JSON.stringify(payload, null, 2);
|
|
56
|
+
if (setting === "1" || setting === "true" || setting === "stdout") {
|
|
57
|
+
GraphAILogger.info(json);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
fs.writeFileSync(setting, json, "utf-8");
|
|
61
|
+
GraphAILogger.info(`usage written to ${setting}`);
|
|
62
|
+
};
|
|
17
63
|
export const setGraphAILogger = (verbose, logValues) => {
|
|
18
64
|
if (verbose) {
|
|
19
65
|
if (logValues) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ScriptingParams } from "../types/index.js";
|
|
2
|
-
export declare const createMulmoScriptFromUrl: ({ urls, templateName, outDirPath, filename, cacheDirPath, llm, llm_model }: ScriptingParams) => Promise<void>;
|
|
3
|
-
export declare const createMulmoScriptFromFile: (fileName: string, { templateName, outDirPath, filename, cacheDirPath, llm, llm_model, verbose }: ScriptingParams) => Promise<void>;
|
|
2
|
+
export declare const createMulmoScriptFromUrl: ({ urls, templateName, outDirPath, filename, cacheDirPath, llm, llm_model, usageCollector }: ScriptingParams) => Promise<void>;
|
|
3
|
+
export declare const createMulmoScriptFromFile: (fileName: string, { templateName, outDirPath, filename, cacheDirPath, llm, llm_model, verbose, usageCollector }: ScriptingParams) => Promise<void>;
|
|
@@ -15,6 +15,7 @@ import { mulmoScriptSchema, urlsSchema } from "../types/schema.js";
|
|
|
15
15
|
import { cliLoadingPlugin } from "../utils/plugins.js";
|
|
16
16
|
import { graphDataScriptFromUrlPrompt } from "../utils/prompt.js";
|
|
17
17
|
import { llmPair, settings2GraphAIConfig } from "../utils/utils.js";
|
|
18
|
+
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
18
19
|
import { readFileSync } from "fs";
|
|
19
20
|
dotenv.config({ quiet: true });
|
|
20
21
|
const vanillaAgents = agents.default ?? agents;
|
|
@@ -207,7 +208,7 @@ const graphDataText = {
|
|
|
207
208
|
},
|
|
208
209
|
},
|
|
209
210
|
};
|
|
210
|
-
export const createMulmoScriptFromUrl = async ({ urls, templateName, outDirPath, filename, cacheDirPath, llm, llm_model }) => {
|
|
211
|
+
export const createMulmoScriptFromUrl = async ({ urls, templateName, outDirPath, filename, cacheDirPath, llm, llm_model, usageCollector }) => {
|
|
211
212
|
mkdir(outDirPath);
|
|
212
213
|
mkdir(cacheDirPath);
|
|
213
214
|
const parsedUrls = urlsSchema.parse(urls);
|
|
@@ -239,6 +240,7 @@ export const createMulmoScriptFromUrl = async ({ urls, templateName, outDirPath,
|
|
|
239
240
|
graph.injectValue("llmModel", model);
|
|
240
241
|
graph.injectValue("maxTokens", max_tokens);
|
|
241
242
|
graph.registerCallback(cliLoadingPlugin({ nodeId: "mulmoScript", message: "Generating script..." }));
|
|
243
|
+
graph.registerCallback(createUsageCallback(usageCollector));
|
|
242
244
|
const result = await graph.run();
|
|
243
245
|
if (!result?.writeJSON?.path) {
|
|
244
246
|
showErrorMessage("Script generation failed. Please try again.");
|
|
@@ -246,7 +248,7 @@ export const createMulmoScriptFromUrl = async ({ urls, templateName, outDirPath,
|
|
|
246
248
|
}
|
|
247
249
|
writingMessage(result?.writeJSON?.path ?? "");
|
|
248
250
|
};
|
|
249
|
-
export const createMulmoScriptFromFile = async (fileName, { templateName, outDirPath, filename, cacheDirPath, llm, llm_model, verbose }) => {
|
|
251
|
+
export const createMulmoScriptFromFile = async (fileName, { templateName, outDirPath, filename, cacheDirPath, llm, llm_model, verbose, usageCollector }) => {
|
|
250
252
|
mkdir(outDirPath);
|
|
251
253
|
mkdir(cacheDirPath);
|
|
252
254
|
const filePath = path.resolve(process.cwd(), fileName);
|
|
@@ -272,6 +274,7 @@ export const createMulmoScriptFromFile = async (fileName, { templateName, outDir
|
|
|
272
274
|
if (!verbose) {
|
|
273
275
|
graph.registerCallback(cliLoadingPlugin({ nodeId: "mulmoScript", message: "Generating script..." }));
|
|
274
276
|
}
|
|
277
|
+
graph.registerCallback(createUsageCallback(usageCollector));
|
|
275
278
|
const result = await graph.run();
|
|
276
279
|
if (!result?.writeJSON?.path) {
|
|
277
280
|
showErrorMessage("Script generation failed. Please try again.");
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ScriptingParams } from "../types/index.js";
|
|
2
|
-
export declare const createMulmoScriptInteractively: ({ outDirPath, cacheDirPath, filename, templateName, urls, llm, llm_model }: ScriptingParams) => Promise<void>;
|
|
2
|
+
export declare const createMulmoScriptInteractively: ({ outDirPath, cacheDirPath, filename, templateName, urls, llm, llm_model, usageCollector, }: ScriptingParams) => Promise<void>;
|
|
@@ -14,6 +14,7 @@ import { mulmoScriptSchema } from "../types/index.js";
|
|
|
14
14
|
import { browserlessAgent } from "@graphai/browserless_agent";
|
|
15
15
|
import validateSchemaAgent from "../agents/validate_schema_agent.js";
|
|
16
16
|
import { llmPair, settings2GraphAIConfig } from "../utils/utils.js";
|
|
17
|
+
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
17
18
|
import { interactiveClarificationPrompt, prefixPrompt } from "../utils/prompt.js";
|
|
18
19
|
// import { cliLoadingPlugin } from "../utils/plugins.js";
|
|
19
20
|
dotenv.config({ quiet: true });
|
|
@@ -199,7 +200,7 @@ const graphData = {
|
|
|
199
200
|
},
|
|
200
201
|
},
|
|
201
202
|
};
|
|
202
|
-
const scrapeWebContent = async (urls, cacheDirPath) => {
|
|
203
|
+
const scrapeWebContent = async (urls, cacheDirPath, usageCollector) => {
|
|
203
204
|
mkdir(cacheDirPath);
|
|
204
205
|
GraphAILogger.info(`${agentHeader} Scraping ${urls.length} URLs...\n`);
|
|
205
206
|
const browserlessCache = browserlessCacheGenerator(cacheDirPath);
|
|
@@ -212,16 +213,17 @@ const scrapeWebContent = async (urls, cacheDirPath) => {
|
|
|
212
213
|
];
|
|
213
214
|
const graph = new GraphAI(graphDataForScraping, { ...vanillaAgents, openAIAgent, textInputAgent, fileWriteAgent, browserlessAgent }, { agentFilters });
|
|
214
215
|
graph.injectValue("urls", urls);
|
|
216
|
+
graph.registerCallback(createUsageCallback(usageCollector));
|
|
215
217
|
const result = (await graph.run());
|
|
216
218
|
if (!result?.sourceText?.text) {
|
|
217
219
|
return "";
|
|
218
220
|
}
|
|
219
221
|
return `\n\n${prefixPrompt}\n${result?.sourceText.text}`;
|
|
220
222
|
};
|
|
221
|
-
export const createMulmoScriptInteractively = async ({ outDirPath, cacheDirPath, filename, templateName, urls, llm, llm_model }) => {
|
|
223
|
+
export const createMulmoScriptInteractively = async ({ outDirPath, cacheDirPath, filename, templateName, urls, llm, llm_model, usageCollector, }) => {
|
|
222
224
|
mkdir(outDirPath);
|
|
223
225
|
// if urls is not empty, scrape web content and reference it in the prompt
|
|
224
|
-
const webContentPrompt = urls.length > 0 ? await scrapeWebContent(urls, cacheDirPath) : "";
|
|
226
|
+
const webContentPrompt = urls.length > 0 ? await scrapeWebContent(urls, cacheDirPath, usageCollector) : "";
|
|
225
227
|
const { agent, model, max_tokens } = llmPair(llm, llm_model);
|
|
226
228
|
GraphAILogger.log({ agent, model, max_tokens });
|
|
227
229
|
const agentFilters = [
|
|
@@ -255,6 +257,7 @@ export const createMulmoScriptInteractively = async ({ outDirPath, cacheDirPath,
|
|
|
255
257
|
}
|
|
256
258
|
}
|
|
257
259
|
});
|
|
260
|
+
graph.registerCallback(createUsageCallback(usageCollector));
|
|
258
261
|
// graph.registerCallback(cliLoadingPlugin({ nodeId: "reply", message: "Loading..." }));
|
|
259
262
|
GraphAILogger.info(`${agentHeader} Hi! What topic would you like me to generate about?\n`);
|
|
260
263
|
await graph.run();
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { UsageCollectorAPI } from "../types/usage.js";
|
|
2
|
+
export declare const deepResearch: (usageCollector?: UsageCollectorAPI) => Promise<void>;
|
|
@@ -7,6 +7,7 @@ import * as agents from "@graphai/vanilla";
|
|
|
7
7
|
import tavilySearchAgent from "../agents/tavily_agent.js";
|
|
8
8
|
import { cliLoadingPlugin } from "../utils/plugins.js";
|
|
9
9
|
import { searchQueryPrompt, reflectionPrompt, finalAnswerPrompt } from "../utils/prompt.js";
|
|
10
|
+
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
10
11
|
dotenv.config({ quiet: true });
|
|
11
12
|
const vanillaAgents = agents.default ?? agents;
|
|
12
13
|
const agentHeader = "\x1b[34m● \x1b[0m\x1b[1mAgent\x1b[0m:\x1b[0m";
|
|
@@ -246,7 +247,7 @@ const graphData = {
|
|
|
246
247
|
},
|
|
247
248
|
},
|
|
248
249
|
};
|
|
249
|
-
export const deepResearch = async () => {
|
|
250
|
+
export const deepResearch = async (usageCollector) => {
|
|
250
251
|
const agentFilters = [
|
|
251
252
|
{
|
|
252
253
|
name: "consoleStreamDataAgentFilter",
|
|
@@ -260,6 +261,7 @@ export const deepResearch = async () => {
|
|
|
260
261
|
graph.registerCallback(cliLoadingPlugin({ nodeId: "reflectionAgent", message: "Analyzing search results..." }));
|
|
261
262
|
graph.registerCallback(cliLoadingPlugin({ nodeId: "tavilySearchAgent", message: "Searching..." }));
|
|
262
263
|
graph.registerCallback(cliLoadingPlugin({ nodeId: "finalAnswer", message: "Generating final answer..." }));
|
|
264
|
+
graph.registerCallback(createUsageCallback(usageCollector));
|
|
263
265
|
GraphAILogger.info(`${agentHeader} What would you like to know?\n`);
|
|
264
266
|
await graph.run();
|
|
265
267
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MulmoStoryboard, StoryToScriptGenerateMode } from "../types/index.js";
|
|
2
2
|
import type { LLM } from "../types/provider2agent.js";
|
|
3
|
-
|
|
3
|
+
import type { UsageCollectorAPI } from "../types/usage.js";
|
|
4
|
+
export declare const storyToScript: ({ story, beatsPerScene, templateName, outdir, fileName, llm, llmModel, generateMode, usageCollector, }: {
|
|
4
5
|
story: MulmoStoryboard;
|
|
5
6
|
beatsPerScene: number;
|
|
6
7
|
templateName: string;
|
|
@@ -9,4 +10,5 @@ export declare const storyToScript: ({ story, beatsPerScene, templateName, outdi
|
|
|
9
10
|
llm?: LLM;
|
|
10
11
|
llmModel?: string;
|
|
11
12
|
generateMode: StoryToScriptGenerateMode;
|
|
13
|
+
usageCollector?: UsageCollectorAPI;
|
|
12
14
|
}) => Promise<void>;
|
|
@@ -10,6 +10,7 @@ import { graphDataScriptGeneratePrompt, sceneToBeatsPrompt, storyToScriptInfoPro
|
|
|
10
10
|
import { fileWriteAgent } from "@graphai/vanilla_node_agents";
|
|
11
11
|
import validateSchemaAgent from "../agents/validate_schema_agent.js";
|
|
12
12
|
import { llmPair, settings2GraphAIConfig } from "../utils/utils.js";
|
|
13
|
+
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
13
14
|
import { storyToScriptGenerateMode } from "../types/const.js";
|
|
14
15
|
import { cliLoadingPlugin } from "../utils/plugins.js";
|
|
15
16
|
const vanillaAgents = agents.default ?? agents;
|
|
@@ -248,7 +249,7 @@ const generateScriptPrompt = async (template, beatsPerScene, story) => {
|
|
|
248
249
|
const script = readScriptTemplateFile(template.scriptName);
|
|
249
250
|
return storyToScriptPrompt(script, beatsPerScene, story);
|
|
250
251
|
};
|
|
251
|
-
export const storyToScript = async ({ story, beatsPerScene, templateName, outdir, fileName, llm, llmModel, generateMode, }) => {
|
|
252
|
+
export const storyToScript = async ({ story, beatsPerScene, templateName, outdir, fileName, llm, llmModel, generateMode, usageCollector, }) => {
|
|
252
253
|
const template = readAndParseJson(getPromptTemplateFilePath(templateName), mulmoPromptTemplateSchema);
|
|
253
254
|
const { agent, model, max_tokens } = llmPair(llm, llmModel);
|
|
254
255
|
const beatsPrompt = await generateBeatsPrompt(template, beatsPerScene, story);
|
|
@@ -271,6 +272,7 @@ export const storyToScript = async ({ story, beatsPerScene, templateName, outdir
|
|
|
271
272
|
graph.injectValue("llmModel", model);
|
|
272
273
|
graph.injectValue("maxTokens", max_tokens);
|
|
273
274
|
graph.registerCallback(cliLoadingPlugin({ nodeId: "script", message: "Generating script..." }));
|
|
275
|
+
graph.registerCallback(createUsageCallback(usageCollector));
|
|
274
276
|
const result = await graph.run();
|
|
275
277
|
writingMessage(result?.writeJSON?.path ?? "");
|
|
276
278
|
};
|
package/lib/types/agent.d.ts
CHANGED
|
@@ -10,10 +10,12 @@ export type OpenAIImageOptions = {
|
|
|
10
10
|
quality?: OpenAIImageQuality;
|
|
11
11
|
background?: "opaque" | "transparent" | "auto";
|
|
12
12
|
};
|
|
13
|
+
import type { AgentUsage } from "./usage.js";
|
|
13
14
|
export type AgentBufferResult = {
|
|
14
15
|
buffer?: Buffer;
|
|
15
16
|
saved?: string;
|
|
16
17
|
text?: string;
|
|
18
|
+
usage?: AgentUsage;
|
|
17
19
|
};
|
|
18
20
|
export type AgentPromptInputs = {
|
|
19
21
|
prompt: string;
|
package/lib/types/index.d.ts
CHANGED
package/lib/types/index.js
CHANGED
package/lib/types/type.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CallbackFunction } from "graphai";
|
|
2
|
+
import type { UsageCollectorAPI } from "./usage.js";
|
|
2
3
|
import { langSchema, localizedTextSchema, mulmoBeatSchema, mulmoScriptSchema, mulmoStudioSchema, mulmoStudioBeatSchema, mulmoStoryboardSchema, mulmoStoryboardSceneSchema, mulmoStudioMultiLingualSchema, mulmoStudioMultiLingualArraySchema, mulmoStudioMultiLingualDataSchema, mulmoStudioMultiLingualFileSchema, speakerDictionarySchema, speakerSchema, mulmoSpeechParamsSchema, mulmoImageParamsSchema, mulmoImageParamsImagesValueSchema, mulmoImageParamsImagesSchema, mulmoFillOptionSchema, mulmoTransitionSchema, mulmoVideoFilterSchema, mulmoMovieParamsSchema, mulmoSoundEffectParamsSchema, mulmoLipSyncParamsSchema, textSlideParamsSchema, speechOptionsSchema, speakerDataSchema, mulmoCanvasDimensionSchema, mulmoPromptTemplateSchema, mulmoPromptTemplateFileSchema, text2ImageProviderSchema, text2HtmlImageProviderSchema, text2MovieProviderSchema, text2SpeechProviderSchema, mulmoPresentationStyleSchema, multiLingualTextsSchema, mulmoImageAssetSchema, mulmoMermaidMediaSchema, mulmoTextSlideMediaSchema, mulmoMarkdownMediaSchema, mulmoImageMediaSchema, mulmoChartMediaSchema, mediaSourceSchema, mediaSourceMermaidSchema, backgroundImageSchema, backgroundImageSourceSchema, mulmoSessionStateSchema, mulmoOpenAIImageModelSchema, mulmoGoogleImageModelSchema, mulmoGoogleMovieModelSchema, mulmoReplicateMovieModelSchema, mulmoImagePromptMediaSchema, mulmoMovieMediaSchema, mulmoMoviePromptMediaSchema, markdownLayoutSchema, row2Schema, grid2x2Schema } from "./schema.js";
|
|
3
4
|
import { pdf_modes, pdf_sizes, storyToScriptGenerateMode } from "./const.js";
|
|
4
5
|
import type { LLM } from "./provider2agent.js";
|
|
@@ -68,6 +69,7 @@ export type MulmoStudioContext = {
|
|
|
68
69
|
sessionState: MulmoSessionState;
|
|
69
70
|
presentationStyle: MulmoPresentationStyle;
|
|
70
71
|
multiLingual: MulmoStudioMultiLingualArray;
|
|
72
|
+
usageCollector?: UsageCollectorAPI;
|
|
71
73
|
};
|
|
72
74
|
export type ScriptingParams = {
|
|
73
75
|
urls: string[];
|
|
@@ -78,6 +80,7 @@ export type ScriptingParams = {
|
|
|
78
80
|
llm_model?: string;
|
|
79
81
|
llm?: LLM;
|
|
80
82
|
verbose?: boolean;
|
|
83
|
+
usageCollector?: UsageCollectorAPI;
|
|
81
84
|
};
|
|
82
85
|
export type ImageProcessorParams = {
|
|
83
86
|
beat: MulmoBeat;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type AgentUsage = {
|
|
2
|
+
provider: string;
|
|
3
|
+
model: string;
|
|
4
|
+
inputTokens?: number;
|
|
5
|
+
outputTokens?: number;
|
|
6
|
+
totalTokens?: number;
|
|
7
|
+
predictSec?: number;
|
|
8
|
+
inputChars?: number;
|
|
9
|
+
};
|
|
10
|
+
export type UsageRecord = {
|
|
11
|
+
agent: string;
|
|
12
|
+
provider: string;
|
|
13
|
+
model: string;
|
|
14
|
+
beatIndex?: number;
|
|
15
|
+
inputTokens?: number;
|
|
16
|
+
outputTokens?: number;
|
|
17
|
+
totalTokens?: number;
|
|
18
|
+
predictSec?: number;
|
|
19
|
+
inputChars?: number;
|
|
20
|
+
cached: boolean;
|
|
21
|
+
retryAttempt?: number;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
};
|
|
24
|
+
export type UsageCollectorAPI = {
|
|
25
|
+
add(record: Omit<UsageRecord, "timestamp"> & {
|
|
26
|
+
timestamp?: string;
|
|
27
|
+
}): void;
|
|
28
|
+
snapshot(): UsageRecord[];
|
|
29
|
+
merge(other: UsageCollectorAPI): void;
|
|
30
|
+
clear(): void;
|
|
31
|
+
readonly size: number;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/utils/context.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MulmoStudioBeat, MulmoScript, MulmoPresentationStyle, MulmoStudioMultiLingual, FileObject } from "../types/type.js";
|
|
2
|
+
import { UsageCollector } from "./usage_collector.js";
|
|
2
3
|
export declare const silentMp3 = "https://github.com/receptron/mulmocast-cli/raw/refs/heads/main/assets/audio/silent300.mp3";
|
|
3
4
|
export type MulmoErrorFormatter = (error: unknown) => string | null;
|
|
4
5
|
export declare const setMulmoErrorFormatter: (formatter: MulmoErrorFormatter | null) => void;
|
|
@@ -5504,4 +5505,5 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
|
|
|
5504
5505
|
force: boolean;
|
|
5505
5506
|
backup: boolean;
|
|
5506
5507
|
lang: string;
|
|
5508
|
+
usageCollector: UsageCollector;
|
|
5507
5509
|
} | null>;
|
package/lib/utils/context.js
CHANGED
|
@@ -4,6 +4,7 @@ import { beatId, multiLingualObjectToArray } from "./utils.js";
|
|
|
4
4
|
import { mulmoStudioSchema, mulmoCaptionParamsSchema, mulmoPresentationStyleSchema } from "../types/schema.js";
|
|
5
5
|
import { MulmoPresentationStyleMethods, MulmoScriptMethods, MulmoStudioMultiLingualMethod } from "../methods/index.js";
|
|
6
6
|
import { loadMulmoConfig, mergeConfigWithScript } from "./mulmo_config.js";
|
|
7
|
+
import { UsageCollector } from "./usage_collector.js";
|
|
7
8
|
export const silentMp3 = "https://github.com/receptron/mulmocast-cli/raw/refs/heads/main/assets/audio/silent300.mp3";
|
|
8
9
|
let mulmoErrorFormatter = null;
|
|
9
10
|
export const setMulmoErrorFormatter = (formatter) => {
|
|
@@ -151,6 +152,7 @@ export const initializeContextFromFiles = async (files, raiseError, force, withB
|
|
|
151
152
|
force: Boolean(force),
|
|
152
153
|
backup: Boolean(withBackup),
|
|
153
154
|
lang: targetLang ?? studio.script.lang, // This lang is target Language. studio.lang is default Language
|
|
155
|
+
usageCollector: new UsageCollector(),
|
|
154
156
|
};
|
|
155
157
|
}
|
|
156
158
|
catch (error) {
|
package/lib/utils/filters.js
CHANGED
|
@@ -37,7 +37,7 @@ export const fileCacheAgentFilter = async (context, next) => {
|
|
|
37
37
|
const output = (await next(context)) || undefined;
|
|
38
38
|
const { buffer, text, saved } = output ?? {};
|
|
39
39
|
if (saved) {
|
|
40
|
-
return
|
|
40
|
+
return output;
|
|
41
41
|
}
|
|
42
42
|
if (buffer) {
|
|
43
43
|
writingMessage(file);
|
|
@@ -45,7 +45,7 @@ export const fileCacheAgentFilter = async (context, next) => {
|
|
|
45
45
|
if (backup) {
|
|
46
46
|
await fsPromise.writeFile(getBackupFilePath(file), buffer);
|
|
47
47
|
}
|
|
48
|
-
return
|
|
48
|
+
return output;
|
|
49
49
|
}
|
|
50
50
|
else if (text) {
|
|
51
51
|
writingMessage(file);
|
|
@@ -53,10 +53,7 @@ export const fileCacheAgentFilter = async (context, next) => {
|
|
|
53
53
|
if (backup) {
|
|
54
54
|
await fsPromise.writeFile(getBackupFilePath(file), text, "utf-8");
|
|
55
55
|
}
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
else if (saved) {
|
|
59
|
-
return true;
|
|
56
|
+
return output;
|
|
60
57
|
}
|
|
61
58
|
GraphAILogger.log("no cache, no buffer: " + file);
|
|
62
59
|
return false;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type Replicate from "replicate";
|
|
2
|
+
export declare const runReplicateWithMetrics: (replicate: Replicate, identifier: `${string}/${string}` | `${string}/${string}:${string}`, input: object, signal?: AbortSignal) => Promise<{
|
|
3
|
+
output: unknown;
|
|
4
|
+
predictSec?: number;
|
|
5
|
+
}>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Capture Replicate's exact billed seconds (`metrics.predict_time`) via the
|
|
2
|
+
// progress callback on `replicate.run()`. Avoids switching the four
|
|
3
|
+
// replicate agents from `.run()` to `predictions.create()` + poll, which
|
|
4
|
+
// would be a much bigger blast radius.
|
|
5
|
+
export const runReplicateWithMetrics = async (replicate, identifier, input, signal) => {
|
|
6
|
+
let lastPrediction;
|
|
7
|
+
const output = await replicate.run(identifier, { input, signal }, (prediction) => {
|
|
8
|
+
lastPrediction = prediction;
|
|
9
|
+
});
|
|
10
|
+
return { output, predictSec: lastPrediction?.metrics?.predict_time };
|
|
11
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type CallbackFunction } from "graphai";
|
|
2
|
+
import type { MulmoStudioContext } from "../types/type.js";
|
|
3
|
+
import type { UsageCollectorAPI } from "../types/usage.js";
|
|
4
|
+
export declare const createUsageCallback: (target: MulmoStudioContext | UsageCollectorAPI | undefined) => CallbackFunction;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { NodeState } from "graphai";
|
|
2
|
+
// @graphai/* LLM agents (openAIAgent / geminiAgent / anthropicAgent / groqAgent)
|
|
3
|
+
// return usage in the OpenAI chat-completions wire shape:
|
|
4
|
+
// { prompt_tokens, completion_tokens, total_tokens }
|
|
5
|
+
// without provider/model on the usage object. We derive provider from the
|
|
6
|
+
// node's agentId and model from the node's params or the spread response.
|
|
7
|
+
const LLM_AGENT_TO_PROVIDER = {
|
|
8
|
+
openAIAgent: "openai",
|
|
9
|
+
geminiAgent: "google",
|
|
10
|
+
anthropicAgent: "anthropic",
|
|
11
|
+
groqAgent: "groq",
|
|
12
|
+
};
|
|
13
|
+
const isAgentUsage = (value) => {
|
|
14
|
+
if (!value || typeof value !== "object")
|
|
15
|
+
return false;
|
|
16
|
+
const { provider, model } = value;
|
|
17
|
+
return typeof provider === "string" && typeof model === "string";
|
|
18
|
+
};
|
|
19
|
+
const extractLLMUsage = (log) => {
|
|
20
|
+
const provider = log.agentId ? LLM_AGENT_TO_PROVIDER[log.agentId] : undefined;
|
|
21
|
+
if (!provider)
|
|
22
|
+
return undefined;
|
|
23
|
+
const result = log.result;
|
|
24
|
+
const u = result?.usage;
|
|
25
|
+
if (!u || typeof u.prompt_tokens !== "number" || typeof u.total_tokens !== "number")
|
|
26
|
+
return undefined;
|
|
27
|
+
const pickModel = () => {
|
|
28
|
+
const fromParams = log.params?.model;
|
|
29
|
+
if (typeof fromParams === "string")
|
|
30
|
+
return fromParams;
|
|
31
|
+
if (typeof result?.model === "string")
|
|
32
|
+
return result.model;
|
|
33
|
+
return "unknown";
|
|
34
|
+
};
|
|
35
|
+
const model = pickModel();
|
|
36
|
+
return {
|
|
37
|
+
provider,
|
|
38
|
+
model,
|
|
39
|
+
inputTokens: u.prompt_tokens,
|
|
40
|
+
outputTokens: u.completion_tokens,
|
|
41
|
+
totalTokens: u.total_tokens,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const extractUsage = (log) => {
|
|
45
|
+
// 1. mulmocast AgentUsage shape (image/TTS agents in this repo).
|
|
46
|
+
if (log.result && typeof log.result === "object") {
|
|
47
|
+
const { usage } = log.result;
|
|
48
|
+
if (isAgentUsage(usage))
|
|
49
|
+
return usage;
|
|
50
|
+
}
|
|
51
|
+
// 2. @graphai/* LLM shape — derive provider/model from log metadata.
|
|
52
|
+
return extractLLMUsage(log);
|
|
53
|
+
};
|
|
54
|
+
const resolveCollector = (target) => {
|
|
55
|
+
if (!target)
|
|
56
|
+
return undefined;
|
|
57
|
+
// A MulmoStudioContext has the optional `usageCollector` slot; the API itself
|
|
58
|
+
// doesn't. Distinguishing on the slot name lets either form pass through.
|
|
59
|
+
if ("usageCollector" in target)
|
|
60
|
+
return target.usageCollector;
|
|
61
|
+
return target;
|
|
62
|
+
};
|
|
63
|
+
// GraphAI callback that pushes any agent's reported usage into the given
|
|
64
|
+
// UsageCollector when the node completes successfully. No-op when the
|
|
65
|
+
// collector is absent (either undefined directly or `context.usageCollector`
|
|
66
|
+
// undefined), the node fails, or the result has no `usage`.
|
|
67
|
+
export const createUsageCallback = (target) => {
|
|
68
|
+
return (log, isUpdate) => {
|
|
69
|
+
if (isUpdate)
|
|
70
|
+
return;
|
|
71
|
+
if (log.state !== NodeState.Completed)
|
|
72
|
+
return;
|
|
73
|
+
const collector = resolveCollector(target);
|
|
74
|
+
if (!collector)
|
|
75
|
+
return;
|
|
76
|
+
const usage = extractUsage(log);
|
|
77
|
+
if (!usage)
|
|
78
|
+
return;
|
|
79
|
+
collector.add({
|
|
80
|
+
agent: log.agentId ?? "unknown",
|
|
81
|
+
provider: usage.provider,
|
|
82
|
+
model: usage.model,
|
|
83
|
+
beatIndex: log.mapIndex,
|
|
84
|
+
inputTokens: usage.inputTokens,
|
|
85
|
+
outputTokens: usage.outputTokens,
|
|
86
|
+
totalTokens: usage.totalTokens,
|
|
87
|
+
predictSec: usage.predictSec,
|
|
88
|
+
inputChars: usage.inputChars,
|
|
89
|
+
cached: false,
|
|
90
|
+
retryAttempt: log.retryCount,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UsageRecord, UsageCollectorAPI } from "../types/usage.js";
|
|
2
|
+
export type { UsageRecord, UsageCollectorAPI } from "../types/usage.js";
|
|
3
|
+
export declare class UsageCollector implements UsageCollectorAPI {
|
|
4
|
+
private readonly records;
|
|
5
|
+
add(record: Omit<UsageRecord, "timestamp"> & {
|
|
6
|
+
timestamp?: string;
|
|
7
|
+
}): void;
|
|
8
|
+
snapshot(): UsageRecord[];
|
|
9
|
+
merge(other: UsageCollectorAPI): void;
|
|
10
|
+
clear(): void;
|
|
11
|
+
get size(): number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class UsageCollector {
|
|
2
|
+
records = [];
|
|
3
|
+
add(record) {
|
|
4
|
+
this.records.push({ ...record, timestamp: record.timestamp ?? new Date().toISOString() });
|
|
5
|
+
}
|
|
6
|
+
snapshot() {
|
|
7
|
+
return this.records.map((record) => ({ ...record }));
|
|
8
|
+
}
|
|
9
|
+
merge(other) {
|
|
10
|
+
other.snapshot().forEach((record) => this.records.push({ ...record }));
|
|
11
|
+
}
|
|
12
|
+
clear() {
|
|
13
|
+
this.records.length = 0;
|
|
14
|
+
}
|
|
15
|
+
get size() {
|
|
16
|
+
return this.records.length;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mulmocast",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.21",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.node.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"resolutions": {
|
|
27
27
|
"minimatch": "^10.2.5",
|
|
28
|
-
"tar": "7.5.
|
|
28
|
+
"tar": "7.5.16",
|
|
29
29
|
"yauzl": "^3.3.1"
|
|
30
30
|
},
|
|
31
31
|
"bin": {
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"@inquirer/input": "^5.0.13",
|
|
103
103
|
"@inquirer/select": "^5.1.5",
|
|
104
104
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
105
|
-
"@modernized/fluent-ffmpeg": "^1.0.
|
|
105
|
+
"@modernized/fluent-ffmpeg": "^1.0.1",
|
|
106
106
|
"@mozilla/readability": "^0.6.0",
|
|
107
107
|
"@mulmocast/deck": "^1.1.0",
|
|
108
108
|
"@tavily/core": "^0.7.3",
|
|
@@ -127,16 +127,16 @@
|
|
|
127
127
|
"@types/jsdom": "^28.0.3",
|
|
128
128
|
"@types/yargs": "^17.0.35",
|
|
129
129
|
"cross-env": "^10.1.0",
|
|
130
|
-
"eslint": "^10.
|
|
130
|
+
"eslint": "^10.5.0",
|
|
131
131
|
"eslint-config-prettier": "^10.1.8",
|
|
132
132
|
"eslint-plugin-import": "^2.32.0",
|
|
133
133
|
"eslint-plugin-prettier": "^5.5.6",
|
|
134
134
|
"eslint-plugin-sonarjs": "^4.0.3",
|
|
135
135
|
"globals": "^17.6.0",
|
|
136
|
-
"prettier": "^3.8.
|
|
136
|
+
"prettier": "^3.8.4",
|
|
137
137
|
"tsx": "^4.22.4",
|
|
138
138
|
"typescript": "6.0.3",
|
|
139
|
-
"typescript-eslint": "^8.
|
|
139
|
+
"typescript-eslint": "^8.61.1"
|
|
140
140
|
},
|
|
141
141
|
"engines": {
|
|
142
142
|
"node": ">=22.0.0"
|