mulmocast 2.1.18 → 2.1.20

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.
@@ -16,9 +16,9 @@
16
16
  },
17
17
  "speechParams": {
18
18
  "speakers": {
19
- "Announcer": { "provider": "nijivoice", "displayName": { "ja": "アナウンサー" }, "voiceId": "3708ad43-cace-486c-a4ca-8fe41186e20c" },
20
- "Student": { "provider": "nijivoice", "displayName": { "ja": "太郎" }, "voiceId": "a7619e48-bf6a-4f9f-843f-40485651257f" },
21
- "Teacher": { "provider": "nijivoice", "displayName": { "ja": "先生" }, "voiceId": "bc06c63f-fef6-43b6-92f7-67f919bd5dae" }
19
+ "Announcer": { "provider": "gemini", "displayName": { "ja": "アナウンサー" }, "voiceId": "Aoede" },
20
+ "Student": { "provider": "gemini", "displayName": { "ja": "太郎" }, "voiceId": "Puck" },
21
+ "Teacher": { "provider": "gemini", "displayName": { "ja": "先生" }, "voiceId": "Charon" }
22
22
  }
23
23
  }
24
24
  },
@@ -2,9 +2,9 @@ import dotenv from "dotenv";
2
2
  import { GraphAI, TaskManager, GraphAILogger } from "graphai";
3
3
  import * as agents from "@graphai/vanilla";
4
4
  import { fileWriteAgent } from "@graphai/vanilla_node_agents";
5
- import { ttsNijivoiceAgent, ttsOpenaiAgent, ttsGoogleAgent, ttsGeminiAgent, ttsElevenlabsAgent, ttsKotodamaAgent, addBGMAgent, combineAudioFilesAgent, mediaMockAgent, } from "../agents/index.js";
5
+ import { ttsOpenaiAgent, ttsGoogleAgent, ttsGeminiAgent, ttsElevenlabsAgent, ttsKotodamaAgent, addBGMAgent, combineAudioFilesAgent, mediaMockAgent, } from "../agents/index.js";
6
6
  import { text2SpeechProviderSchema } from "../types/index.js";
7
- import { fileCacheAgentFilter, nijovoiceTextAgentFilter } from "../utils/filters.js";
7
+ import { fileCacheAgentFilter } from "../utils/filters.js";
8
8
  import { getAudioArtifactFilePath, getAudioFilePath, getOutputStudioFilePath, resolveDirPath, defaultBGMPath, mkdir, writingMessage } from "../utils/file.js";
9
9
  import { localizedText, settings2GraphAIConfig } from "../utils/utils.js";
10
10
  import { text2hash } from "../utils/utils_node.js";
@@ -214,14 +214,9 @@ const agentFilters = [
214
214
  agent: fileCacheAgentFilter,
215
215
  nodeIds: ["tts"],
216
216
  },
217
- {
218
- name: "nijovoiceTextAgentFilter",
219
- agent: nijovoiceTextAgentFilter,
220
- nodeIds: ["tts"],
221
- },
222
217
  ];
