mulmocast 2.6.20 → 2.6.22

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.
Files changed (53) hide show
  1. package/README.md +36 -0
  2. package/lib/actions/audio.js +3 -0
  3. package/lib/actions/image_references.js +3 -0
  4. package/lib/actions/images.js +3 -0
  5. package/lib/actions/translate.d.ts +2 -0
  6. package/lib/actions/translate.js +5 -0
  7. package/lib/agents/image_genai_agent.js +12 -3
  8. package/lib/agents/image_openai_agent.js +20 -5
  9. package/lib/agents/image_replicate_agent.js +9 -3
  10. package/lib/agents/lipsync_replicate_agent.js +10 -5
  11. package/lib/agents/movie_genai_agent.js +22 -4
  12. package/lib/agents/movie_replicate_agent.js +16 -5
  13. package/lib/agents/sound_effect_replicate_agent.js +4 -4
  14. package/lib/agents/tts_elevenlabs_agent.js +12 -1
  15. package/lib/agents/tts_gemini_agent.js +59 -32
  16. package/lib/agents/tts_google_agent.js +9 -1
  17. package/lib/agents/tts_kotodama_agent.js +9 -1
  18. package/lib/agents/tts_openai_agent.js +9 -1
  19. package/lib/cli/commands/audio/handler.js +2 -1
  20. package/lib/cli/commands/bundle/handler.js +2 -1
  21. package/lib/cli/commands/html/handler.js +2 -1
  22. package/lib/cli/commands/image/handler.js +2 -1
  23. package/lib/cli/commands/markdown/handler.js +2 -1
  24. package/lib/cli/commands/movie/handler.js +2 -1
  25. package/lib/cli/commands/pdf/handler.js +2 -1
  26. package/lib/cli/commands/tool/whisper/handler.js +60 -37
  27. package/lib/cli/commands/translate/handler.js +2 -1
  28. package/lib/cli/helpers.d.ts +1 -0
  29. package/lib/cli/helpers.js +46 -0
  30. package/lib/tools/create_mulmo_script_from_url.d.ts +2 -2
  31. package/lib/tools/create_mulmo_script_from_url.js +5 -2
  32. package/lib/tools/create_mulmo_script_interactively.d.ts +1 -1
  33. package/lib/tools/create_mulmo_script_interactively.js +6 -3
  34. package/lib/tools/deep_research.d.ts +2 -1
  35. package/lib/tools/deep_research.js +3 -1
  36. package/lib/tools/story_to_script.d.ts +3 -1
  37. package/lib/tools/story_to_script.js +3 -1
  38. package/lib/types/agent.d.ts +2 -0
  39. package/lib/types/index.d.ts +1 -0
  40. package/lib/types/index.js +1 -0
  41. package/lib/types/type.d.ts +3 -0
  42. package/lib/types/usage.d.ts +32 -0
  43. package/lib/types/usage.js +1 -0
  44. package/lib/utils/context.d.ts +2 -0
  45. package/lib/utils/context.js +2 -0
  46. package/lib/utils/filters.js +3 -6
  47. package/lib/utils/replicate_usage.d.ts +5 -0
  48. package/lib/utils/replicate_usage.js +11 -0
  49. package/lib/utils/usage_callback.d.ts +4 -0
  50. package/lib/utils/usage_callback.js +93 -0
  51. package/lib/utils/usage_collector.d.ts +12 -0
  52. package/lib/utils/usage_collector.js +18 -0
  53. package/package.json +6 -6
@@ -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
- return { buffer };
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
- return { buffer };
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
  };
@@ -51,40 +51,63 @@ export const handler = async (argv) => {
51
51
  GraphAILogger.error("Error: OPENAI_API_KEY environment variable is required");
52
52
  process.exit(1);
53
53
  }
