mulmocast 1.2.43 → 1.2.44

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.
@@ -1,6 +1,7 @@
1
1
  import type { GraphData } from "graphai";
2
2
  import { MulmoStudioContext, MulmoBeat, PublicAPIArgs } from "../types/index.js";
3
3
  export declare const getBeatAudioPath: (text: string, context: MulmoStudioContext, beat: MulmoBeat, lang?: string) => string | undefined;
4
+ export declare const getBeatAudioPathOrUrl: (text: string, context: MulmoStudioContext, beat: MulmoBeat, lang?: string) => string | undefined;
4
5
  export declare const listLocalizedAudioPaths: (context: MulmoStudioContext) => (string | undefined)[];
5
6
  export declare const audio_graph_data: GraphData;
6
7
  export declare const generateBeatAudio: (index: number, context: MulmoStudioContext, args?: PublicAPIArgs & {
@@ -9,38 +9,43 @@ import { getAudioArtifactFilePath, getAudioFilePath, getOutputStudioFilePath, re
9
9
  import { localizedText, settings2GraphAIConfig } from "../utils/utils.js";
10
10
  import { text2hash } from "../utils/utils_node.js";
11
11
  import { provider2TTSAgent } from "../utils/provider2agent.js";
12
+ import { invalidAudioSourceError } from "../utils/error_cause.js";
12
13
  import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
13
14
  import { MulmoMediaSourceMethods } from "../methods/mulmo_media_source.js";
14
15
  dotenv.config({ quiet: true });
15
16
  const vanillaAgents = agents.default ?? agents;
16
- const getAudioPath = (context, beat, audioFile) => {
17
+ const getAudioPathOrUrl = (context, beat, maybeAudioFile) => {
17
18
  if (beat.audio?.type === "audio") {
18
- const path = MulmoMediaSourceMethods.resolve(beat.audio.source, context);
19
- if (path) {
20
- return path;
19
+ const pathOrUrl = MulmoMediaSourceMethods.resolve(beat.audio.source, context);
20
+ if (pathOrUrl) {
21
+ return pathOrUrl;
21
22
  }
22
- throw new Error("Invalid audio source");
23
+ throw new Error("Invalid audio source", { cause: invalidAudioSourceError(context.studio.script.beats.indexOf(beat)) });
23
24
  }
24
25
  if (beat.text === undefined || beat.text === "" || context.studio.script.audioParams.suppressSpeech) {
25
26
  return undefined; // It indicates that the audio is not needed.
26
27
  }
27
- return audioFile;
28
+ return maybeAudioFile;
28
29
  };
30
+ // for back forward compatible
29
31
  export const getBeatAudioPath = (text, context, beat, lang) => {
32
+ return getBeatAudioPathOrUrl(text, context, beat, lang);
33
+ };
34
+ export const getBeatAudioPathOrUrl = (text, context, beat, lang) => {
30
35
  const audioDirPath = MulmoStudioContextMethods.getAudioDirPath(context);
31
36
  const { voiceId, provider, speechOptions, model } = MulmoStudioContextMethods.getAudioParam(context, beat, lang);
32
37
  const hash_string = [text, voiceId, speechOptions?.instruction ?? "", speechOptions?.speed ?? 1.0, provider, model ?? ""].join(":");
33
- GraphAILogger.log(`getBeatAudioPath [${hash_string}]`);
38
+ GraphAILogger.log(`getBeatAudioPathOrUrl [${hash_string}]`);
34
39
  const audioFileName = `${context.studio.filename}_${text2hash(hash_string)}`;
35
- const audioFile = getAudioFilePath(audioDirPath, context.studio.filename, audioFileName, lang);
36
- return getAudioPath(context, beat, audioFile);
40
+ const maybeAudioFile = getAudioFilePath(audioDirPath, context.studio.filename, audioFileName, lang);
41
+ return getAudioPathOrUrl(context, beat, maybeAudioFile);
37
42
  };
38
43
  export const listLocalizedAudioPaths = (context) => {
39
44
  const lang = context.lang ?? context.studio.script.lang;
40
45
  return context.studio.script.beats.map((beat, index) => {
41
46
  const multiLingual = context.multiLingual[index];
42
47
  const text = localizedText(beat, multiLingual, lang);
43
- return getBeatAudioPath(text, context, beat, lang);
48
+ return getBeatAudioPathOrUrl(text, context, beat, lang);
44
49
  });
45
50
  };
46
51
  const preprocessorAgent = (namedInputs) => {
@@ -48,7 +53,7 @@ const preprocessorAgent = (namedInputs) => {
48
53
  // const { lang } = context;
49
54
  const text = localizedText(beat, multiLingual, lang);
50
55
  const { voiceId, provider, speechOptions, model } = MulmoStudioContextMethods.getAudioParam(context, beat, lang);
51
- const audioPath = getBeatAudioPath(text, context, beat, lang);
56
+ const audioPath = getBeatAudioPathOrUrl(text, context, beat, lang);
52
57
  studioBeat.audioFile = audioPath; // TODO: Passing by reference is difficult to maintain, so pass it using graphai inputs
53
58
  const needsTTS = !beat.audio && audioPath !== undefined;
54
59
  return {
@@ -1,9 +1,7 @@
1
- import fs from "fs";
2
1
  import { GraphAI, GraphAILogger } from "graphai";
3
- import { getReferenceImagePath, resolveAssetPath } from "../utils/file.js";
4
- import { getExtention } from "../utils/utils.js";
2
+ import { getReferenceImagePath } from "../utils/file.js";
5
3
  import { graphOption } from "./images.js";
6
- import { MulmoPresentationStyleMethods } from "../methods/index.js";
4
+ import { MulmoPresentationStyleMethods, MulmoMediaSourceMethods } from "../methods/index.js";
7
5
  import { imageOpenaiAgent, mediaMockAgent, imageGenAIAgent, imageReplicateAgent } from "../agents/index.js";
8
6
  // public api
9
7
  // Application may call this function directly to generate reference image.
@@ -43,18 +41,6 @@ export const generateReferenceImage = async (inputs) => {
43
41
  await graph.run();
44
42
  return imagePath;
45
43
  };
46
- const downLoadImage = async (context, key, url) => {
47
- const response = await fetch(url);
48
- if (!response.ok) {
49
- throw new Error(`Failed to download image: ${url}`);
50
- }
51
- const buffer = Buffer.from(await response.arrayBuffer());
52
- // Detect file extension from Content-Type header or URL
53
- const extension = getExtention(response.headers.get("content-type"), url);
54
- const imagePath = getReferenceImagePath(context, key, extension);
55
- await fs.promises.writeFile(imagePath, buffer);
56
- return imagePath;
57
- };
58
44
  export const getImageRefs = async (context) => {
59
45
  const images = context.presentationStyle.imageParams?.images;
60
46
  if (!images) {
@@ -69,12 +55,7 @@ export const getImageRefs = async (context) => {
69
55
  imageRefs[key] = await generateReferenceImage({ context, key, index, image, force: false });
70
56
  }
71
57
  else if (image.type === "image") {
72
- if (image.source.kind === "path") {
73
- imageRefs[key] = resolveAssetPath(context, image.source.path);
74
- }
75
- else if (image.source.kind === "url") {
76
- imageRefs[key] = await downLoadImage(context, key, image.source.url);
77
- }
58
+ imageRefs[key] = await MulmoMediaSourceMethods.imageReference(image.source, context, key);
78
59
  }
79
60
  }));
80
61
  return imageRefs;
@@ -10,6 +10,7 @@ import { MulmoPresentationStyleMethods, MulmoStudioContextMethods } from "../met
10
10
  import { getOutputStudioFilePath, mkdir } from "../utils/file.js";
11
11
  import { fileCacheAgentFilter } from "../utils/filters.js";
12
12
  import { settings2GraphAIConfig } from "../utils/utils.js";
13
+ import { audioCheckerError } from "../utils/error_cause.js";
13
14
  import { extractImageFromMovie, ffmpegGetMediaDuration, trimMusic } from "../utils/ffmpeg_utils.js";
14
15
  import { getImageRefs } from "./image_references.js";
15
16
  import { imagePreprocessAgent, imagePluginAgent, htmlImageGeneratorAgent } from "./image_agents.js";
@@ -201,13 +202,8 @@ export const beat_graph_data = {
201
202
  }
202
203
  catch (error) {
203
204
  GraphAILogger.error(error);
204
- throw new Error("audioChecker: ffmpegGetMediaDuration error.", {
205
- cause: {
206
- type: "FileNotExist",
207
- action: "images",
208
- agentName: "audioChecker",
209
- beat_index: namedInputs.index,
210
- },
205
+ throw new Error(`audioChecker: ffmpegGetMediaDuration error: index=${namedInputs.index} file=${sourceFile}`, {
206
+ cause: audioCheckerError(namedInputs.index, sourceFile),
211
207
  });
212
208
  }
213
209
  },
@@ -2,6 +2,7 @@ import { GraphAILogger, assert } from "graphai";
2
2
  import { mulmoTransitionSchema, mulmoFillOptionSchema } from "../types/index.js";
3
3
  import { MulmoPresentationStyleMethods } from "../methods/index.js";
4
4
  import { getAudioArtifactFilePath, getOutputVideoFilePath, writingMessage, isFile } from "../utils/file.js";
5
+ import { createVideoFileError, createVideoSourceError } from "../utils/error_cause.js";
5
6
  import { FfmpegContextAddInput, FfmpegContextInit, FfmpegContextPushFormattedAudio, FfmpegContextGenerateOutput, } from "../utils/ffmpeg_utils.js";
6
7
  import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
7
8
  // const isMac = process.platform === "darwin";
@@ -163,8 +164,8 @@ const createVideo = async (audioArtifactFilePath, outputVideoPath, context) => {
163
164
  return timestamp; // Skip voice-over beats.
164
165
  }
165
166
  const sourceFile = studioBeat.lipSyncFile ?? studioBeat.soundEffectFile ?? studioBeat.movieFile ?? studioBeat.htmlImageFile ?? studioBeat.imageFile;
166
- assert(!!sourceFile, `studioBeat.imageFile or studioBeat.movieFile is not set: index=${index}`);
167
- assert(isFile(sourceFile), `studioBeat.imageFile or studioBeat.movieFile is not exist or not file: index=${index}`);
167
+ assert(!!sourceFile, `studioBeat.imageFile or studioBeat.movieFile is not set: index=${index}`, false, createVideoSourceError(index));
168
+ assert(isFile(sourceFile), `studioBeat.imageFile or studioBeat.movieFile is not exist or not file: index=${index} file=${sourceFile}`, false, createVideoFileError(index, sourceFile));
168
169
  assert(!!studioBeat.duration, `studioBeat.duration is not set: index=${index}`);
169
170
  const extraPadding = (() => {
170
171
  // We need to consider only intro and outro padding because the other paddings were already added to the beat.duration
@@ -241,7 +241,7 @@ export const translateBeat = async (index, context, targetLangs, args) => {
241
241
  try {
242
242
  const { outputMultilingualFilePath } = getOutputMultilingualFilePathAndMkdir(context);
243
243
  const config = settings2GraphAIConfig(settings, process.env);
244
- assert(!!config?.openAIAgent?.apiKey, "The OPENAI_API_KEY environment variable is missing or empty");
244
+ assert(!!config?.openAIAgent?.apiKey, "The OPENAI_API_KEY environment variable is missing or empty"); // TODO: cause
245
245
  const graph = new GraphAI(beatGraph, { ...vanillaAgents, fileWriteAgent, openAIAgent }, { agentFilters, config });
246
246
  graph.injectValue("context", context);
247
247
  graph.injectValue("targetLangs", targetLangs);
@@ -276,7 +276,7 @@ export const translate = async (context, args) => {
276
276
  ? args?.targetLangs
277
277
  : [...new Set([context.lang, context.studio.script.captionParams?.lang].filter((x) => !isNull(x)))];
278
278
  const config = settings2GraphAIConfig(settings, process.env);
279
- assert(!!config?.openAIAgent?.apiKey, "The OPENAI_API_KEY environment variable is missing or empty");
279
+ assert(!!config?.openAIAgent?.apiKey, "The OPENAI_API_KEY environment variable is missing or empty"); // TODO: cause
280
280
  const graph = new GraphAI(translate_graph_data, { ...vanillaAgents, fileWriteAgent, openAIAgent }, { agentFilters, config });
281
281
  graph.injectValue("context", context);
282
282
  graph.injectValue("targetLangs", targetLangs);
@@ -1,14 +1,14 @@
1
- import fs from "fs";
2
1
  import { GraphAILogger } from "graphai";
3
2
  import { FfmpegContextAddInput, FfmpegContextInit, FfmpegContextGenerateOutput, ffmpegGetMediaDuration } from "../utils/ffmpeg_utils.js";
4
3
  import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
4
+ import { isFile } from "../utils/file.js";
5
5
  const addBGMAgent = async ({ namedInputs, params, }) => {
6
6
  const { voiceFile, outputFile, context } = namedInputs;
7
7
  const { musicFile } = params;
8
- if (!fs.existsSync(voiceFile)) {
8
+ if (!isFile(voiceFile)) {
9
9
  throw new Error(`AddBGMAgent voiceFile not exist: ${voiceFile}`);
10
10
  }
11
- if (!musicFile.match(/^http/) && !fs.existsSync(musicFile)) {
11
+ if (!musicFile.match(/^http/) && !isFile(musicFile)) {
12
12
  throw new Error(`AddBGMAgent musicFile not exist: ${musicFile}`);
13
13
  }
14
14
  const { duration: speechDuration } = await ffmpegGetMediaDuration(voiceFile);
@@ -3,6 +3,7 @@ import { silent60secPath, isFile } from "../utils/file.js";
3
3
  import { FfmpegContextInit, FfmpegContextGenerateOutput, FfmpegContextInputFormattedAudio, ffmpegGetMediaDuration, } from "../utils/ffmpeg_utils.js";
4
4
  import { MulmoMediaSourceMethods } from "../methods/mulmo_media_source.js";
5
5
  import { userAssert } from "../utils/utils.js";
6
+ import { getAudioInputIdsError } from "../utils/error_cause.js";
6
7
  const getMovieDuration = async (context, beat) => {
7
8
  if (beat.image?.type === "movie") {
8
9
  const pathOrUrl = MulmoMediaSourceMethods.resolve(beat.image.source, context);
@@ -72,7 +73,7 @@ const getInputIds = (context, mediaDurations, ffmpegContext, silentIds) => {
72
73
  const paddingId = `[padding_${index}]`;
73
74
  if (studioBeat.audioFile) {
74
75
  if (!/^https?:\/\//.test(studioBeat.audioFile)) {
75
- assert(isFile(studioBeat.audioFile), `studioBeat.audioFile is not exist or not file: index=${index} file=${studioBeat.audioFile}`);
76
+ assert(isFile(studioBeat.audioFile), `studioBeat.audioFile is not exist or not file: index=${index} file=${studioBeat.audioFile}`, false, getAudioInputIdsError(index, studioBeat.audioFile));
76
77
  }
77
78
  const audioId = FfmpegContextInputFormattedAudio(ffmpegContext, studioBeat.audioFile);
78
79
  inputIds.push(audioId);
@@ -9,7 +9,7 @@ export const MulmoBeatMethods = {
9
9
  getPlugin(beat) {
10
10
  const plugin = findImagePlugin(beat?.image?.type);
11
11
  if (!plugin) {
12
- throw new Error(`invalid beat image type: ${beat.image}`);
12
+ throw new Error(`invalid beat image type: ${beat.image}`); // TODO: cause
13
13
  }
14
14
  return plugin;
15
15
  },
@@ -1,5 +1,9 @@
1
- import { MulmoMediaSource, MulmoStudioContext } from "../types/index.js";
1
+ import type { MulmoMediaSource, MulmoMediaMermaidSource, MulmoStudioContext, ImageType } from "../types/index.js";
2
+ export declare const getExtention: (contentType: string | null, url: string) => string;
2
3
  export declare const MulmoMediaSourceMethods: {
3
- getText(mediaSource: MulmoMediaSource, context: MulmoStudioContext): Promise<string | null>;
4
+ getText(mediaSource: MulmoMediaMermaidSource, context: MulmoStudioContext): Promise<string | null>;
4
5
  resolve(mediaSource: MulmoMediaSource | undefined, context: MulmoStudioContext): string | null;
6
+ imageReference(mediaSource: MulmoMediaSource, context: MulmoStudioContext, key: string): Promise<string>;
7
+ imagePluginSource(mediaSource: MulmoMediaSource, context: MulmoStudioContext, expectImagePath: string, imageType: ImageType): Promise<string>;
8
+ imagePluginSourcePath(mediaSource: MulmoMediaSource, context: MulmoStudioContext, expectImagePath: string, imageType: ImageType): string | undefined;
5
9
  };
@@ -1,16 +1,52 @@
1
1
  import fs from "fs";
2
- import { getFullPath, resolveAssetPath } from "../utils/file.js";
2
+ import { GraphAILogger, assert } from "graphai";
3
+ import { getFullPath, getReferenceImagePath, resolveAssetPath } from "../utils/file.js";
4
+ import { downLoadReferenceImageError, getTextError, imageReferenceUnknownMediaError, downloadImagePluginError, imagePluginUnknownMediaError, } from "../utils/error_cause.js";
5
+ // for image reference
6
+ export const getExtention = (contentType, url) => {
7
+ if (contentType?.includes("jpeg") || contentType?.includes("jpg")) {
8
+ return "jpg";
9
+ }
10
+ else if (contentType?.includes("png")) {
11
+ return "png";
12
+ }
13
+ // Fall back to URL extension
14
+ const urlExtension = url.split(".").pop()?.toLowerCase();
15
+ if (urlExtension && ["jpg", "jpeg", "png"].includes(urlExtension)) {
16
+ return urlExtension === "jpeg" ? "jpg" : urlExtension;
17
+ }
18
+ return "png"; // default
19
+ };
20
+ const downLoadReferenceImage = async (context, key, url) => {
21
+ const response = await fetch(url);
22
+ assert(response.ok, `Failed to download reference image: ${url}`, false, downLoadReferenceImageError(key, url));
23
+ const buffer = Buffer.from(await response.arrayBuffer());
24
+ // Detect file extension from Content-Type header or URL
25
+ const extension = getExtention(response.headers.get("content-type"), url);
26
+ const imagePath = getReferenceImagePath(context, key, extension);
27
+ await fs.promises.writeFile(imagePath, buffer);
28
+ return imagePath;
29
+ };
30
+ // for image
31
+ function pluginSourceFixExtention(path, imageType) {
32
+ if (imageType === "movie") {
33
+ if (!path.endsWith(".png")) {
34
+ GraphAILogger.warn(`Expected .png extension for movie type, got: ${path}`);
35
+ }
36
+ return path.replace(/\.png$/, ".mov");
37
+ }
38
+ return path;
39
+ }
40
+ // end of util
3
41
  export const MulmoMediaSourceMethods = {
4
42
  async getText(mediaSource, context) {
5
43
  if (mediaSource.kind === "text") {
6
44
  return mediaSource.text;
7
45
  }
8
46
  if (mediaSource.kind === "url") {
9
- const res = await fetch(mediaSource.url);
10
- if (!res.ok) {
11
- throw new Error(`Failed to fetch media source: ${mediaSource.url}`);
12
- }
13
- return await res.text();
47
+ const response = await fetch(mediaSource.url);
48
+ assert(response.ok, `Failed to download mermaid code text: ${mediaSource.url}`, false, getTextError(mediaSource.url)); // TODO: index
49
+ return await response.text();
14
50
  }
15
51
  if (mediaSource.kind === "path") {
16
52
  const path = getFullPath(context.fileDirs.mulmoFileDirPath, mediaSource.path);
@@ -29,4 +65,43 @@ export const MulmoMediaSourceMethods = {
29
65
  }
30
66
  return null;
31
67
  },
68
+ // if url then download image and save it to file. both case return local image path. For image reference
69
+ async imageReference(mediaSource, context, key) {
70
+ if (mediaSource.kind === "path") {
71
+ return resolveAssetPath(context, mediaSource.path);
72
+ }
73
+ else if (mediaSource.kind === "url") {
74
+ return await downLoadReferenceImage(context, key, mediaSource.url);
75
+ }
76
+ // TODO base64
77
+ throw new Error(`imageReference media unknown error`, { cause: imageReferenceUnknownMediaError(key) });
78
+ },
79
+ async imagePluginSource(mediaSource, context, expectImagePath, imageType) {
80
+ if (mediaSource.kind === "url") {
81
+ const response = await fetch(mediaSource.url);
82
+ assert(response.ok, `Failed to download image plugin: ${imageType} ${mediaSource.url}`, false, downloadImagePluginError(mediaSource.url, imageType)); // TODO: key, id, index
83
+ const buffer = Buffer.from(await response.arrayBuffer());
84
+ // Detect file extension from Content-Type header or URL
85
+ const imagePath = pluginSourceFixExtention(expectImagePath, imageType);
86
+ await fs.promises.writeFile(imagePath, buffer);
87
+ return imagePath;
88
+ }
89
+ const path = MulmoMediaSourceMethods.resolve(mediaSource, context);
90
+ if (path) {
91
+ return path;
92
+ }
93
+ // base64??
94
+ GraphAILogger.error(`Image Plugin unknown ${imageType} source type:`, mediaSource);
95
+ throw new Error(`ERROR: unknown ${imageType} source type`, { cause: imagePluginUnknownMediaError(imageType) }); // TODO index
96
+ },
97
+ imagePluginSourcePath(mediaSource, context, expectImagePath, imageType) {
98
+ if (mediaSource?.kind === "url") {
99
+ return pluginSourceFixExtention(expectImagePath, imageType);
100
+ }
101
+ const path = MulmoMediaSourceMethods.resolve(mediaSource, context);
102
+ if (path) {
103
+ return path;
104
+ }
105
+ return undefined;
106
+ },
32
107
  };