mulmocast 2.6.22 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/lib/agents/tts_elevenlabs_agent.js +6 -1
- package/lib/cli/commands/audio/builder.d.ts +2 -14
- package/lib/cli/commands/audio/builder.js +2 -2
- package/lib/cli/commands/audio/handler.d.ts +2 -0
- package/lib/cli/commands/audio/handler.js +5 -1
- package/lib/cli/commands/image/builder.d.ts +2 -14
- package/lib/cli/commands/image/builder.js +2 -2
- package/lib/cli/commands/image/handler.d.ts +2 -0
- package/lib/cli/commands/image/handler.js +5 -1
- package/lib/cli/commands/movie/builder.d.ts +2 -14
- package/lib/cli/commands/movie/builder.js +2 -2
- package/lib/cli/commands/movie/handler.d.ts +2 -0
- package/lib/cli/commands/movie/handler.js +5 -1
- package/lib/cli/commands/pdf/builder.d.ts +2 -14
- package/lib/cli/commands/pdf/builder.js +2 -2
- package/lib/cli/commands/pdf/handler.d.ts +2 -0
- package/lib/cli/commands/pdf/handler.js +5 -1
- package/lib/cli/commands/translate/builder.d.ts +3 -15
- package/lib/cli/commands/translate/builder.js +2 -2
- package/lib/cli/commands/translate/handler.d.ts +2 -0
- package/lib/cli/commands/translate/handler.js +5 -1
- package/lib/cli/common.d.ts +5 -0
- package/lib/cli/common.js +13 -0
- package/lib/cli/helpers.d.ts +2 -0
- package/lib/cli/helpers.js +19 -0
- package/lib/index.common.d.ts +2 -0
- package/lib/index.common.js +2 -0
- package/lib/methods/mulmo_presentation_style.d.ts +2 -0
- package/lib/methods/mulmo_presentation_style.js +9 -6
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/provider2agent.d.ts +17 -0
- package/lib/types/provider2agent.js +79 -0
- package/lib/types/usage.d.ts +21 -0
- package/lib/utils/estimate_usage.d.ts +11 -0
- package/lib/utils/estimate_usage.js +332 -0
- package/lib/utils/estimate_usage_format.d.ts +2 -0
- package/lib/utils/estimate_usage_format.js +64 -0
- package/package.json +17 -16
package/README.md
CHANGED
|
@@ -419,6 +419,30 @@ Payload shape:
|
|
|
419
419
|
|
|
420
420
|
`byModel` groups by `provider:model` (different models have different rate cards, so summing across them isn't meaningful). The full per-call snapshot is also included so a billing layer can apply its own grouping. See [docs/api.md § Usage tracking](./docs/api.md#usage-tracking) for the per-agent field-population matrix and the programmatic (library) API.
|
|
421
421
|
|
|
422
|
+
### Usage estimation (before running anything)
|
|
423
|
+
|
|
424
|
+
Every generation command accepts `--estimate`: it prints what the command would consume — scoped to that command's pipeline — and exits without generating:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
mulmo movie script.json --estimate # table per provider:model + total cost
|
|
428
|
+
mulmo audio script.json --estimate --json # raw UsageEstimate[] records (pipe-friendly)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`mulmo audio --estimate` covers TTS (plus translation when `-l` differs from the script language); `mulmo images` / `mulmo pdf` cover image, htmlImage, movie, sound-effect, lip-sync and reference-media generation; `mulmo movie` covers everything; `mulmo translate` only translation. Values marked `~` are heuristic estimates; unmarked values are exact for the given script.
|
|
432
|
+
|
|
433
|
+
The library API can estimate the same thing — per beat, per process — without calling any API:
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import { estimateUsage } from "mulmocast";
|
|
437
|
+
|
|
438
|
+
const estimates = estimateUsage(script, { targetLangs: ["ja"] });
|
|
439
|
+
// [{ process: "tts", beatIndex: 0, provider: "openai", model: "gpt-4o-mini-tts",
|
|
440
|
+
// inputChars: { value: 50, precision: "exact" }, outputTokens: { value: 150, precision: "estimated" },
|
|
441
|
+
// costUSD: 0.0019, pricingAsOf: "2026-07-03" }, ...]
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Each metric carries a `precision` flag: `"exact"` for values that are deterministic from the script (TTS character counts, tokenized OpenAI prompts, fixed image-output token tables) and `"estimated"` for heuristics (LLM output length, speech duration derived from text). `costUSD` appears when pricing data exists in `modelPricing` (`src/types/provider2agent.ts`), where every price records the date it was last verified. See [docs/api.md § Pre-run estimation](./docs/api.md#pre-run-estimation-estimateusage).
|
|
445
|
+
|
|
422
446
|
## Cache and Re-run
|
|
423
447
|
When running the same `mulmo` command multiple times, previously generated files are treated as cache. For example, audio or image files will not be regenerated if they already exist.
|
|
424
448
|
|
|
@@ -44,7 +44,12 @@ export const ttsElevenlabsAgent = async ({ namedInputs, params, config, }) => {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
GraphAILogger.info(e);
|
|
47
|
-
|
|
47
|
+
// Include the underlying message so fetch failures (DNS / TLS /
|
|
48
|
+
// ETIMEDOUT / ECONNRESET) are diagnosable from the thrown error
|
|
49
|
+
// — previously every such case collapsed to the static label.
|
|
50
|
+
// Same template as #1452 / #1453 / #1454 / #1455 / #1456 / #1457.
|
|
51
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
52
|
+
throw new Error(`TTS Eleven Labs Error: ${detail}`, {
|
|
48
53
|
cause: agentGenerationError("ttsElevenlabsAgent", audioAction, audioFileTarget),
|
|
49
54
|
});
|
|
50
55
|
}
|
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import type { Argv } from "yargs";
|
|
2
2
|
export declare const builder: (yargs: Argv) => Argv<{
|
|
3
|
-
|
|
3
|
+
estimate: boolean;
|
|
4
4
|
} & {
|
|
5
|
-
|
|
6
|
-
} & {
|
|
7
|
-
l: string | undefined;
|
|
8
|
-
} & {
|
|
9
|
-
f: boolean;
|
|
10
|
-
} & {
|
|
11
|
-
g: boolean;
|
|
12
|
-
} & {
|
|
13
|
-
backup: boolean;
|
|
14
|
-
} & {
|
|
15
|
-
p: string | undefined;
|
|
16
|
-
} & {
|
|
17
|
-
file: string | undefined;
|
|
5
|
+
json: boolean;
|
|
18
6
|
} & {
|
|
19
7
|
a: string | undefined;
|
|
20
8
|
}>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { commonOptions } from "../../common.js";
|
|
2
|
-
export const builder = (yargs) => commonOptions(yargs).option("a", {
|
|
1
|
+
import { commonOptions, estimateOptions } from "../../common.js";
|
|
2
|
+
export const builder = (yargs) => estimateOptions(commonOptions(yargs)).option("a", {
|
|
3
3
|
alias: "audiodir",
|
|
4
4
|
describe: "Audio output directory",
|
|
5
5
|
type: "string",
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { audio } from "../../../actions/index.js";
|
|
2
|
-
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded, printUsageEstimate } 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
|
+
if (argv.estimate) {
|
|
9
|
+
printUsageEstimate(context, "audio", argv.json);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
await runTranslateIfNeeded(context);
|
|
9
13
|
await audio(context);
|
|
10
14
|
dumpUsageIfRequested(context);
|
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import type { Argv } from "yargs";
|
|
2
2
|
export declare const builder: (yargs: Argv) => Argv<{
|
|
3
|
-
|
|
3
|
+
estimate: boolean;
|
|
4
4
|
} & {
|
|
5
|
-
|
|
6
|
-
} & {
|
|
7
|
-
l: string | undefined;
|
|
8
|
-
} & {
|
|
9
|
-
f: boolean;
|
|
10
|
-
} & {
|
|
11
|
-
g: boolean;
|
|
12
|
-
} & {
|
|
13
|
-
backup: boolean;
|
|
14
|
-
} & {
|
|
15
|
-
p: string | undefined;
|
|
16
|
-
} & {
|
|
17
|
-
file: string | undefined;
|
|
5
|
+
json: boolean;
|
|
18
6
|
} & {
|
|
19
7
|
i: string | undefined;
|
|
20
8
|
}>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { commonOptions } from "../../common.js";
|
|
2
|
-
export const builder = (yargs) => commonOptions(yargs).option("i", {
|
|
1
|
+
import { commonOptions, estimateOptions } from "../../common.js";
|
|
2
|
+
export const builder = (yargs) => estimateOptions(commonOptions(yargs)).option("i", {
|
|
3
3
|
alias: "imagedir",
|
|
4
4
|
describe: "Image output directory",
|
|
5
5
|
type: "string",
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { images } from "../../../actions/index.js";
|
|
2
|
-
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded, printUsageEstimate } 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
|
+
if (argv.estimate) {
|
|
9
|
+
printUsageEstimate(context, "images", argv.json);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
await runTranslateIfNeeded(context);
|
|
9
13
|
await images(context);
|
|
10
14
|
dumpUsageIfRequested(context);
|
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import type { Argv } from "yargs";
|
|
2
2
|
export declare const builder: (yargs: Argv) => Argv<{
|
|
3
|
-
|
|
3
|
+
estimate: boolean;
|
|
4
4
|
} & {
|
|
5
|
-
|
|
6
|
-
} & {
|
|
7
|
-
l: string | undefined;
|
|
8
|
-
} & {
|
|
9
|
-
f: boolean;
|
|
10
|
-
} & {
|
|
11
|
-
g: boolean;
|
|
12
|
-
} & {
|
|
13
|
-
backup: boolean;
|
|
14
|
-
} & {
|
|
15
|
-
p: string | undefined;
|
|
16
|
-
} & {
|
|
17
|
-
file: string | undefined;
|
|
5
|
+
json: boolean;
|
|
18
6
|
} & {
|
|
19
7
|
a: string | undefined;
|
|
20
8
|
} & {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { commonOptions } from "../../common.js";
|
|
1
|
+
import { commonOptions, estimateOptions } from "../../common.js";
|
|
2
2
|
import { languages } from "../../../types/const.js";
|
|
3
|
-
export const builder = (yargs) => commonOptions(yargs)
|
|
3
|
+
export const builder = (yargs) => estimateOptions(commonOptions(yargs))
|
|
4
4
|
.option("a", {
|
|
5
5
|
alias: "audiodir",
|
|
6
6
|
describe: "Audio output directory",
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { audio, images, movie, captions } from "../../../actions/index.js";
|
|
2
|
-
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded, printUsageEstimate } 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
|
+
if (argv.estimate) {
|
|
9
|
+
printUsageEstimate(context, "movie", argv.json);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
await runTranslateIfNeeded(context, true);
|
|
9
13
|
await audio(context).then(images).then(captions).then(movie);
|
|
10
14
|
dumpUsageIfRequested(context);
|
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import type { Argv } from "yargs";
|
|
2
2
|
export declare const builder: (yargs: Argv) => Argv<{
|
|
3
|
-
|
|
3
|
+
estimate: boolean;
|
|
4
4
|
} & {
|
|
5
|
-
|
|
6
|
-
} & {
|
|
7
|
-
l: string | undefined;
|
|
8
|
-
} & {
|
|
9
|
-
f: boolean;
|
|
10
|
-
} & {
|
|
11
|
-
g: boolean;
|
|
12
|
-
} & {
|
|
13
|
-
backup: boolean;
|
|
14
|
-
} & {
|
|
15
|
-
p: string | undefined;
|
|
16
|
-
} & {
|
|
17
|
-
file: string | undefined;
|
|
5
|
+
json: boolean;
|
|
18
6
|
} & {
|
|
19
7
|
i: string | undefined;
|
|
20
8
|
} & {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { commonOptions } from "../../common.js";
|
|
1
|
+
import { commonOptions, estimateOptions } from "../../common.js";
|
|
2
2
|
import { pdf_modes, pdf_sizes } from "../../../types/const.js";
|
|
3
|
-
export const builder = (yargs) => commonOptions(yargs)
|
|
3
|
+
export const builder = (yargs) => estimateOptions(commonOptions(yargs))
|
|
4
4
|
.option("i", {
|
|
5
5
|
alias: "imagedir",
|
|
6
6
|
describe: "Image output directory",
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { images, pdf } from "../../../actions/index.js";
|
|
2
|
-
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, runTranslateIfNeeded, printUsageEstimate } 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
|
+
if (argv.estimate) {
|
|
9
|
+
printUsageEstimate(context, "pdf", argv.json);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
await runTranslateIfNeeded(context);
|
|
9
13
|
await images(context);
|
|
10
14
|
await pdf(context, argv.pdf_mode, argv.pdf_size);
|
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import type { Argv } from "yargs";
|
|
2
|
-
export declare const builder: (yargs: Argv) => Argv<
|
|
3
|
-
|
|
2
|
+
export declare const builder: (yargs: Argv) => Argv<{
|
|
3
|
+
estimate: boolean;
|
|
4
4
|
} & {
|
|
5
|
-
|
|
5
|
+
json: boolean;
|
|
6
6
|
} & {
|
|
7
|
-
l: string | undefined;
|
|
8
|
-
} & {
|
|
9
|
-
f: boolean;
|
|
10
|
-
} & {
|
|
11
|
-
g: boolean;
|
|
12
|
-
} & {
|
|
13
|
-
backup: boolean;
|
|
14
|
-
} & {
|
|
15
|
-
p: string | undefined;
|
|
16
|
-
} & {
|
|
17
|
-
file: string | undefined;
|
|
18
|
-
}, "file"> & {
|
|
19
7
|
file: string | undefined;
|
|
20
8
|
}>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { commonOptions } from "../../common.js";
|
|
2
|
-
export const builder = (yargs) => commonOptions(yargs).positional("file", {
|
|
1
|
+
import { commonOptions, estimateOptions } from "../../common.js";
|
|
2
|
+
export const builder = (yargs) => estimateOptions(commonOptions(yargs)).positional("file", {
|
|
3
3
|
describe: "Mulmo Script File",
|
|
4
4
|
type: "string",
|
|
5
5
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { translate } from "../../../actions/index.js";
|
|
2
|
-
import { dumpUsageIfRequested, initializeContext } from "../../helpers.js";
|
|
2
|
+
import { dumpUsageIfRequested, initializeContext, printUsageEstimate } 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
|
+
if (argv.estimate) {
|
|
9
|
+
printUsageEstimate(context, "translate", argv.json);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
await translate(context);
|
|
9
13
|
dumpUsageIfRequested(context);
|
|
10
14
|
};
|
package/lib/cli/common.d.ts
CHANGED
package/lib/cli/common.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { languages } from "../types/const.js";
|
|
2
|
+
export const estimateOptions = (yargs) => {
|
|
3
|
+
return yargs
|
|
4
|
+
.option("estimate", {
|
|
5
|
+
describe: "Estimate API usage (tokens / characters / seconds / cost) and exit without generating",
|
|
6
|
+
type: "boolean",
|
|
7
|
+
default: false,
|
|
8
|
+
})
|
|
9
|
+
.option("json", {
|
|
10
|
+
describe: "With --estimate, print the raw JSON records instead of a table",
|
|
11
|
+
type: "boolean",
|
|
12
|
+
default: false,
|
|
13
|
+
});
|
|
14
|
+
};
|
|
2
15
|
export const commonOptions = (yargs) => {
|
|
3
16
|
return yargs
|
|
4
17
|
.option("o", {
|
package/lib/cli/helpers.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { type EstimateAction } from "../utils/estimate_usage.js";
|
|
1
2
|
import type { CliArgs } from "../types/cli_types.js";
|
|
2
3
|
import { FileObject, InitOptions, MulmoStudioContext } from "../types/index.js";
|
|
3
4
|
export declare const runTranslateIfNeeded: (context: MulmoStudioContext, includeCaption?: boolean) => Promise<void>;
|
|
4
5
|
export declare const dumpUsageIfRequested: (context: MulmoStudioContext) => void;
|
|
6
|
+
export declare const printUsageEstimate: (context: MulmoStudioContext, action: EstimateAction, asJson?: boolean) => void;
|
|
5
7
|
export declare const setGraphAILogger: (verbose: boolean | undefined, logValues?: Record<string, unknown>) => void;
|
|
6
8
|
export declare const getFileObject: (args: {
|
|
7
9
|
basedir?: string;
|
package/lib/cli/helpers.js
CHANGED
|
@@ -8,6 +8,8 @@ import { outDirName, imageDirName, audioDirName } from "../types/const.js";
|
|
|
8
8
|
import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
|
|
9
9
|
import { translate } from "../actions/translate.js";
|
|
10
10
|
import { initializeContextFromFiles } from "../utils/context.js";
|
|
11
|
+
import { estimateUsage, actionEstimateProcesses } from "../utils/estimate_usage.js";
|
|
12
|
+
import { formatUsageEstimates } from "../utils/estimate_usage_format.js";
|
|
11
13
|
export const runTranslateIfNeeded = async (context, includeCaption = false) => {
|
|
12
14
|
if (MulmoStudioContextMethods.needTranslate(context, includeCaption)) {
|
|
13
15
|
GraphAILogger.log("run translate");
|
|
@@ -60,6 +62,23 @@ export const dumpUsageIfRequested = (context) => {
|
|
|
60
62
|
fs.writeFileSync(setting, json, "utf-8");
|
|
61
63
|
GraphAILogger.info(`usage written to ${setting}`);
|
|
62
64
|
};
|
|
65
|
+
// Pre-run estimate for --estimate: scoped to what the invoked action would consume,
|
|
66
|
+
// resolved with the same context (script, presentation style, -l, caption lang) as a real run.
|
|
67
|
+
export const printUsageEstimate = (context, action, asJson = false) => {
|
|
68
|
+
const script = context.studio.script;
|
|
69
|
+
const lang = context.lang;
|
|
70
|
+
// The caption language triggers translation only where the real run does:
|
|
71
|
+
// movie (runTranslateIfNeeded with includeCaption) and the translate action itself.
|
|
72
|
+
const captionLang = action === "movie" || action === "translate" ? script.captionParams?.lang : undefined;
|
|
73
|
+
const targetLangs = [...new Set([lang, captionLang].filter((l) => !!l))];
|
|
74
|
+
const records = estimateUsage(script, {
|
|
75
|
+
presentationStyle: context.presentationStyle,
|
|
76
|
+
langs: lang ? [lang] : undefined,
|
|
77
|
+
targetLangs,
|
|
78
|
+
processes: actionEstimateProcesses[action],
|
|
79
|
+
});
|
|
80
|
+
GraphAILogger.info(asJson ? JSON.stringify(records, null, 2) : formatUsageEstimates(records));
|
|
81
|
+
};
|
|
63
82
|
export const setGraphAILogger = (verbose, logValues) => {
|
|
64
83
|
if (verbose) {
|
|
65
84
|
if (logValues) {
|
package/lib/index.common.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export * from "./utils/string.js";
|
|
|
5
5
|
export * from "./utils/utils.js";
|
|
6
6
|
export * from "./utils/prompt.js";
|
|
7
7
|
export * from "./utils/error_cause.js";
|
|
8
|
+
export * from "./utils/estimate_usage.js";
|
|
9
|
+
export * from "./utils/estimate_usage_format.js";
|
|
8
10
|
export * from "./methods/mulmo_presentation_style.js";
|
|
9
11
|
export * from "./methods/mulmo_script.js";
|
|
10
12
|
export * from "./methods/mulmo_studio_context.js";
|
package/lib/index.common.js
CHANGED
|
@@ -6,6 +6,8 @@ export * from "./utils/string.js";
|
|
|
6
6
|
export * from "./utils/utils.js";
|
|
7
7
|
export * from "./utils/prompt.js";
|
|
8
8
|
export * from "./utils/error_cause.js";
|
|
9
|
+
export * from "./utils/estimate_usage.js";
|
|
10
|
+
export * from "./utils/estimate_usage_format.js";
|
|
9
11
|
export * from "./methods/mulmo_presentation_style.js";
|
|
10
12
|
export * from "./methods/mulmo_script.js";
|
|
11
13
|
export * from "./methods/mulmo_studio_context.js";
|
|
@@ -27,6 +27,7 @@ export declare const MulmoPresentationStyleMethods: {
|
|
|
27
27
|
*/
|
|
28
28
|
getResolvedSlideTheme(presentationStyle: MulmoPresentationStyle, beat: MulmoBeat): SlideTheme;
|
|
29
29
|
getDefaultSpeaker(presentationStyle: MulmoPresentationStyle): string;
|
|
30
|
+
getSpeakerData(presentationStyle: MulmoPresentationStyle, beat: MulmoBeat, lang: string | undefined): SpeakerData;
|
|
30
31
|
getSpeaker(context: MulmoStudioContext, beat: MulmoBeat, targetLang: string | undefined): SpeakerData;
|
|
31
32
|
getMovieTransition(context: MulmoStudioContext, beat: MulmoBeat): MulmoTransition | null;
|
|
32
33
|
getText2ImageProvider(provider: Text2ImageProvider | undefined): Text2ImageProvider;
|
|
@@ -222,6 +223,7 @@ export declare const MulmoPresentationStyleMethods: {
|
|
|
222
223
|
video?: string;
|
|
223
224
|
audio: string;
|
|
224
225
|
image?: string;
|
|
226
|
+
price_per_sec?: number;
|
|
225
227
|
}>;
|
|
226
228
|
};
|
|
227
229
|
/** Concurrency for image/movie generation graph (uses min of imageParams/movieParams) */
|
|
@@ -73,19 +73,22 @@ export const MulmoPresentationStyleMethods = {
|
|
|
73
73
|
}
|
|
74
74
|
return keys[0];
|
|
75
75
|
},
|
|
76
|
-
|
|
77
|
-
userAssert(!!
|
|
78
|
-
const speakerId = beat?.speaker ?? MulmoPresentationStyleMethods.getDefaultSpeaker(
|
|
79
|
-
const speaker =
|
|
76
|
+
getSpeakerData(presentationStyle, beat, lang) {
|
|
77
|
+
userAssert(!!presentationStyle?.speechParams?.speakers, "presentationStyle.speechParams.speakers is not set!!");
|
|
78
|
+
const speakerId = beat?.speaker ?? MulmoPresentationStyleMethods.getDefaultSpeaker(presentationStyle);
|
|
79
|
+
const speaker = presentationStyle.speechParams.speakers[speakerId];
|
|
80
80
|
userAssert(!!speaker, `speaker is not set: speaker "${speakerId}"`);
|
|
81
81
|
// Check if the speaker has a language-specific version.
|
|
82
|
-
// Normally, lang is determined by the context, but lang may be specified when using the API.
|
|
83
|
-
const lang = targetLang ?? context.lang ?? context?.studio?.script?.lang;
|
|
84
82
|
if (speaker.lang && lang && speaker.lang[lang]) {
|
|
85
83
|
return speaker.lang[lang];
|
|
86
84
|
}
|
|
87
85
|
return speaker;
|
|
88
86
|
},
|
|
87
|
+
getSpeaker(context, beat, targetLang) {
|
|
88
|
+
// Normally, lang is determined by the context, but lang may be specified when using the API.
|
|
89
|
+
const lang = targetLang ?? context.lang ?? context?.studio?.script?.lang;
|
|
90
|
+
return MulmoPresentationStyleMethods.getSpeakerData(context.presentationStyle, beat, lang);
|
|
91
|
+
},
|
|
89
92
|
getMovieTransition(context, beat) {
|
|
90
93
|
const transitionData = beat.movieParams?.transition ?? context.presentationStyle.movieParams?.transition;
|
|
91
94
|
if (!transitionData)
|
package/lib/types/index.d.ts
CHANGED
package/lib/types/index.js
CHANGED
|
@@ -158,6 +158,7 @@ export declare const provider2LipSyncAgent: {
|
|
|
158
158
|
video?: string;
|
|
159
159
|
audio: string;
|
|
160
160
|
image?: string;
|
|
161
|
+
price_per_sec?: number;
|
|
161
162
|
}>;
|
|
162
163
|
};
|
|
163
164
|
};
|
|
@@ -213,4 +214,20 @@ export declare const llm: (keyof typeof provider2LLMAgent)[];
|
|
|
213
214
|
export type LLM = keyof typeof provider2LLMAgent;
|
|
214
215
|
export declare const htmlLLMProvider: string[];
|
|
215
216
|
export declare const getModelDuration: (provider: keyof typeof provider2MovieAgent, model: string, movieDuration?: number) => number | undefined;
|
|
217
|
+
export type ModelPricing = {
|
|
218
|
+
unit: "tokens" | "chars" | "seconds" | "images";
|
|
219
|
+
inputPerMTokensUSD?: number;
|
|
220
|
+
outputPerMTokensUSD?: number;
|
|
221
|
+
perMCharsUSD?: number;
|
|
222
|
+
perSecUSD?: number;
|
|
223
|
+
perImageUSD?: number;
|
|
224
|
+
asOf: string;
|
|
225
|
+
};
|
|
226
|
+
export declare const gptImageOutputTokens: Record<string, {
|
|
227
|
+
low: number;
|
|
228
|
+
medium: number;
|
|
229
|
+
high: number;
|
|
230
|
+
}>;
|
|
231
|
+
export declare const modelPricing: Record<string, Record<string, ModelPricing>>;
|
|
232
|
+
export declare const getModelPricing: (provider: string, model: string) => ModelPricing | undefined;
|
|
216
233
|
export {};
|
|
@@ -517,3 +517,82 @@ export const getModelDuration = (provider, model, movieDuration) => {
|
|
|
517
517
|
}
|
|
518
518
|
return durations?.[0];
|
|
519
519
|
};
|
|
520
|
+
// gpt-image-1 / gpt-image-1-mini emit a fixed number of image output tokens per size × quality.
|
|
521
|
+
// Later gpt-image models (1.5 / 2) use variable resolutions, so this table is only an approximation for them.
|
|
522
|
+
// Source: https://developers.openai.com/api/docs/guides/image-generation (verified 2026-07-03)
|
|
523
|
+
export const gptImageOutputTokens = {
|
|
524
|
+
"1024x1024": { low: 272, medium: 1056, high: 4160 },
|
|
525
|
+
"1024x1536": { low: 408, medium: 1584, high: 6240 },
|
|
526
|
+
"1536x1024": { low: 400, medium: 1568, high: 6208 },
|
|
527
|
+
};
|
|
528
|
+
// Spot-checked against replicate.com model pages on this date (seedance-1-lite, kling-v2.1, omni-human).
|
|
529
|
+
// Some replicate prices vary by resolution/variant; price_per_sec holds the rate for the typical configuration
|
|
530
|
+
// (e.g. seedance-1-lite at 720p, kling-v2.1 standard).
|
|
531
|
+
const REPLICATE_PRICING_AS_OF = "2026-07-03";
|
|
532
|
+
const replicateMoviePricing = Object.fromEntries(Object.entries(provider2MovieAgent.replicate.modelParams).map(([model, params]) => [
|
|
533
|
+
model,
|
|
534
|
+
{ unit: "seconds", perSecUSD: params.price_per_sec, asOf: REPLICATE_PRICING_AS_OF },
|
|
535
|
+
]));
|
|
536
|
+
const replicateLipSyncPricing = Object.fromEntries(Object.entries(provider2LipSyncAgent.replicate.modelParams)
|
|
537
|
+
.filter(([, params]) => params.price_per_sec !== undefined)
|
|
538
|
+
.map(([model, params]) => [model, { unit: "seconds", perSecUSD: params.price_per_sec, asOf: REPLICATE_PRICING_AS_OF }]));
|
|
539
|
+
export const modelPricing = {
|
|
540
|
+
openai: {
|
|
541
|
+
// https://developers.openai.com/api/docs/models/gpt-4o-mini-tts
|
|
542
|
+
"gpt-4o-mini-tts": { unit: "tokens", inputPerMTokensUSD: 0.6, outputPerMTokensUSD: 12, asOf: "2026-07-03" },
|
|
543
|
+
"tts-1": { unit: "chars", perMCharsUSD: 15, asOf: "2026-07-03" },
|
|
544
|
+
"tts-1-hd": { unit: "chars", perMCharsUSD: 30, asOf: "2026-07-03" },
|
|
545
|
+
// https://developers.openai.com/api/docs/models/gpt-image-1 (image input tokens, $10/1M, are not modeled)
|
|
546
|
+
"gpt-image-1": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 40, asOf: "2026-07-03" },
|
|
547
|
+
"gpt-image-1.5": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 32, asOf: "2026-07-03" },
|
|
548
|
+
"gpt-image-2": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 30, asOf: "2026-07-03" },
|
|
549
|
+
"gpt-image-1-mini": { unit: "tokens", inputPerMTokensUSD: 2, outputPerMTokensUSD: 8, asOf: "2026-07-03" },
|
|
550
|
+
// https://developers.openai.com/api/docs/models/gpt-5 etc.
|
|
551
|
+
"gpt-5": { unit: "tokens", inputPerMTokensUSD: 1.25, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
552
|
+
"gpt-5-mini": { unit: "tokens", inputPerMTokensUSD: 0.25, outputPerMTokensUSD: 2, asOf: "2026-07-03" },
|
|
553
|
+
"gpt-4o": { unit: "tokens", inputPerMTokensUSD: 2.5, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
554
|
+
},
|
|
555
|
+
gemini: {
|
|
556
|
+
// https://ai.google.dev/gemini-api/docs/pricing
|
|
557
|
+
"gemini-2.5-flash-preview-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
558
|
+
"gemini-2.5-pro-preview-tts": { unit: "tokens", inputPerMTokensUSD: 1, outputPerMTokensUSD: 20, asOf: "2026-07-03" },
|
|
559
|
+
},
|
|
560
|
+
google: {
|
|
561
|
+
// https://ai.google.dev/gemini-api/docs/pricing ($0.039/image ≒ 1290 output tokens at $30/1M)
|
|
562
|
+
"gemini-2.5-flash-image": { unit: "images", inputPerMTokensUSD: 0.3, perImageUSD: 0.039, asOf: "2026-07-03" },
|
|
563
|
+
// Veo per second of generated video (720p). veo-2.0-generate-001 and veo-3.0-generate-001
|
|
564
|
+
// were shut down on 2026-06-30, so they intentionally have no price.
|
|
565
|
+
"veo-3.1-generate-preview": { unit: "seconds", perSecUSD: 0.4, asOf: "2026-07-03" },
|
|
566
|
+
"veo-3.1-lite-generate-preview": { unit: "seconds", perSecUSD: 0.05, asOf: "2026-07-03" },
|
|
567
|
+
// Google Cloud Text-to-Speech per 1M characters, keyed by voice tier.
|
|
568
|
+
// https://cloud.google.com/text-to-speech/pricing
|
|
569
|
+
"tts-standard": { unit: "chars", perMCharsUSD: 4, asOf: "2026-07-03" },
|
|
570
|
+
"tts-wavenet": { unit: "chars", perMCharsUSD: 4, asOf: "2026-07-03" },
|
|
571
|
+
"tts-neural2": { unit: "chars", perMCharsUSD: 16, asOf: "2026-07-03" },
|
|
572
|
+
"tts-chirp3-hd": { unit: "chars", perMCharsUSD: 30, asOf: "2026-07-03" },
|
|
573
|
+
"tts-studio": { unit: "chars", perMCharsUSD: 160, asOf: "2026-07-03" },
|
|
574
|
+
},
|
|
575
|
+
anthropic: {
|
|
576
|
+
// https://platform.claude.com/docs/en/docs/about-claude/pricing
|
|
577
|
+
"claude-sonnet-4-5-20250929": { unit: "tokens", inputPerMTokensUSD: 3, outputPerMTokensUSD: 15, asOf: "2026-07-03" },
|
|
578
|
+
},
|
|
579
|
+
elevenlabs: {
|
|
580
|
+
// https://elevenlabs.io/pricing/api ($0.10 per 1k chars for multilingual/v3, $0.05 for flash/turbo)
|
|
581
|
+
eleven_v3: { unit: "chars", perMCharsUSD: 100, asOf: "2026-07-03" },
|
|
582
|
+
eleven_multilingual_v2: { unit: "chars", perMCharsUSD: 100, asOf: "2026-07-03" },
|
|
583
|
+
eleven_turbo_v2_5: { unit: "chars", perMCharsUSD: 50, asOf: "2026-07-03" },
|
|
584
|
+
eleven_turbo_v2: { unit: "chars", perMCharsUSD: 50, asOf: "2026-07-03" },
|
|
585
|
+
eleven_flash_v2_5: { unit: "chars", perMCharsUSD: 50, asOf: "2026-07-03" },
|
|
586
|
+
eleven_flash_v2: { unit: "chars", perMCharsUSD: 50, asOf: "2026-07-03" },
|
|
587
|
+
},
|
|
588
|
+
replicate: {
|
|
589
|
+
...replicateMoviePricing,
|
|
590
|
+
...replicateLipSyncPricing,
|
|
591
|
+
// https://replicate.com/bytedance/seedream-4 ($0.03 per output image)
|
|
592
|
+
// zsxkib/mmaudio is intentionally unpriced: it bills by GPU time (~$0.005 per run), not by clip duration.
|
|
593
|
+
"bytedance/seedream-4": { unit: "images", perImageUSD: 0.03, asOf: "2026-07-03" },
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
export const getModelPricing = (provider, model) => {
|
|
597
|
+
return modelPricing[provider]?.[model];
|
|
598
|
+
};
|
package/lib/types/usage.d.ts
CHANGED
|
@@ -30,3 +30,24 @@ export type UsageCollectorAPI = {
|
|
|
30
30
|
clear(): void;
|
|
31
31
|
readonly size: number;
|
|
32
32
|
};
|
|
33
|
+
export type EstimatePrecision = "exact" | "estimated";
|
|
34
|
+
export type EstimatedMetric = {
|
|
35
|
+
value: number;
|
|
36
|
+
precision: EstimatePrecision;
|
|
37
|
+
};
|
|
38
|
+
export type UsageEstimateProcess = "tts" | "image" | "htmlImage" | "movie" | "soundEffect" | "lipSync" | "translate" | "imageReference" | "movieReference";
|
|
39
|
+
export type UsageEstimate = {
|
|
40
|
+
process: UsageEstimateProcess;
|
|
41
|
+
beatIndex?: number;
|
|
42
|
+
refKey?: string;
|
|
43
|
+
lang?: string;
|
|
44
|
+
provider: string;
|
|
45
|
+
model: string;
|
|
46
|
+
inputTokens?: EstimatedMetric;
|
|
47
|
+
outputTokens?: EstimatedMetric;
|
|
48
|
+
inputChars?: EstimatedMetric;
|
|
49
|
+
predictSec?: EstimatedMetric;
|
|
50
|
+
imageCount?: EstimatedMetric;
|
|
51
|
+
costUSD?: number;
|
|
52
|
+
pricingAsOf?: string;
|
|
53
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MulmoScript, MulmoPresentationStyle } from "../types/index.js";
|
|
2
|
+
import type { UsageEstimate, UsageEstimateProcess } from "../types/usage.js";
|
|
3
|
+
export type EstimateUsageOptions = {
|
|
4
|
+
langs?: string[];
|
|
5
|
+
targetLangs?: string[];
|
|
6
|
+
presentationStyle?: MulmoPresentationStyle;
|
|
7
|
+
processes?: UsageEstimateProcess[];
|
|
8
|
+
};
|
|
9
|
+
export type EstimateAction = "audio" | "images" | "pdf" | "movie" | "translate";
|
|
10
|
+
export declare const actionEstimateProcesses: Record<EstimateAction, UsageEstimateProcess[]>;
|
|
11
|
+
export declare const estimateUsage: (script: MulmoScript, options?: EstimateUsageOptions) => UsageEstimate[];
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Pre-run usage estimator: walks a MulmoScript and returns, per beat and per
|
|
2
|
+
// process, the API usage a full generation run would consume. Pure and
|
|
3
|
+
// browser-compatible (js-tiktoken is pure JS). No cache awareness: estimates
|
|
4
|
+
// assume every asset is generated fresh.
|
|
5
|
+
import { Tiktoken } from "js-tiktoken/lite";
|
|
6
|
+
import o200k_base from "js-tiktoken/ranks/o200k_base";
|
|
7
|
+
import { provider2TTSAgent, provider2MovieAgent, gptImageOutputTokens, getModelDuration, getModelPricing, defaultProviders, } from "../types/provider2agent.js";
|
|
8
|
+
import { text2SpeechProviderSchema, text2MovieProviderSchema } from "../types/schema.js";
|
|
9
|
+
import { MulmoPresentationStyleMethods } from "../methods/mulmo_presentation_style.js";
|
|
10
|
+
import { MulmoBeatMethods } from "../methods/mulmo_beat.js";
|
|
11
|
+
import { imagePrompt, htmlImageSystemPrompt, translateSystemPrompt, translatePrompts } from "./prompt.js";
|
|
12
|
+
const MILLION = 1_000_000;
|
|
13
|
+
const LATIN_CHARS_PER_TOKEN = 4;
|
|
14
|
+
const CJK_CHARS_PER_SEC = 6;
|
|
15
|
+
const LATIN_CHARS_PER_SEC = 15;
|
|
16
|
+
// Measured ≈50 audio output tokens per second (docs/api.md § Known gaps, probed in PR #1439).
|
|
17
|
+
const OPENAI_TTS_AUDIO_TOKENS_PER_SEC = 50;
|
|
18
|
+
// Documented: Gemini TTS audio tokens = 25 per second of audio.
|
|
19
|
+
const GEMINI_TTS_AUDIO_TOKENS_PER_SEC = 25;
|
|
20
|
+
// gpt-5 completion_tokens include reasoning tokens; a simple slide measured ≈4.4k (probe 2026-07-03).
|
|
21
|
+
const HTML_OUTPUT_TOKENS_GUESS = 4000;
|
|
22
|
+
const TRANSLATE_OUTPUT_FACTOR = 1.2;
|
|
23
|
+
// The translate action doesn't set a model, so @graphai/openai_agent's default applies.
|
|
24
|
+
const TRANSLATE_DEFAULT_MODEL = "gpt-4o";
|
|
25
|
+
const DEFAULT_MOVIE_DURATION_SEC = 8;
|
|
26
|
+
const GPT_IMAGE_FIXED_TOKEN_MODELS = ["gpt-image-1", "gpt-image-1-mini"];
|
|
27
|
+
// Fixed request framing the API adds on top of the tokenized prompt text. Both values
|
|
28
|
+
// reproduced the billed prompt_tokens exactly, per record, in the 2026-07-03 probes
|
|
29
|
+
// (scripts/probe/probe_estimate_vs_actual.ts): chat completions (system + user) +10,
|
|
30
|
+
// image generation +6.
|
|
31
|
+
const OPENAI_CHAT_INPUT_OVERHEAD_TOKENS = 10;
|
|
32
|
+
const GPT_IMAGE_INPUT_OVERHEAD_TOKENS = 6;
|
|
33
|
+
// What each CLI action actually consumes. translate is part of every generation scope
|
|
34
|
+
// because the CLI runs it implicitly when the target/caption language differs from the
|
|
35
|
+
// script language; the estimator emits zero translate records otherwise.
|
|
36
|
+
const VISUAL_PROCESSES = ["image", "htmlImage", "movie", "soundEffect", "lipSync", "imageReference", "movieReference"];
|
|
37
|
+
export const actionEstimateProcesses = {
|
|
38
|
+
audio: ["tts", "translate"],
|
|
39
|
+
images: [...VISUAL_PROCESSES, "translate"],
|
|
40
|
+
pdf: [...VISUAL_PROCESSES, "translate"],
|
|
41
|
+
movie: ["tts", ...VISUAL_PROCESSES, "translate"],
|
|
42
|
+
translate: ["translate"],
|
|
43
|
+
};
|
|
44
|
+
const isDefined = (value) => value !== undefined;
|
|
45
|
+
const isKeyOf = (obj, key) => key in obj;
|
|
46
|
+
let openAITokenizer;
|
|
47
|
+
const countOpenAITokens = (text) => {
|
|
48
|
+
// Lazy: building the o200k_base BPE table is expensive and most importers never estimate.
|
|
49
|
+
openAITokenizer = openAITokenizer ?? new Tiktoken(o200k_base);
|
|
50
|
+
return openAITokenizer.encode(text).length;
|
|
51
|
+
};
|
|
52
|
+
const cjkPattern = /[⺀-鿿가-豈-ヲ-゚]/g;
|
|
53
|
+
const countCjkChars = (text) => (text.match(cjkPattern) ?? []).length;
|
|
54
|
+
const countHeuristicTokens = (text) => {
|
|
55
|
+
const cjkChars = countCjkChars(text);
|
|
56
|
+
return cjkChars + Math.ceil((text.length - cjkChars) / LATIN_CHARS_PER_TOKEN);
|
|
57
|
+
};
|
|
58
|
+
const estimateSpeechSec = (text) => {
|
|
59
|
+
const cjkChars = countCjkChars(text);
|
|
60
|
+
const latinChars = text.length - cjkChars;
|
|
61
|
+
return Math.max(1, Math.round(cjkChars / CJK_CHARS_PER_SEC + latinChars / LATIN_CHARS_PER_SEC));
|
|
62
|
+
};
|
|
63
|
+
const exact = (value) => ({ value, precision: "exact" });
|
|
64
|
+
const estimated = (value) => ({ value, precision: "estimated" });
|
|
65
|
+
const metric = (value, isExact) => ({ value, precision: isExact ? "exact" : "estimated" });
|
|
66
|
+
const computeCostUSD = (pricing, record) => {
|
|
67
|
+
const tokenValue = (m, rate) => (m && rate ? (m.value * rate) / MILLION : 0);
|
|
68
|
+
const total = tokenValue(record.inputTokens, pricing.inputPerMTokensUSD) +
|
|
69
|
+
tokenValue(record.outputTokens, pricing.outputPerMTokensUSD) +
|
|
70
|
+
tokenValue(record.inputChars, pricing.perMCharsUSD) +
|
|
71
|
+
(record.predictSec && pricing.perSecUSD ? record.predictSec.value * pricing.perSecUSD : 0) +
|
|
72
|
+
(record.imageCount && pricing.perImageUSD ? record.imageCount.value * pricing.perImageUSD : 0);
|
|
73
|
+
return total > 0 ? total : undefined;
|
|
74
|
+
};
|
|
75
|
+
const attachCost = (record, pricingKey) => {
|
|
76
|
+
const pricing = getModelPricing(record.provider, pricingKey ?? record.model);
|
|
77
|
+
if (!pricing) {
|
|
78
|
+
return record;
|
|
79
|
+
}
|
|
80
|
+
const costUSD = computeCostUSD(pricing, record);
|
|
81
|
+
return costUSD !== undefined ? { ...record, costUSD, pricingAsOf: pricing.asOf } : record;
|
|
82
|
+
};
|
|
83
|
+
const buildOpenAITts = ({ speaker, text, textIsFinal, beatIndex, lang }) => {
|
|
84
|
+
const model = speaker.model ?? provider2TTSAgent.openai.defaultModel;
|
|
85
|
+
// gpt-* TTS models are token-billed; legacy tts-1 models are character-billed.
|
|
86
|
+
const tokenMetrics = model.startsWith("gpt-")
|
|
87
|
+
? {
|
|
88
|
+
inputTokens: metric(countOpenAITokens(text), textIsFinal),
|
|
89
|
+
outputTokens: estimated(estimateSpeechSec(text) * OPENAI_TTS_AUDIO_TOKENS_PER_SEC),
|
|
90
|
+
}
|
|
91
|
+
: {};
|
|
92
|
+
return { process: "tts", beatIndex, lang, provider: "openai", model, inputChars: metric(text.length, textIsFinal), ...tokenMetrics };
|
|
93
|
+
};
|
|
94
|
+
const buildGeminiTts = ({ speaker, speechOptions, text, beatIndex, lang }) => {
|
|
95
|
+
// The gemini TTS agent wraps the transcript in a "Director's Notes" prompt when an instruction is set.
|
|
96
|
+
const prompt = speechOptions?.instruction ? `### DIRECTOR'S NOTES\n${speechOptions.instruction}\n\n#### TRANSCRIPT\n${text}` : text;
|
|
97
|
+
return {
|
|
98
|
+
process: "tts",
|
|
99
|
+
beatIndex,
|
|
100
|
+
lang,
|
|
101
|
+
provider: "gemini",
|
|
102
|
+
model: speaker.model ?? provider2TTSAgent.gemini.defaultModel,
|
|
103
|
+
inputTokens: estimated(countHeuristicTokens(prompt)),
|
|
104
|
+
outputTokens: estimated(estimateSpeechSec(text) * GEMINI_TTS_AUDIO_TOKENS_PER_SEC),
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
const buildGoogleTts = ({ speaker, text, textIsFinal, beatIndex, lang }) => {
|
|
108
|
+
// Mirrors the runtime usage record: model falls back to the voice name; billing is per character.
|
|
109
|
+
const model = speaker.model ?? speaker.voiceId;
|
|
110
|
+
return { process: "tts", beatIndex, lang, provider: "google", model, inputChars: metric(text.length, textIsFinal) };
|
|
111
|
+
};
|
|
112
|
+
const buildElevenLabsTts = ({ speaker, text, textIsFinal, beatIndex, lang }) => {
|
|
113
|
+
const model = speaker.model ?? provider2TTSAgent.elevenlabs.defaultModel;
|
|
114
|
+
return { process: "tts", beatIndex, lang, provider: "elevenlabs", model, inputChars: metric(text.length, textIsFinal) };
|
|
115
|
+
};
|
|
116
|
+
const buildKotodamaTts = ({ speaker, text, textIsFinal, beatIndex, lang }) => {
|
|
117
|
+
// Mirrors the runtime usage record: the kotodama agent reports the speaker id as the model.
|
|
118
|
+
return { process: "tts", beatIndex, lang, provider: "kotodama", model: speaker.voiceId, inputChars: metric(text.length, textIsFinal) };
|
|
119
|
+
};
|
|
120
|
+
const ttsRecordBuilders = {
|
|
121
|
+
openai: buildOpenAITts,
|
|
122
|
+
gemini: buildGeminiTts,
|
|
123
|
+
google: buildGoogleTts,
|
|
124
|
+
elevenlabs: buildElevenLabsTts,
|
|
125
|
+
kotodama: buildKotodamaTts,
|
|
126
|
+
};
|
|
127
|
+
const googleTtsPricingKey = (voiceId) => {
|
|
128
|
+
const tiers = {
|
|
129
|
+
neural2: "tts-neural2",
|
|
130
|
+
wavenet: "tts-wavenet",
|
|
131
|
+
studio: "tts-studio",
|
|
132
|
+
chirp: "tts-chirp3-hd",
|
|
133
|
+
standard: "tts-standard",
|
|
134
|
+
};
|
|
135
|
+
const lower = voiceId.toLowerCase();
|
|
136
|
+
const tier = Object.keys(tiers).find((key) => lower.includes(key));
|
|
137
|
+
return tier ? tiers[tier] : undefined;
|
|
138
|
+
};
|
|
139
|
+
const estimateBeatTts = (script, style, beat, beatIndex, lang) => {
|
|
140
|
+
if (beat.audio || !beat.text || script.audioParams?.suppressSpeech || !style.speechParams?.speakers) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const speaker = MulmoPresentationStyleMethods.getSpeakerData(style, beat, lang);
|
|
144
|
+
const provider = text2SpeechProviderSchema.parse(speaker.provider);
|
|
145
|
+
const builder = ttsRecordBuilders[provider];
|
|
146
|
+
if (!builder) {
|
|
147
|
+
return undefined; // mock
|
|
148
|
+
}
|
|
149
|
+
// For non-default languages the TTS input is a translation that doesn't exist yet.
|
|
150
|
+
const textIsFinal = lang === (script.lang ?? "en");
|
|
151
|
+
const speechOptions = { ...speaker.speechOptions, ...beat.speechOptions };
|
|
152
|
+
const record = builder({ speaker, speechOptions, text: beat.text, textIsFinal, beatIndex, lang });
|
|
153
|
+
return attachCost(record, provider === "google" ? googleTtsPricingKey(speaker.voiceId) : undefined);
|
|
154
|
+
};
|
|
155
|
+
// ---- Images / movies / sound effects / lip sync ----
|
|
156
|
+
const gptImageSize = (canvasSize) => {
|
|
157
|
+
if (canvasSize.width > canvasSize.height) {
|
|
158
|
+
return "1536x1024";
|
|
159
|
+
}
|
|
160
|
+
return canvasSize.width < canvasSize.height ? "1024x1536" : "1024x1024";
|
|
161
|
+
};
|
|
162
|
+
const gptImageOutputMetric = (model, canvasSize, quality) => {
|
|
163
|
+
const table = gptImageOutputTokens[gptImageSize(canvasSize)];
|
|
164
|
+
const knownQuality = quality === "low" || quality === "medium" || quality === "high" ? quality : undefined;
|
|
165
|
+
// Unspecified quality means API "auto"; assume "high" as the conservative upper bound.
|
|
166
|
+
const tokens = table[knownQuality ?? "high"];
|
|
167
|
+
return metric(tokens, GPT_IMAGE_FIXED_TOKEN_MODELS.includes(model) && knownQuality !== undefined);
|
|
168
|
+
};
|
|
169
|
+
const buildImageRecord = ({ process, provider, model, prompt, canvasSize, quality, beatIndex, refKey }) => {
|
|
170
|
+
const base = { process, beatIndex, refKey, provider, model };
|
|
171
|
+
if (provider === "openai") {
|
|
172
|
+
return {
|
|
173
|
+
...base,
|
|
174
|
+
inputTokens: exact(countOpenAITokens(prompt) + GPT_IMAGE_INPUT_OVERHEAD_TOKENS),
|
|
175
|
+
outputTokens: gptImageOutputMetric(model, canvasSize, quality),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (provider === "google") {
|
|
179
|
+
return { ...base, inputTokens: estimated(countHeuristicTokens(prompt)), imageCount: exact(1) };
|
|
180
|
+
}
|
|
181
|
+
return { ...base, imageCount: exact(1) }; // replicate bills per image
|
|
182
|
+
};
|
|
183
|
+
const isMovieBeat = (beat) => Boolean(beat.moviePrompt || beat.image?.type === "movie");
|
|
184
|
+
// Mirrors imagePreprocessAgent: no AI image when a plugin renders the beat or the beat is movie-only.
|
|
185
|
+
const needsImageGeneration = (beat) => !beat.image && !(beat.moviePrompt && !beat.imagePrompt);
|
|
186
|
+
const estimateImageGeneration = (style, beat, beatIndex) => {
|
|
187
|
+
const info = MulmoPresentationStyleMethods.getImageAgentInfo(style, beat);
|
|
188
|
+
const { provider, model, quality } = info.imageParams;
|
|
189
|
+
if (!provider || provider === "mock" || !model) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const prompt = imagePrompt(beat, info.imageParams.style);
|
|
193
|
+
const canvasSize = MulmoPresentationStyleMethods.getCanvasSize(style);
|
|
194
|
+
return attachCost(buildImageRecord({ process: "image", provider, model, prompt, canvasSize, quality, beatIndex }));
|
|
195
|
+
};
|
|
196
|
+
const estimateHtmlImage = (style, beat, beatIndex) => {
|
|
197
|
+
const info = MulmoPresentationStyleMethods.getHtmlImageAgentInfo(style);
|
|
198
|
+
if (info.provider === "mock") {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const fullPrompt = htmlImageSystemPrompt(MulmoPresentationStyleMethods.getCanvasSize(style)) + "\n" + (MulmoBeatMethods.getHtmlPrompt(beat) ?? "");
|
|
202
|
+
const inputTokens = info.provider === "openai"
|
|
203
|
+
? exact(countOpenAITokens(fullPrompt) + OPENAI_CHAT_INPUT_OVERHEAD_TOKENS)
|
|
204
|
+
: estimated(countHeuristicTokens(fullPrompt) + OPENAI_CHAT_INPUT_OVERHEAD_TOKENS);
|
|
205
|
+
return attachCost({
|
|
206
|
+
process: "htmlImage",
|
|
207
|
+
beatIndex,
|
|
208
|
+
provider: info.provider,
|
|
209
|
+
model: info.model,
|
|
210
|
+
inputTokens,
|
|
211
|
+
outputTokens: estimated(HTML_OUTPUT_TOKENS_GUESS),
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
const beatDurationMetric = (beat) => {
|
|
215
|
+
if (beat.duration !== undefined) {
|
|
216
|
+
return exact(beat.duration);
|
|
217
|
+
}
|
|
218
|
+
return estimated(beat.text ? estimateSpeechSec(beat.text) : DEFAULT_MOVIE_DURATION_SEC);
|
|
219
|
+
};
|
|
220
|
+
const snapMovieDuration = (provider, model, duration) => {
|
|
221
|
+
if (!isKeyOf(provider2MovieAgent[provider].modelParams, model)) {
|
|
222
|
+
return duration;
|
|
223
|
+
}
|
|
224
|
+
const snapped = getModelDuration(provider, model, duration.value);
|
|
225
|
+
return snapped !== undefined ? metric(snapped, duration.precision === "exact") : duration;
|
|
226
|
+
};
|
|
227
|
+
const estimateMovieGeneration = (style, beat, beatIndex, refKey) => {
|
|
228
|
+
const info = MulmoPresentationStyleMethods.getMovieAgentInfo(style, beat);
|
|
229
|
+
const provider = text2MovieProviderSchema.parse(info.movieParams?.provider ?? defaultProviders.text2movie);
|
|
230
|
+
if (!isKeyOf(provider2MovieAgent, provider) || provider === "mock") {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const model = info.movieParams?.model ?? provider2MovieAgent[provider].defaultModel;
|
|
234
|
+
const duration = beat ? beatDurationMetric(beat) : estimated(DEFAULT_MOVIE_DURATION_SEC);
|
|
235
|
+
const predictSec = snapMovieDuration(provider, model, duration);
|
|
236
|
+
return attachCost({ process: refKey === undefined ? "movie" : "movieReference", beatIndex, refKey, provider, model, predictSec });
|
|
237
|
+
};
|
|
238
|
+
const estimateSoundEffect = (style, beat, beatIndex) => {
|
|
239
|
+
const info = MulmoPresentationStyleMethods.getSoundEffectAgentInfo(style, beat);
|
|
240
|
+
const model = beat.soundEffectParams?.model ?? style.soundEffectParams?.model ?? info.defaultModel;
|
|
241
|
+
const provider = beat.soundEffectParams?.provider ?? style.soundEffectParams?.provider ?? defaultProviders.soundEffect;
|
|
242
|
+
// Replicate bills sound effects by GPU prediction time, which differs from clip duration.
|
|
243
|
+
const predictSec = estimated(beatDurationMetric(beat).value);
|
|
244
|
+
return attachCost({ process: "soundEffect", beatIndex, provider, model, predictSec });
|
|
245
|
+
};
|
|
246
|
+
const estimateLipSync = (style, beat, beatIndex) => {
|
|
247
|
+
const info = MulmoPresentationStyleMethods.getLipSyncAgentInfo(style, beat);
|
|
248
|
+
const model = beat.lipSyncParams?.model ?? style.lipSyncParams?.model ?? info.defaultModel;
|
|
249
|
+
const provider = beat.lipSyncParams?.provider ?? style.lipSyncParams?.provider ?? defaultProviders.lipSync;
|
|
250
|
+
return attachCost({ process: "lipSync", beatIndex, provider, model, predictSec: beatDurationMetric(beat) });
|
|
251
|
+
};
|
|
252
|
+
const estimateBeatVisuals = (style, beat, beatIndex) => {
|
|
253
|
+
if (beat.htmlPrompt) {
|
|
254
|
+
// The image preprocessor returns early for htmlPrompt beats; no other generation runs.
|
|
255
|
+
return [estimateHtmlImage(style, beat, beatIndex)].filter(isDefined);
|
|
256
|
+
}
|
|
257
|
+
return [
|
|
258
|
+
needsImageGeneration(beat) ? estimateImageGeneration(style, beat, beatIndex) : undefined,
|
|
259
|
+
beat.moviePrompt ? estimateMovieGeneration(style, beat, beatIndex) : undefined,
|
|
260
|
+
beat.soundEffectPrompt && isMovieBeat(beat) ? estimateSoundEffect(style, beat, beatIndex) : undefined,
|
|
261
|
+
beat.enableLipSync ? estimateLipSync(style, beat, beatIndex) : undefined,
|
|
262
|
+
].filter(isDefined);
|
|
263
|
+
};
|
|
264
|
+
// ---- Reference images / movies (imageParams.images and beat.images) ----
|
|
265
|
+
const estimateReferenceImage = (style, prompt, canvasSize, refKey, beatIndex) => {
|
|
266
|
+
const info = MulmoPresentationStyleMethods.getImageAgentInfo(style);
|
|
267
|
+
const { provider, model, quality } = info.imageParams;
|
|
268
|
+
if (!provider || provider === "mock" || !model) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
// Mirrors generateReferenceImage: the style is appended to the reference prompt.
|
|
272
|
+
const fullPrompt = `${prompt}\n${info.imageParams.style || ""}`;
|
|
273
|
+
const size = canvasSize ?? MulmoPresentationStyleMethods.getCanvasSize(style);
|
|
274
|
+
return attachCost(buildImageRecord({ process: "imageReference", provider, model, prompt: fullPrompt, canvasSize: size, quality, refKey, beatIndex }));
|
|
275
|
+
};
|
|
276
|
+
const estimateMediaReferences = (style, images, beatIndex) => {
|
|
277
|
+
return Object.entries(images ?? {})
|
|
278
|
+
.map(([refKey, media]) => {
|
|
279
|
+
if (media.type === "imagePrompt") {
|
|
280
|
+
return estimateReferenceImage(style, media.prompt, media.canvasSize, refKey, beatIndex);
|
|
281
|
+
}
|
|
282
|
+
if (media.type === "moviePrompt") {
|
|
283
|
+
return estimateMovieGeneration(style, undefined, beatIndex, refKey);
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
})
|
|
287
|
+
.filter(isDefined);
|
|
288
|
+
};
|
|
289
|
+
// ---- Translate ----
|
|
290
|
+
const buildTranslateInput = (defaultLang, text, targetLang) => {
|
|
291
|
+
const substitutions = { ":lang": defaultLang, ":beat.text": text, ":targetLang": targetLang };
|
|
292
|
+
const prompt = translatePrompts.map((line) => substitutions[line] ?? line).join("\n");
|
|
293
|
+
return translateSystemPrompt + "\n" + prompt;
|
|
294
|
+
};
|
|
295
|
+
const buildTranslateRecord = (defaultLang, text, targetLang, beatIndex) => {
|
|
296
|
+
const input = buildTranslateInput(defaultLang, text, targetLang);
|
|
297
|
+
return attachCost({
|
|
298
|
+
process: "translate",
|
|
299
|
+
beatIndex,
|
|
300
|
+
lang: targetLang,
|
|
301
|
+
provider: "openai",
|
|
302
|
+
model: TRANSLATE_DEFAULT_MODEL,
|
|
303
|
+
inputTokens: exact(countOpenAITokens(input) + OPENAI_CHAT_INPUT_OVERHEAD_TOKENS),
|
|
304
|
+
outputTokens: estimated(Math.ceil(countOpenAITokens(text) * TRANSLATE_OUTPUT_FACTOR)),
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
const estimateTranslate = (script, targetLangs) => {
|
|
308
|
+
const defaultLang = script.lang ?? "en";
|
|
309
|
+
return targetLangs
|
|
310
|
+
.filter((targetLang) => targetLang !== defaultLang)
|
|
311
|
+
.flatMap((targetLang) => script.beats.map((beat, beatIndex) => (beat.text ? buildTranslateRecord(defaultLang, beat.text, targetLang, beatIndex) : undefined)).filter(isDefined));
|
|
312
|
+
};
|
|
313
|
+
// ---- Entry point ----
|
|
314
|
+
const defaultTargetLangs = (style, langs, defaultLang) => {
|
|
315
|
+
// Mirrors the translate action's default: the requested languages plus the caption language.
|
|
316
|
+
const candidates = [...langs, style.captionParams?.lang];
|
|
317
|
+
return [...new Set(candidates.filter((lang) => !!lang && lang !== defaultLang))];
|
|
318
|
+
};
|
|
319
|
+
export const estimateUsage = (script, options) => {
|
|
320
|
+
const style = options?.presentationStyle ?? script;
|
|
321
|
+
const defaultLang = script.lang ?? "en";
|
|
322
|
+
const langs = options?.langs ?? [defaultLang];
|
|
323
|
+
const targetLangs = options?.targetLangs ?? defaultTargetLangs(style, langs, defaultLang);
|
|
324
|
+
const beatRecords = script.beats.flatMap((beat, beatIndex) => [
|
|
325
|
+
...langs.map((lang) => estimateBeatTts(script, style, beat, beatIndex, lang)).filter(isDefined),
|
|
326
|
+
...estimateBeatVisuals(style, beat, beatIndex),
|
|
327
|
+
...estimateMediaReferences(style, beat.images, beatIndex),
|
|
328
|
+
]);
|
|
329
|
+
const records = [...estimateMediaReferences(style, style.imageParams?.images), ...beatRecords, ...estimateTranslate(script, targetLangs)];
|
|
330
|
+
const processes = options?.processes;
|
|
331
|
+
return processes ? records.filter((record) => processes.includes(record.process)) : records;
|
|
332
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const METRIC_KEYS = ["inputTokens", "outputTokens", "inputChars", "predictSec", "imageCount"];
|
|
2
|
+
const emptyGroup = (process, model) => ({
|
|
3
|
+
process,
|
|
4
|
+
model,
|
|
5
|
+
metrics: Object.fromEntries(METRIC_KEYS.map((key) => [key, { value: 0, estimated: false, present: false }])),
|
|
6
|
+
costUSD: 0,
|
|
7
|
+
unpricedRecords: 0,
|
|
8
|
+
records: 0,
|
|
9
|
+
});
|
|
10
|
+
const addMetric = (sum, metric) => {
|
|
11
|
+
if (!metric) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
sum.value += metric.value;
|
|
15
|
+
sum.estimated = sum.estimated || metric.precision === "estimated";
|
|
16
|
+
sum.present = true;
|
|
17
|
+
};
|
|
18
|
+
const buildGroups = (records) => {
|
|
19
|
+
const groups = new Map();
|
|
20
|
+
records.forEach((record) => {
|
|
21
|
+
const key = `${record.process}|${record.provider}:${record.model}`;
|
|
22
|
+
const group = groups.get(key) ?? emptyGroup(record.process, `${record.provider}:${record.model}`);
|
|
23
|
+
METRIC_KEYS.forEach((metricKey) => addMetric(group.metrics[metricKey], record[metricKey]));
|
|
24
|
+
group.costUSD += record.costUSD ?? 0;
|
|
25
|
+
group.unpricedRecords += record.costUSD === undefined ? 1 : 0;
|
|
26
|
+
group.records += 1;
|
|
27
|
+
groups.set(key, group);
|
|
28
|
+
});
|
|
29
|
+
return [...groups.values()];
|
|
30
|
+
};
|
|
31
|
+
const metricCell = (sum) => {
|
|
32
|
+
if (!sum.present) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
const marker = sum.estimated ? "~" : "";
|
|
36
|
+
return `${marker}${Math.round(sum.value * 100) / 100}`;
|
|
37
|
+
};
|
|
38
|
+
const groupRow = (group) => {
|
|
39
|
+
const cells = [group.process, group.model, ...METRIC_KEYS.map((key) => metricCell(group.metrics[key])), group.costUSD > 0 ? group.costUSD.toFixed(4) : ""];
|
|
40
|
+
return `| ${cells.join(" | ")} |`;
|
|
41
|
+
};
|
|
42
|
+
const totalLine = (groups, records) => {
|
|
43
|
+
const totalCost = groups.reduce((sum, group) => sum + group.costUSD, 0);
|
|
44
|
+
const unpriced = groups.reduce((sum, group) => sum + group.unpricedRecords, 0);
|
|
45
|
+
const asOf = [...new Set(records.map((record) => record.pricingAsOf).filter((date) => !!date))].sort()[0];
|
|
46
|
+
const parts = [`Total estimated cost: ≈ $${totalCost.toFixed(4)}`];
|
|
47
|
+
if (unpriced > 0) {
|
|
48
|
+
parts.push(`(${unpriced} record(s) without pricing data)`);
|
|
49
|
+
}
|
|
50
|
+
if (asOf) {
|
|
51
|
+
parts.push(`(prices as of ${asOf})`);
|
|
52
|
+
}
|
|
53
|
+
return parts.join(" ");
|
|
54
|
+
};
|
|
55
|
+
export const formatUsageEstimates = (records) => {
|
|
56
|
+
if (records.length === 0) {
|
|
57
|
+
return "No billable API usage estimated.";
|
|
58
|
+
}
|
|
59
|
+
const groups = buildGroups(records);
|
|
60
|
+
const header = "| process | provider:model | input tokens | output tokens | input chars | predict sec | images | cost (USD) |";
|
|
61
|
+
const separator = "|---|---|---|---|---|---|---|---|";
|
|
62
|
+
const note = "~ marks heuristic estimates; unmarked values are exact for the given script.";
|
|
63
|
+
return [header, separator, ...groups.map(groupRow), "", totalLine(groups, records), note].join("\n");
|
|
64
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mulmocast",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.node.js",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"resolutions": {
|
|
27
27
|
"minimatch": "^10.2.5",
|
|
28
|
-
"tar": "7.5.
|
|
29
|
-
"yauzl": "^3.
|
|
28
|
+
"tar": "7.5.19",
|
|
29
|
+
"yauzl": "^3.4.0"
|
|
30
30
|
},
|
|
31
31
|
"bin": {
|
|
32
32
|
"mulmo": "lib/cli/bin.js",
|
|
@@ -89,9 +89,9 @@
|
|
|
89
89
|
"homepage": "https://github.com/receptron/mulmocast-cli#readme",
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@google-cloud/text-to-speech": "^6.4.1",
|
|
92
|
-
"@google/genai": "^2.
|
|
92
|
+
"@google/genai": "^2.10.0",
|
|
93
93
|
"@graphai/anthropic_agent": "^2.0.12",
|
|
94
|
-
"@graphai/browserless_agent": "^2.0.
|
|
94
|
+
"@graphai/browserless_agent": "^2.0.3",
|
|
95
95
|
"@graphai/gemini_agent": "^2.0.5",
|
|
96
96
|
"@graphai/groq_agent": "^2.0.2",
|
|
97
97
|
"@graphai/input_agents": "^1.0.2",
|
|
@@ -99,22 +99,23 @@
|
|
|
99
99
|
"@graphai/stream_agent_filter": "^2.0.3",
|
|
100
100
|
"@graphai/vanilla": "^2.0.12",
|
|
101
101
|
"@graphai/vanilla_node_agents": "^2.0.4",
|
|
102
|
-
"@inquirer/input": "^5.
|
|
103
|
-
"@inquirer/select": "^5.1
|
|
102
|
+
"@inquirer/input": "^5.1.2",
|
|
103
|
+
"@inquirer/select": "^5.2.1",
|
|
104
104
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
105
105
|
"@modernized/fluent-ffmpeg": "^1.0.1",
|
|
106
106
|
"@mozilla/readability": "^0.6.0",
|
|
107
107
|
"@mulmocast/deck": "^1.1.0",
|
|
108
|
-
"@tavily/core": "^0.7.
|
|
108
|
+
"@tavily/core": "^0.7.6",
|
|
109
109
|
"archiver": "^7.0.1",
|
|
110
110
|
"clipboardy": "^5.3.1",
|
|
111
111
|
"dotenv": "^17.4.2",
|
|
112
112
|
"graphai": "^2.0.18",
|
|
113
|
+
"js-tiktoken": "^1.0.21",
|
|
113
114
|
"jsdom": "^29.1.1",
|
|
114
115
|
"marked": "^18.0.5",
|
|
115
|
-
"mulmocast-vision": "^1.0.
|
|
116
|
-
"ora": "^9.4.
|
|
117
|
-
"puppeteer": "^25.
|
|
116
|
+
"mulmocast-vision": "^1.0.11",
|
|
117
|
+
"ora": "^9.4.1",
|
|
118
|
+
"puppeteer": "^25.3.0",
|
|
118
119
|
"replicate": "^1.4.0",
|
|
119
120
|
"yaml": "^2.9.0",
|
|
120
121
|
"yargs": "^18.0.0",
|
|
@@ -127,16 +128,16 @@
|
|
|
127
128
|
"@types/jsdom": "^28.0.3",
|
|
128
129
|
"@types/yargs": "^17.0.35",
|
|
129
130
|
"cross-env": "^10.1.0",
|
|
130
|
-
"eslint": "^10.
|
|
131
|
+
"eslint": "^10.6.0",
|
|
131
132
|
"eslint-config-prettier": "^10.1.8",
|
|
132
133
|
"eslint-plugin-import": "^2.32.0",
|
|
133
134
|
"eslint-plugin-prettier": "^5.5.6",
|
|
134
|
-
"eslint-plugin-sonarjs": "^4.0
|
|
135
|
-
"globals": "^17.
|
|
136
|
-
"prettier": "^3.
|
|
135
|
+
"eslint-plugin-sonarjs": "^4.1.0",
|
|
136
|
+
"globals": "^17.7.0",
|
|
137
|
+
"prettier": "^3.9.4",
|
|
137
138
|
"tsx": "^4.22.4",
|
|
138
139
|
"typescript": "6.0.3",
|
|
139
|
-
"typescript-eslint": "^8.
|
|
140
|
+
"typescript-eslint": "^8.62.1"
|
|
140
141
|
},
|
|
141
142
|
"engines": {
|
|
142
143
|
"node": ">=22.0.0"
|