54
- try {
55
- // Get audio duration using FFmpeg
56
- const { duration: audioDuration } = await ffmpegGetMediaDuration(fullPath);
57
- GraphAILogger.info(`Audio duration: ${audioDuration.toFixed(2)} seconds`);
58
- const openai = new OpenAI({ apiKey });
59
- const transcription = await openai.audio.transcriptions.create({
60
- file: createReadStream(fullPath),
61
- model: "whisper-1",
62
- response_format: "verbose_json",
63
- timestamp_granularities: ["word", "segment"],
64
- });
65
- if (transcription.segments) {
66
- const starts = transcription.segments.map((segment) => segment.start);
67
- starts[0] = 0;
68
- starts.push(audioDuration);
69
- // Create beats from transcription segments
70
- const beats = transcription.segments.map((segment, index) => {
71
- const duration = Math.round((starts[index + 1] - starts[index]) * 100) / 100;
72
- return {
73
- text: segment.text,
74
- duration,
75
- /*
76
- image: {
77
- type: "textSlide",
78
- slide: {
79
- title: "Place Holder",
80
- },
81
- },
82
- */
83
- };
54
+ // Each phase has its own try/catch so a failure points at the right
55
+ // subsystem instead of being collapsed into "Error transcribing audio"
56
+ // (same anti-pattern as #1451 in tts_gemini_agent.ts — an ffmpeg
57
+ // SIGABRT or a permission-denied write would otherwise read as an
58
+ // OpenAI-side failure).
59
+ // Phase 1 ffmpeg: read the audio file's duration.
60
+ const audioDuration = await (async () => {
61
+ try {
62
+ const { duration } = await ffmpegGetMediaDuration(fullPath);
63
+ return duration;
64
+ }
65
+ catch (error) {
66
+ GraphAILogger.error("Error reading audio duration with ffmpeg:", error);
67
+ process.exit(1);
68
+ }
69
+ })();
70
+ GraphAILogger.info(`Audio duration: ${audioDuration.toFixed(2)} seconds`);
71
+ // Phase 2 OpenAI: Whisper transcription API call.
72
+ const transcription = await (async () => {
73
+ try {
74
+ const openai = new OpenAI({ apiKey });
75
+ return await openai.audio.transcriptions.create({
76
+ file: createReadStream(fullPath),
77
+ model: "whisper-1",
78
+ response_format: "verbose_json",
79
+ timestamp_granularities: ["word", "segment"],
84
80
  });
85
- // Create the script with the processed beats
86
- const script = createMulmoScript(fullPath, beats);
87
- // Save script to output directory
81
+ }
82
+ catch (error) {
83
+ GraphAILogger.error("Error calling OpenAI transcription API:", error);
84
+ process.exit(1);
85
+ }
86
+ })();
87
+ if (transcription.segments) {
88
+ const starts = transcription.segments.map((segment) => segment.start);
89
+ starts[0] = 0;
90
+ starts.push(audioDuration);
91
+ // Create beats from transcription segments
92
+ const beats = transcription.segments.map((segment, index) => {
93
+ const duration = Math.round((starts[index + 1] - starts[index]) * 100) / 100;
94
+ return {
95
+ text: segment.text,
96
+ duration,
97
+ /*
98
+ image: {
99
+ type: "textSlide",
100
+ slide: {
101
+ title: "Place Holder",
102
+ },
103
+ },
104
+ */
105
+ };
106
+ });
107
+ // Create the script with the processed beats
108
+ const script = createMulmoScript(fullPath, beats);
109
+ // Phase 3 — fs: write the generated script to disk.
110
+ try {
88
111
  const outputDir = "output";
89
112
  if (!existsSync(outputDir)) {
90
113
  mkdirSync(outputDir, { recursive: true });
@@ -93,9 +116,9 @@ export const handler = async (argv) => {
93
116
  writeFileSync(outputPath, JSON.stringify(script, null, 2));
94
117
  GraphAILogger.info(`Script saved to: ${outputPath}`);
95
118
  }
96
- }
97
- catch (error) {
98
- GraphAILogger.error("Error transcribing audio:", error);
99
- process.exit(1);
119
+ catch (error) {
120
+ GraphAILogger.error("Error writing transcription output file:", error);
121
+ process.exit(1);
122
+ }
100
123
  }
101
124
  };
@@ -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
  };
@@ -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;
@@ -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
- export declare const deepResearch: () => Promise<void>;
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
- export declare const storyToScript: ({ story, beatsPerScene, templateName, outdir, fileName, llm, llmModel, generateMode, }: {
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
  };
@@ -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;
@@ -1,3 +1,4 @@
1
1
  export * from "./type.js";
2
2
  export * from "./schema.js";
3
3
  export * from "./viewer.js";
4
+ export * from "./usage.js";
@@ -1,3 +1,4 @@
1
1
  export * from "./type.js";
2
2
  export * from "./schema.js";
3
3
  export * from "./viewer.js";
4
+ export * from "./usage.js";
@@ -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 {};
@@ -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>;
@@ -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) {
@@ -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 true;
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 true;
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 true;
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;