223
218
  const getConcurrency = (context) => {
224
- // Check if any speaker uses nijivoice or elevenlabs (providers that require concurrency = 1)
219
+ // Check if any speaker uses elevenlabs or kotodama (providers that require concurrency = 1)
225
220
  const hasLimitedConcurrencyProvider = Object.values(context.presentationStyle.speechParams.speakers).some((speaker) => {
226
221
  const provider = text2SpeechProviderSchema.parse(speaker.provider);
227
222
  return provider2TTSAgent[provider].hasLimitedConcurrency;
@@ -232,7 +227,6 @@ const audioAgents = {
232
227
  ...vanillaAgents,
233
228
  fileWriteAgent,
234
229
  ttsOpenaiAgent,
235
- ttsNijivoiceAgent,
236
230
  ttsGoogleAgent,
237
231
  ttsGeminiAgent,
238
232
  ttsKotodamaAgent,
@@ -28,7 +28,7 @@ export declare const getTransitionVideoId: (transition: MulmoTransition, videoId
28
28
  };
29
29
  export declare const getConcatVideoFilter: (concatVideoId: string, videoIdsForBeats: VideoId[]) => string;
30
30
  export declare const validateBeatSource: (studioBeat: MulmoStudioContext["studio"]["beats"][number], index: number) => string;
31
- export declare const addSplitAndExtractFrames: (ffmpegContext: FfmpegContext, videoId: string, duration: number, isMovie: boolean, needFirst: boolean, needLast: boolean, canvasInfo: {
31
+ export declare const addSplitAndExtractFrames: (ffmpegContext: FfmpegContext, videoId: string, firstDuration: number, lastDuration: number, isMovie: boolean, needFirst: boolean, needLast: boolean, canvasInfo: {
32
32
  width: number;
33
33
  height: number;
34
34
  }) => void;
@@ -147,8 +147,7 @@ const addTransitionEffects = (ffmpegContext, captionedVideoId, context, transiti
147
147
  // Limit transition duration to be no longer than either beat's duration
148
148
  const prevBeatDuration = context.studio.beats[beatIndex - 1].duration ?? 1;
149
149
  const currentBeatDuration = context.studio.beats[beatIndex].duration ?? 1;
150
- const maxDuration = Math.min(prevBeatDuration, currentBeatDuration) * 0.9; // Use 90% to leave some margin
151
- const duration = Math.min(transition.duration, maxDuration);
150
+ const duration = getClampedTransitionDuration(transition.duration, prevBeatDuration, currentBeatDuration);
152
151
  const outputVideoId = `trans_${beatIndex}_o`;
153
152
  const processedVideoId = `${transitionVideoId}_f`;
154
153
  if (transition.type === "fade") {
@@ -277,6 +276,33 @@ export const getConcatVideoFilter = (concatVideoId, videoIdsForBeats) => {
277
276
  const inputs = videoIds.map((id) => `[${id}]`).join("");
278
277
  return `${inputs}concat=n=${videoIds.length}:v=1:a=0[${concatVideoId}]`;
279
278
  };
279
+ const getClampedTransitionDuration = (transitionDuration, prevBeatDuration, currentBeatDuration) => {
280
+ const maxDuration = Math.min(prevBeatDuration, currentBeatDuration) * 0.9; // Use 90% to leave some margin
281
+ return Math.min(transitionDuration, maxDuration);
282
+ };
283
+ const getTransitionFrameDurations = (context, index) => {
284
+ const minFrame = 1 / 30; // 30fpsを想定。最小1フレーム
285
+ const beats = context.studio.beats;
286
+ const scriptBeats = context.studio.script.beats;
287
+ const currentTransition = MulmoPresentationStyleMethods.getMovieTransition(context, scriptBeats[index]);
288
+ let firstDuration = 0;
289
+ if (currentTransition && index > 0) {
290
+ const prevBeatDuration = beats[index - 1].duration ?? 1;
291
+ const currentBeatDuration = beats[index].duration ?? 1;
292
+ firstDuration = getClampedTransitionDuration(currentTransition.duration, prevBeatDuration, currentBeatDuration);
293
+ }
294
+ const nextTransition = index < scriptBeats.length - 1 ? MulmoPresentationStyleMethods.getMovieTransition(context, scriptBeats[index + 1]) : null;
295
+ let lastDuration = 0;
296
+ if (nextTransition) {
297
+ const prevBeatDuration = beats[index].duration ?? 1;
298
+ const currentBeatDuration = beats[index + 1].duration ?? 1;
299
+ lastDuration = getClampedTransitionDuration(nextTransition.duration, prevBeatDuration, currentBeatDuration);
300
+ }
301
+ return {
302
+ firstDuration: Math.max(firstDuration, minFrame),
303
+ lastDuration: Math.max(lastDuration, minFrame),
304
+ };
305
+ };
280
306
  export const validateBeatSource = (studioBeat, index) => {
281
307
  const sourceFile = studioBeat.lipSyncFile ?? studioBeat.soundEffectFile ?? studioBeat.movieFile ?? studioBeat.htmlImageFile ?? studioBeat.imageFile;
282
308
  assert(!!sourceFile, `studioBeat.imageFile or studioBeat.movieFile is not set: index=${index}`, false, createVideoSourceError(index));
@@ -284,7 +310,7 @@ export const validateBeatSource = (studioBeat, index) => {
284
310
  assert(!!studioBeat.duration, `studioBeat.duration is not set: index=${index}`);
285
311
  return sourceFile;
286
312
  };
287
- export const addSplitAndExtractFrames = (ffmpegContext, videoId, duration, isMovie, needFirst, needLast, canvasInfo) => {
313
+ export const addSplitAndExtractFrames = (ffmpegContext, videoId, firstDuration, lastDuration, isMovie, needFirst, needLast, canvasInfo) => {
288
314
  const outputs = [`[${videoId}]`];
289
315
  if (needFirst)
290
316
  outputs.push(`[${videoId}_first_src]`);
@@ -294,20 +320,20 @@ export const addSplitAndExtractFrames = (ffmpegContext, videoId, duration, isMov
294
320
  if (needFirst) {
295
321
  // Create static frame using nullsrc as base for proper framerate/timebase
296
322
  // Note: setpts must NOT be used here as it loses framerate metadata needed by xfade
297
- ffmpegContext.filterComplex.push(`nullsrc=size=${canvasInfo.width}x${canvasInfo.height}:duration=${duration}:rate=30[${videoId}_first_null]`);
323
+ ffmpegContext.filterComplex.push(`nullsrc=size=${canvasInfo.width}x${canvasInfo.height}:duration=${firstDuration}:rate=30[${videoId}_first_null]`);
298
324
  ffmpegContext.filterComplex.push(`[${videoId}_first_src]select='eq(n,0)',scale=${canvasInfo.width}:${canvasInfo.height}[${videoId}_first_frame]`);
299
325
  ffmpegContext.filterComplex.push(`[${videoId}_first_null][${videoId}_first_frame]overlay=format=auto,fps=30[${videoId}_first]`);
300
326
  }
301
327
  if (needLast) {
302
328
  if (isMovie) {
303
329
  // Movie beats: extract actual last frame
304
- ffmpegContext.filterComplex.push(`nullsrc=size=${canvasInfo.width}x${canvasInfo.height}:duration=${duration}:rate=30[${videoId}_last_null]`);
330
+ ffmpegContext.filterComplex.push(`nullsrc=size=${canvasInfo.width}x${canvasInfo.height}:duration=${lastDuration}:rate=30[${videoId}_last_null]`);
305
331
  ffmpegContext.filterComplex.push(`[${videoId}_last_src]reverse,select='eq(n,0)',reverse,scale=${canvasInfo.width}:${canvasInfo.height}[${videoId}_last_frame]`);
306
332
  ffmpegContext.filterComplex.push(`[${videoId}_last_null][${videoId}_last_frame]overlay=format=auto,fps=30[${videoId}_last]`);
307
333
  }
308
334
  else {
309
335
  // Image beats: all frames are identical, so just select one
310
- ffmpegContext.filterComplex.push(`nullsrc=size=${canvasInfo.width}x${canvasInfo.height}:duration=${duration}:rate=30[${videoId}_last_null]`);
336
+ ffmpegContext.filterComplex.push(`nullsrc=size=${canvasInfo.width}x${canvasInfo.height}:duration=${lastDuration}:rate=30[${videoId}_last_null]`);
311
337
  ffmpegContext.filterComplex.push(`[${videoId}_last_src]select='eq(n,0)',scale=${canvasInfo.width}:${canvasInfo.height}[${videoId}_last_frame]`);
312
338
  ffmpegContext.filterComplex.push(`[${videoId}_last_null][${videoId}_last_frame]overlay=format=auto,fps=30[${videoId}_last]`);
313
339
  }
@@ -364,7 +390,8 @@ export const createVideo = async (audioArtifactFilePath, outputVideoPath, contex
364
390
  const needLast = needsLastFrame[index]; // Next beat has transition
365
391
  videoIdsForBeats.push(videoId);
366
392
  if (needFirst || needLast) {
367
- addSplitAndExtractFrames(ffmpegContext, videoId, duration, isMovie, needFirst, needLast, canvasInfo);
393
+ const { firstDuration, lastDuration } = getTransitionFrameDurations(context, index);
394
+ addSplitAndExtractFrames(ffmpegContext, videoId, firstDuration, lastDuration, isMovie, needFirst, needLast, canvasInfo);
368
395
  }
369
396
  // Record transition info if this beat has a transition
370
397
  const transition = MulmoPresentationStyleMethods.getMovieTransition(context, beat);
@@ -8,7 +8,6 @@ import movieGenAIAgent from "./movie_genai_agent.js";
8
8
  import movieReplicateAgent from "./movie_replicate_agent.js";
9
9
  import mediaMockAgent from "./media_mock_agent.js";
10
10
  import ttsElevenlabsAgent from "./tts_elevenlabs_agent.js";
11
- import ttsNijivoiceAgent from "./tts_nijivoice_agent.js";
12
11
  import ttsOpenaiAgent from "./tts_openai_agent.js";
13
12
  import ttsGoogleAgent from "./tts_google_agent.js";
14
13
  import ttsGeminiAgent from "./tts_gemini_agent.js";
@@ -21,4 +20,4 @@ import { browserlessAgent } from "@graphai/browserless_agent";
21
20
  import { textInputAgent } from "@graphai/input_agents";
22
21
  import { openAIAgent } from "@graphai/openai_agent";
23
22
  import { fileWriteAgent } from "@graphai/vanilla_node_agents";
24
- export { openAIAgent, fileWriteAgent, browserlessAgent, textInputAgent, addBGMAgent, combineAudioFilesAgent, imageGenAIAgent, imageOpenaiAgent, imageReplicateAgent, tavilySearchAgent, movieGenAIAgent, movieReplicateAgent, mediaMockAgent, ttsElevenlabsAgent, ttsNijivoiceAgent, ttsOpenaiAgent, ttsGoogleAgent, ttsGeminiAgent, ttsKotodamaAgent, validateSchemaAgent, soundEffectReplicateAgent, lipSyncReplicateAgent, puppeteerCrawlerAgent, };
23
+ export { openAIAgent, fileWriteAgent, browserlessAgent, textInputAgent, addBGMAgent, combineAudioFilesAgent, imageGenAIAgent, imageOpenaiAgent, imageReplicateAgent, tavilySearchAgent, movieGenAIAgent, movieReplicateAgent, mediaMockAgent, ttsElevenlabsAgent, ttsOpenaiAgent, ttsGoogleAgent, ttsGeminiAgent, ttsKotodamaAgent, validateSchemaAgent, soundEffectReplicateAgent, lipSyncReplicateAgent, puppeteerCrawlerAgent, };
@@ -8,7 +8,6 @@ import movieGenAIAgent from "./movie_genai_agent.js";
8
8
  import movieReplicateAgent from "./movie_replicate_agent.js";
9
9
  import mediaMockAgent from "./media_mock_agent.js";
10
10
  import ttsElevenlabsAgent from "./tts_elevenlabs_agent.js";
11
- import ttsNijivoiceAgent from "./tts_nijivoice_agent.js";
12
11
  import ttsOpenaiAgent from "./tts_openai_agent.js";
13
12
  import ttsGoogleAgent from "./tts_google_agent.js";
14
13
  import ttsGeminiAgent from "./tts_gemini_agent.js";
@@ -22,4 +21,4 @@ import { textInputAgent } from "@graphai/input_agents";
22
21
  import { openAIAgent } from "@graphai/openai_agent";
23
22
  // import * as vanilla from "@graphai/vanilla";
24
23
  import { fileWriteAgent } from "@graphai/vanilla_node_agents";
25
- export { openAIAgent, fileWriteAgent, browserlessAgent, textInputAgent, addBGMAgent, combineAudioFilesAgent, imageGenAIAgent, imageOpenaiAgent, imageReplicateAgent, tavilySearchAgent, movieGenAIAgent, movieReplicateAgent, mediaMockAgent, ttsElevenlabsAgent, ttsNijivoiceAgent, ttsOpenaiAgent, ttsGoogleAgent, ttsGeminiAgent, ttsKotodamaAgent, validateSchemaAgent, soundEffectReplicateAgent, lipSyncReplicateAgent, puppeteerCrawlerAgent, };
24
+ export { openAIAgent, fileWriteAgent, browserlessAgent, textInputAgent, addBGMAgent, combineAudioFilesAgent, imageGenAIAgent, imageOpenaiAgent, imageReplicateAgent, tavilySearchAgent, movieGenAIAgent, movieReplicateAgent, mediaMockAgent, ttsElevenlabsAgent, ttsOpenaiAgent, ttsGoogleAgent, ttsGeminiAgent, ttsKotodamaAgent, validateSchemaAgent, soundEffectReplicateAgent, lipSyncReplicateAgent, puppeteerCrawlerAgent, };
@@ -890,22 +890,22 @@ export const promptTemplates = [
890
890
  displayName: {
891
891
  ja: "アナウンサー",
892
892
  },
893
- provider: "nijivoice",
894
- voiceId: "3708ad43-cace-486c-a4ca-8fe41186e20c",
893
+ provider: "gemini",
894
+ voiceId: "Aoede",
895
895
  },
896
896
  Student: {
897
897
  displayName: {
898
898
  ja: "太郎",
899
899
  },
900
- provider: "nijivoice",
901
- voiceId: "a7619e48-bf6a-4f9f-843f-40485651257f",
900
+ provider: "gemini",
901
+ voiceId: "Puck",
902
902
  },
903
903
  Teacher: {
904
904
  displayName: {
905
905
  ja: "先生",
906
906
  },
907
- provider: "nijivoice",
908
- voiceId: "bc06c63f-fef6-43b6-92f7-67f919bd5dae",
907
+ provider: "gemini",
908
+ voiceId: "Charon",
909
909
  },
910
910
  },
911
911
  },
@@ -73,7 +73,7 @@ export const templateDataSet = {
73
73
  "```",
74
74
  sensei_and_taro: "全てを高校生にも分かるように、太郎くん(Student)と先生(Teacher)の会話、という形の台本にして。ただし要点はしっかりと押さえて。以下に別のトピックに関するサンプルを貼り付けます。このJSONフォーマットに従って。\n" +
75
75
  "```JSON\n" +
76
- `{"$mulmocast":{"version":"1.1","credit":"closing"},"title":"韓国の戒厳令とその日本への影響","description":"韓国で最近発令された戒厳令とその可能性のある影響について、また日本の憲法に関する考慮事項との類似点を含めた洞察に満ちた議論。","lang":"ja","beats":[{"speaker":"Announcer","text":"今日は、韓国で起きた戒厳令について、太郎くんが先生に聞きます。","imagePrompt":"A classroom setting with a curious Japanese student (Taro) and a kind teacher. Calm atmosphere, early morning light coming through the window."},{"speaker":"Student","text":"先生、今日は韓国で起きた戒厳令のことを教えてもらえますか?","imagePrompt":"The student (Taro) sitting at his desk with a serious expression, raising his hand to ask a question. Teacher is slightly surprised but attentive."},{"speaker":"Teacher","text":"もちろんだよ、太郎くん。韓国で最近、大統領が「戒厳令」っていうのを突然宣言したんだ。","imagePrompt":"TV screen showing a breaking news headline in Korean: 'President Declares Martial Law'. Students watching with concern."},{"speaker":"Student","text":"戒厳令ってなんですか?","imagePrompt":"A close-up of the student's puzzled face, with a speech bubble saying '戒厳令って?'"},{"speaker":"Teacher","text":"簡単に言うと、国がすごく危ない状態にあるとき、軍隊を使って人々の自由を制限するためのものなんだ。","imagePrompt":"Illustration of soldiers standing in the street, people being stopped and questioned, with a red 'X' on a protest sign. Moody and serious tone."},{"speaker":"Student","text":"それって怖いですね。なんでそんなことをしたんですか?","imagePrompt":"Student looking anxious, thinking deeply. Background shows a shadowy image of a politician giving orders to the military."},{"speaker":"Teacher","text":"大統領は「国会がうまく機能していないから」と言っていたけど…","imagePrompt":"A tense scene of military personnel entering a national assembly building in Korea, lawmakers looking shocked and resisting."},{"speaker":"Student","text":"ええっ!?国会議員を捕まえようとするなんて、すごく危ないことじゃないですか。","imagePrompt":"The student reacts with shock, comic-style expression with wide eyes and open mouth. Background fades into a dramatic courtroom or parliament chaos."},{"speaker":"Teacher","text":"その通りだよ。もし軍隊が国会を占拠していたら…","imagePrompt":"Dark visual of a locked parliament building with soldiers blocking the entrance, ominous sky in the background."},{"speaker":"Student","text":"韓国ではどうなったんですか?","imagePrompt":"Student leans forward, curious and worried. Background shows a hopeful scene of people holding protest signs with candles at night."},{"speaker":"Teacher","text":"幸い、野党の議員や市民たちが急いで集まって抗議して…","imagePrompt":"Peaceful protest scene in Seoul, citizens holding candles and banners, united. Hopeful tone."},{"speaker":"Student","text":"それは大変なことですね…。日本ではそんなこと起きないんですか?","imagePrompt":"Student looking toward the Japanese flag outside the school window, pensive mood."},{"speaker":"Teacher","text":"実はね、今、日本でも似たような話があるんだよ。","imagePrompt":"Teacher pointing to a newspaper headline: '緊急事態条項の議論進む'. Classroom chalkboard shows a map of Korea and Japan."},{"speaker":"Student","text":"緊急事態宣言って、韓国の戒厳令と同じようなものなんですか?","imagePrompt":"Split screen image: left side shows a soldier in Korea, right side shows a suited Japanese politician giving a press conference."},{"speaker":"Teacher","text":"似ている部分があるね。たとえば、総理大臣が…","imagePrompt":"Diagram-style visual showing the flow of emergency powers from PM to local governments. Simple, clean infographic style."},{"speaker":"Student","text":"それって便利そうですけど、なんだか心配です。","imagePrompt":"Student's concerned expression, behind him a blurry image of a street with emergency sirens glowing in red."},{"speaker":"Teacher","text":"そうだね。もちろん、緊急時には素早い対応が必要だけど…","imagePrompt":"Illustration of a balance scale: one side is 'freedom', the other 'security'. The scale is slightly tilting."},{"speaker":"Student","text":"韓国みたいに、軍隊が政治に口を出してくることもあり得るんですか?","imagePrompt":"Student imagining a military tank next to the Japanese parliament, shown as a thought bubble."},{"speaker":"Teacher","text":"完全にあり得ないとは言えないからこそ、注意が必要なんだ。","imagePrompt":"Japanese citizens reading newspapers and watching news with concerned faces, civic awareness growing."},{"speaker":"Student","text":"ありがとうございます。とても良い勉強になりました。","imagePrompt":"The student bows slightly to the teacher with a grateful expression. The classroom is peaceful again."},{"speaker":"Announcer","text":"ご視聴、ありがとうございました。次回の放送もお楽しみに。","imagePrompt":"Ending screen with soft background music, showing the show's logo and a thank-you message in Japanese."}],"canvasSize":{"width":1536,"height":1024},"imageParams":{"style":"<style>Ghibli style. Student (Taro) is a young teenager with a dark short hair with glasses. Teacher is a middle-aged man with grey hair and moustache.</style>"},"speechParams":{"speakers":{"Announcer":{"provider":"nijivoice","displayName":{"ja":"アナウンサー"},"voiceId":"3708ad43-cace-486c-a4ca-8fe41186e20c"},"Student":{"provider":"nijivoice","displayName":{"ja":"太郎"},"voiceId":"a7619e48-bf6a-4f9f-843f-40485651257f"},"Teacher":{"provider":"nijivoice","displayName":{"ja":"先生"},"voiceId":"bc06c63f-fef6-43b6-92f7-67f919bd5dae"}}}}\n` +
76
+ `{"$mulmocast":{"version":"1.1","credit":"closing"},"title":"韓国の戒厳令とその日本への影響","description":"韓国で最近発令された戒厳令とその可能性のある影響について、また日本の憲法に関する考慮事項との類似点を含めた洞察に満ちた議論。","lang":"ja","beats":[{"speaker":"Announcer","text":"今日は、韓国で起きた戒厳令について、太郎くんが先生に聞きます。","imagePrompt":"A classroom setting with a curious Japanese student (Taro) and a kind teacher. Calm atmosphere, early morning light coming through the window."},{"speaker":"Student","text":"先生、今日は韓国で起きた戒厳令のことを教えてもらえますか?","imagePrompt":"The student (Taro) sitting at his desk with a serious expression, raising his hand to ask a question. Teacher is slightly surprised but attentive."},{"speaker":"Teacher","text":"もちろんだよ、太郎くん。韓国で最近、大統領が「戒厳令」っていうのを突然宣言したんだ。","imagePrompt":"TV screen showing a breaking news headline in Korean: 'President Declares Martial Law'. Students watching with concern."},{"speaker":"Student","text":"戒厳令ってなんですか?","imagePrompt":"A close-up of the student's puzzled face, with a speech bubble saying '戒厳令って?'"},{"speaker":"Teacher","text":"簡単に言うと、国がすごく危ない状態にあるとき、軍隊を使って人々の自由を制限するためのものなんだ。","imagePrompt":"Illustration of soldiers standing in the street, people being stopped and questioned, with a red 'X' on a protest sign. Moody and serious tone."},{"speaker":"Student","text":"それって怖いですね。なんでそんなことをしたんですか?","imagePrompt":"Student looking anxious, thinking deeply. Background shows a shadowy image of a politician giving orders to the military."},{"speaker":"Teacher","text":"大統領は「国会がうまく機能していないから」と言っていたけど…","imagePrompt":"A tense scene of military personnel entering a national assembly building in Korea, lawmakers looking shocked and resisting."},{"speaker":"Student","text":"ええっ!?国会議員を捕まえようとするなんて、すごく危ないことじゃないですか。","imagePrompt":"The student reacts with shock, comic-style expression with wide eyes and open mouth. Background fades into a dramatic courtroom or parliament chaos."},{"speaker":"Teacher","text":"その通りだよ。もし軍隊が国会を占拠していたら…","imagePrompt":"Dark visual of a locked parliament building with soldiers blocking the entrance, ominous sky in the background."},{"speaker":"Student","text":"韓国ではどうなったんですか?","imagePrompt":"Student leans forward, curious and worried. Background shows a hopeful scene of people holding protest signs with candles at night."},{"speaker":"Teacher","text":"幸い、野党の議員や市民たちが急いで集まって抗議して…","imagePrompt":"Peaceful protest scene in Seoul, citizens holding candles and banners, united. Hopeful tone."},{"speaker":"Student","text":"それは大変なことですね…。日本ではそんなこと起きないんですか?","imagePrompt":"Student looking toward the Japanese flag outside the school window, pensive mood."},{"speaker":"Teacher","text":"実はね、今、日本でも似たような話があるんだよ。","imagePrompt":"Teacher pointing to a newspaper headline: '緊急事態条項の議論進む'. Classroom chalkboard shows a map of Korea and Japan."},{"speaker":"Student","text":"緊急事態宣言って、韓国の戒厳令と同じようなものなんですか?","imagePrompt":"Split screen image: left side shows a soldier in Korea, right side shows a suited Japanese politician giving a press conference."},{"speaker":"Teacher","text":"似ている部分があるね。たとえば、総理大臣が…","imagePrompt":"Diagram-style visual showing the flow of emergency powers from PM to local governments. Simple, clean infographic style."},{"speaker":"Student","text":"それって便利そうですけど、なんだか心配です。","imagePrompt":"Student's concerned expression, behind him a blurry image of a street with emergency sirens glowing in red."},{"speaker":"Teacher","text":"そうだね。もちろん、緊急時には素早い対応が必要だけど…","imagePrompt":"Illustration of a balance scale: one side is 'freedom', the other 'security'. The scale is slightly tilting."},{"speaker":"Student","text":"韓国みたいに、軍隊が政治に口を出してくることもあり得るんですか?","imagePrompt":"Student imagining a military tank next to the Japanese parliament, shown as a thought bubble."},{"speaker":"Teacher","text":"完全にあり得ないとは言えないからこそ、注意が必要なんだ。","imagePrompt":"Japanese citizens reading newspapers and watching news with concerned faces, civic awareness growing."},{"speaker":"Student","text":"ありがとうございます。とても良い勉強になりました。","imagePrompt":"The student bows slightly to the teacher with a grateful expression. The classroom is peaceful again."},{"speaker":"Announcer","text":"ご視聴、ありがとうございました。次回の放送もお楽しみに。","imagePrompt":"Ending screen with soft background music, showing the show's logo and a thank-you message in Japanese."}],"canvasSize":{"width":1536,"height":1024},"imageParams":{"style":"<style>Ghibli style. Student (Taro) is a young teenager with a dark short hair with glasses. Teacher is a middle-aged man with grey hair and moustache.</style>"},"speechParams":{"speakers":{"Announcer":{"provider":"gemini","displayName":{"ja":"アナウンサー"},"voiceId":"Aoede"},"Student":{"provider":"gemini","displayName":{"ja":"太郎"},"voiceId":"Puck"},"Teacher":{"provider":"gemini","displayName":{"ja":"先生"},"voiceId":"Charon"}}}}\n` +
77
77
  "```",
78
78
  shorts: "This script is for YouTube shorts. The first beat should be a hook, which describes the topic. Another AI will generate images for each beat based on the image prompt of that beat. Movie prompts must be written in English.\n" +
79
79
  "```JSON\n" +
@@ -113,10 +113,6 @@ export type OpenAITTSAgentParams = TTSAgentParams & {
113
113
  model: string;
114
114
  speed: number;
115
115
  };
116
- export type NijivoiceTTSAgentParams = TTSAgentParams & {
117
- speed: number;
118
- speed_global: number;
119
- };
120
116
  export type KotodamaTTSAgentParams = TTSAgentParams & {
121
117
  decoration: string;
122
118
  };
@@ -1,9 +1,4 @@
1
1
  export declare const provider2TTSAgent: {
2
- nijivoice: {
3
- agentName: string;
4
- hasLimitedConcurrency: boolean;
5
- keyName: string;
6
- };
7
2
  openai: {
8
3
  agentName: string;
9
4
  hasLimitedConcurrency: boolean;
@@ -1,10 +1,5 @@
1
1
  // node & browser
2
2
  export const provider2TTSAgent = {
3
- nijivoice: {
4
- agentName: "ttsNijivoiceAgent",
5
- hasLimitedConcurrency: true,
6
- keyName: "NIJIVOICE_API_KEY",
7
- },
8
3
  openai: {
9
4
  agentName: "ttsOpenaiAgent",
10
5
  hasLimitedConcurrency: false,
@@ -252,8 +252,8 @@ export const getGenAIErrorReason = (error) => {
252
252
  }
253
253
  }
254
254
  }
255
- catch (__error) {
256
- // nothing.
255
+ catch {
256
+ // Ignore JSON parse errors - return undefined if parsing fails
257
257
  }
258
258
  return undefined;
259
259
  };
@@ -1,5 +1,4 @@
1
1
  import type { AgentFilterFunction } from "graphai";
2
- export declare const nijovoiceTextAgentFilter: AgentFilterFunction;
3
2
  export declare const fileCacheAgentFilter: AgentFilterFunction;
4
3
  export declare const browserlessCacheGenerator: (cacheDir: string) => AgentFilterFunction;
5
4
  export declare const getBackupFilePath: (originalPath: string) => string;
@@ -6,15 +6,7 @@ import { GraphAILogger } from "graphai";
6
6
  import { writingMessage, isFile } from "./file.js";
7
7
  import { text2hash } from "./utils_node.js";
8
8
  import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
9
- import { replacementsJa, replacePairsJa } from "../utils/string.js";
10
9
  dotenv.config({ quiet: true });
11
- export const nijovoiceTextAgentFilter = async (context, next) => {
12
- const { text, provider, lang } = context.namedInputs;
13
- if (provider === "nijivoice" && lang === "ja") {
14
- context.namedInputs.text = replacePairsJa(replacementsJa)(text);
15
- }
16
- return next(context);
17
- };
18
10
  export const fileCacheAgentFilter = async (context, next) => {
19
11
  const { force, file, index, mulmoContext, sessionType, id, withBackup } = context.namedInputs.cache;
20
12
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.1.18",
3
+ "version": "2.1.20",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -65,7 +65,10 @@
65
65
  "nijivoice": "npx tsx batch/niji_sample.ts && yarn run movie scripts/samples/niji_voice.json",
66
66
  "generate_action_docs": "npx tsx ./automation/generate_actions_docs/generate_action_docs.ts"
67
67
  },
68
- "repository": "git+ssh://git@github.com/receptron/mulmocast-cli.git",
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+ssh://git@github.com/receptron/mulmocast-cli.git"
71
+ },
69
72
  "author": "snakajima",
70
73
  "license": "AGPL-3.0-only",
71
74
  "bugs": {
@@ -74,10 +77,10 @@
74
77
  "homepage": "https://github.com/receptron/mulmocast-cli#readme",
75
78
  "dependencies": {
76
79
  "@google-cloud/text-to-speech": "^6.4.0",
77
- "@google/genai": "^1.37.0",
80
+ "@google/genai": "^1.38.0",
78
81
  "@graphai/anthropic_agent": "^2.0.12",
79
82
  "@graphai/browserless_agent": "^2.0.1",
80
- "@graphai/gemini_agent": "^2.0.2",
83
+ "@graphai/gemini_agent": "^2.0.4",
81
84
  "@graphai/groq_agent": "^2.0.2",
82
85
  "@graphai/input_agents": "^1.0.2",
83
86
  "@graphai/openai_agent": "^2.0.8",
@@ -86,23 +89,23 @@
86
89
  "@graphai/vanilla_node_agents": "^2.0.4",
87
90
  "@inquirer/input": "^5.0.4",
88
91
  "@inquirer/select": "^5.0.4",
89
- "@modelcontextprotocol/sdk": "^1.25.1",
92
+ "@modelcontextprotocol/sdk": "^1.25.3",
90
93
  "@mozilla/readability": "^0.6.0",
91
94
  "@tavily/core": "^0.5.11",
92
95
  "archiver": "^7.0.1",
93
- "clipboardy": "^5.0.2",
96
+ "clipboardy": "^5.1.0",
94
97
  "dotenv": "^17.2.3",
95
98
  "fluent-ffmpeg": "^2.1.3",
96
99
  "graphai": "^2.0.16",
97
100
  "jsdom": "^27.4.0",
98
101
  "marked": "^17.0.1",
99
102
  "mulmocast-vision": "^1.0.8",
100
- "ora": "^9.0.0",
101
- "puppeteer": "^24.35.0",
103
+ "ora": "^9.1.0",
104
+ "puppeteer": "^24.36.0",
102
105
  "replicate": "^1.4.0",
103
106
  "yaml": "^2.8.2",
104
107
  "yargs": "^18.0.0",
105
- "zod": "^4.3.5"
108
+ "zod": "^4.3.6"
106
109
  },
107
110
  "devDependencies": {
108
111
  "@receptron/test_utils": "^2.0.3",
@@ -114,10 +117,10 @@
114
117
  "eslint-config-prettier": "^10.1.8",
115
118
  "eslint-plugin-prettier": "^5.5.5",
116
119
  "eslint-plugin-sonarjs": "^3.0.5",
117
- "prettier": "^3.8.0",
120
+ "prettier": "^3.8.1",
118
121
  "tsx": "^4.21.0",
119
122
  "typescript": "^5.9.3",
120
- "typescript-eslint": "^8.53.0"
123
+ "typescript-eslint": "^8.53.1"
121
124
  },
122
125
  "engines": {
123
126
  "node": ">=20.0.0"
@@ -19,7 +19,7 @@
19
19
  "speechParams": {
20
20
  "speakers": {
21
21
  "Announcer": {
22
- "provider": "nijivoice",
22
+ "provider": "gemini",
23
23
  "displayName": {
24
24
  "ja": "アナウンサー"
25
25
  },
@@ -29,14 +29,14 @@
29
29
  }
30
30
  },
31
31
  "Student": {
32
- "provider": "nijivoice",
32
+ "provider": "gemini",
33
33
  "displayName": {
34
34
  "ja": "生徒"
35
35
  },
36
36
  "voiceId": "a7619e48-bf6a-4f9f-843f-40485651257f"
37
37
  },
38
38
  "Teacher": {
39
- "provider": "nijivoice",
39
+ "provider": "gemini",
40
40
  "displayName": {
41
41
  "ja": "先生"
42
42
  },
@@ -17,7 +17,7 @@
17
17
  "style": "<style>monochrome"
18
18
  },
19
19
  "speechParams": {
20
- "provider": "nijivoice",
20
+ "provider": "gemini",
21
21
  "speakers": {
22
22
  "Announcer": {
23
23
  "displayName": {
@@ -23,7 +23,7 @@
23
23
  "voiceId": "3JDquces8E8bkmvbh6Bc"
24
24
  },
25
25
  "Nijivoice": {
26
- "provider": "nijivoice",
26
+ "provider": "gemini",
27
27
  "voiceId": "231e0170-0ece-4155-be44-231423062f41"
28
28
  }
29
29
  }
@@ -10,7 +10,7 @@
10
10
  "voiceId": "shimmer",
11
11
  "lang": {
12
12
  "ja": {
13
- "provider": "nijivoice",
13
+ "provider": "gemini",
14
14
  "voiceId": "9d9ed276-49ee-443a-bc19-26e6136d05f0"
15
15
  }
16
16
  }
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "Host": {
32
32
  "voiceId": "3708ad43-cace-486c-a4ca-8fe41186e20c",
33
- "provider": "nijivoice",
33
+ "provider": "gemini",
34
34
  "displayName": {
35
35
  "en": "Japanese Host"
36
36
  }