mulmocast 2.3.0 → 2.3.2

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 CHANGED
@@ -59,7 +59,7 @@ Here is the "Hello World" in MulmoScript.
59
59
  ```JSON
60
60
  {
61
61
  "$mulmocast": {
62
- "version": "1.0"
62
+ "version": "1.1"
63
63
  },
64
64
  "beats": [
65
65
  { "text": "Hello World" }
@@ -247,6 +247,8 @@ Top-level keys are applied as **defaults** (script values take precedence). Use
247
247
 
248
248
  Priority chain: `config (defaults)` < `template/style` < `script` < `config.override` < `presentationStyle (-p)`
249
249
 
250
+ > **Note**: `kind: "path"` entries in `mulmo.config.json` are resolved relative to the **script file directory**, not the config file location. This is consistent with all other path resolution in MulmoScript.
251
+
250
252
  Verify the merged result with:
251
253
  ```bash
252
254
  mulmo tool info merged --script <script.json>
@@ -499,8 +501,6 @@ https://github.com/receptron/mulmocast-cli/tree/main/scripts
499
501
 
500
502
  CLI Usage
501
503
 
502
-
503
-
504
504
  ```
505
505
  mulmo <command> [options]
506
506
 
@@ -511,6 +511,7 @@ Commands:
511
511
  mulmo movie <file> Generate movie file
512
512
  mulmo pdf <file> Generate PDF files
513
513
  mulmo markdown <file> Generate markdown files
514
+ mulmo bundle <file> Generate bundle files
514
515
  mulmo html <file> Generate html files
515
516
  mulmo tool <command> Generate Mulmo script and other tools
516
517
 
@@ -529,13 +530,19 @@ Positionals:
529
530
  file Mulmo Script File [string] [required]
530
531
 
531
532
  Options:
532
- --version Show version number [boolean]
533
- -v, --verbose verbose log [boolean] [required] [default: false]
534
- -h, --help Show help [boolean]
535
- -o, --outdir output dir [string]
536
- -b, --basedir base dir [string]
537
- -l, --lang target language [string] [choices: "en", "ja"]
538
- -f, --force Force regenerate [boolean] [default: false]
533
+ --version Show version number [boolean]
534
+ -v, --verbose verbose log [boolean] [required] [default: false]
535
+ -h, --help Show help [boolean]
536
+ -o, --outdir output dir [string]
537
+ -b, --basedir base dir [string]
538
+ -l, --lang target language
539
+ [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
540
+ "pt", "ar", "hi"]
541
+ -f, --force Force regenerate [boolean] [default: false]
542
+ -g, --grouped Output all files under output/<basename>/ directory
543
+ [boolean] [default: false]
544
+ --backup create backup media file [boolean] [default: false]
545
+ -p, --presentationStyle Presentation Style [string]
539
546
  ```
540
547
 
541
548
  ```
@@ -552,8 +559,13 @@ Options:
552
559
  -h, --help Show help [boolean]
553
560
  -o, --outdir output dir [string]
554
561
  -b, --basedir base dir [string]
555
- -l, --lang target language [string] [choices: "en", "ja"]
562
+ -l, --lang target language
563
+ [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
564
+ "pt", "ar", "hi"]
556
565
  -f, --force Force regenerate [boolean] [default: false]
566
+ -g, --grouped Output all files under output/<basename>/ directory
567
+ [boolean] [default: false]
568
+ --backup create backup media file [boolean] [default: false]
557
569
  -p, --presentationStyle Presentation Style [string]
558
570
  -a, --audiodir Audio output directory [string]
559
571
  ```
@@ -572,8 +584,13 @@ Options:
572
584
  -h, --help Show help [boolean]
573
585
  -o, --outdir output dir [string]
574
586
  -b, --basedir base dir [string]
575
- -l, --lang target language [string] [choices: "en", "ja"]
587
+ -l, --lang target language
588
+ [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
589
+ "pt", "ar", "hi"]
576
590
  -f, --force Force regenerate [boolean] [default: false]
591
+ -g, --grouped Output all files under output/<basename>/ directory
592
+ [boolean] [default: false]
593
+ --backup create backup media file [boolean] [default: false]
577
594
  -p, --presentationStyle Presentation Style [string]
578
595
  -i, --imagedir Image output directory [string]
579
596
  ```
@@ -592,12 +609,19 @@ Options:
592
609
  -h, --help Show help [boolean]
593
610
  -o, --outdir output dir [string]
594
611
  -b, --basedir base dir [string]
595
- -l, --lang target language [string] [choices: "en", "ja"]
612
+ -l, --lang target language
613
+ [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
614
+ "pt", "ar", "hi"]
596
615
  -f, --force Force regenerate [boolean] [default: false]
616
+ -g, --grouped Output all files under output/<basename>/ directory
617
+ [boolean] [default: false]
618
+ --backup create backup media file [boolean] [default: false]
597
619
  -p, --presentationStyle Presentation Style [string]
598
620
  -a, --audiodir Audio output directory [string]
599
621
  -i, --imagedir Image output directory [string]
600
- -c, --caption Video captions [string] [choices: "en", "ja"]
622
+ -c, --caption Video captions
623
+ [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
624
+ "pt", "ar", "hi"]
601
625
  ```
602
626
 
603
627
  ```
@@ -614,9 +638,13 @@ Options:
614
638
  -h, --help Show help [boolean]
615
639
  -o, --outdir output dir [string]
616
640
  -b, --basedir base dir [string]
617
- -l, --lang target language [string] [choices: "en", "ja"]
641
+ -l, --lang target language
642
+ [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
643
+ "pt", "ar", "hi"]
618
644
  -f, --force Force regenerate [boolean] [default: false]
619
- --dryRun Dry run [boolean] [default: false]
645
+ -g, --grouped Output all files under output/<basename>/ directory
646
+ [boolean] [default: false]
647
+ --backup create backup media file [boolean] [default: false]
620
648
  -p, --presentationStyle Presentation Style [string]
621
649
  -i, --imagedir Image output directory [string]
622
650
  --pdf_mode PDF mode
@@ -643,6 +671,9 @@ Options:
643
671
  [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
644
672
  "pt", "ar", "hi"]
645
673
  -f, --force Force regenerate [boolean] [default: false]
674
+ -g, --grouped Output all files under output/<basename>/ directory
675
+ [boolean] [default: false]
676
+ --backup create backup media file [boolean] [default: false]
646
677
  -p, --presentationStyle Presentation Style [string]
647
678
  --image_width Image width (e.g., 400px, 50%, auto) [string]
648
679
  ```
@@ -665,6 +696,9 @@ Options:
665
696
  [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
666
697
  "pt", "ar", "hi"]
667
698
  -f, --force Force regenerate [boolean] [default: false]
699
+ -g, --grouped Output all files under output/<basename>/ directory
700
+ [boolean] [default: false]
701
+ --backup create backup media file [boolean] [default: false]
668
702
  -p, --presentationStyle Presentation Style [string]
669
703
  --image_width Image width (e.g., 400px, 50%, auto) [string]
670
704
  ```
@@ -687,6 +721,8 @@ Options:
687
721
  [string] [choices: "en", "ja", "fr", "es", "de", "zh-CN", "zh-TW", "ko", "it",
688
722
  "pt", "ar", "hi"]
689
723
  -f, --force Force regenerate [boolean] [default: false]
724
+ -g, --grouped Output all files under output/<basename>/ directory
725
+ [boolean] [default: false]
690
726
  --backup create backup media file [boolean] [default: false]
691
727
  -p, --presentationStyle Presentation Style [string]
692
728
  ```
@@ -697,11 +733,16 @@ mulmo tool <command>
697
733
  Generate Mulmo script and other tools
698
734
 
699
735
  Commands:
700
- mulmo tool scripting Generate mulmocast script
701
- mulmo tool complete <file> Complete partial MulmoScript with defaults
702
- mulmo tool prompt Dump prompt from template
703
- mulmo tool schema Dump mulmocast schema
704
- mulmo tool info [category] Show available options (styles, bgm, voices, etc.)
736
+ mulmo tool scripting Generate mulmocast script
737
+ mulmo tool prompt Dump prompt from template
738
+ mulmo tool schema Dump mulmocast schema
739
+ mulmo tool story_to_script <file> Generate Mulmo script from story
740
+ mulmo tool whisper <file> Process file with whisper
741
+ mulmo tool complete <file> Complete MulmoScript with schema defaults
742
+ and optional style
743
+ mulmo tool info [category] Show available options (styles, bgm,
744
+ templates, voices, images, movies, llm,
745
+ themes, config, merged)
705
746
 
706
747
  Options:
707
748
  --version Show version number [boolean]
@@ -726,15 +767,15 @@ Options:
726
767
  -i, --interactive Generate script in interactive mode with user prompts
727
768
  [boolean]
728
769
  -t, --template Template name to use
729
- [string] [choices: "akira_comic", "business", "children_book", "coding",
730
- "comic_strips", "drslump_comic", "ghibli_comic", "ghibli_image_only",
731
- "ghibli_shorts", "ghost_comic", "onepiece_comic", "podcast_standard",
732
- "portrait_movie", "realistic_movie", "sensei_and_taro", "shorts",
733
- "text_and_image", "text_only", "trailer"]
770
+ [string] [choices: "akira_comic", "ani", "business", "characters",
771
+ "children_book", "coding", "comic_strips", "documentary", "drslump_comic",
772
+ "ghibli_comic", "ghibli_comic_strips", "ghost_comic", "html", "image_prompt",
773
+ "leda", "onepiece_comic", "portrait_movie", "realistic_movie",
774
+ "sensei_and_taro", "shorts", "sifi_story", "trailer", "vision"]
734
775
  -c, --cache cache dir [string]
735
776
  -s, --script script filename [string] [default: "script"]
736
777
  --llm llm
737
- [string] [choices: "openai", "anthropic", "gemini", "groq"]
778
+ [string] [choices: "openai", "anthropic", "gemini", "groq", "mock"]
738
779
  --llm_model llm model [string]
739
780
  ```
740
781
 
@@ -753,12 +794,15 @@ Options:
753
794
  -o, --outdir output dir [string]
754
795
  -b, --basedir base dir [string]
755
796
  -t, --template Template name to use
756
- [string] [choices: "business", "children_book", "coding", "comic_strips",
757
- "ghibli_strips", "podcast_standard", "sensei_and_taro"]
797
+ [string] [choices: "akira_comic", "ani", "business", "characters",
798
+ "children_book", "coding", "comic_strips", "documentary", "drslump_comic",
799
+ "ghibli_comic", "ghibli_comic_strips", "ghost_comic", "html", "image_prompt",
800
+ "leda", "onepiece_comic", "portrait_movie", "realistic_movie",
801
+ "sensei_and_taro", "shorts", "sifi_story", "trailer", "vision"]
758
802
  -s, --script script filename [string] [default: "script"]
759
803
  --beats_per_scene beats per scene [number] [default: 3]
760
804
  --llm llm
761
- [string] [choices: "openAI", "anthropic", "gemini", "groq"]
805
+ [string] [choices: "openai", "anthropic", "gemini", "groq", "mock"]
762
806
  --llm_model llm model [string]
763
807
  --mode story to script generation mode
764
808
  [string] [choices: "step_wise", "one_step"] [default: "step_wise"]
@@ -774,8 +818,11 @@ Options:
774
818
  -v, --verbose verbose log [boolean] [required] [default: false]
775
819
  -h, --help Show help [boolean]
776
820
  -t, --template Template name to use
777
- [string] [choices: "business", "children_book", "coding", "comic_strips",
778
- "ghibli_strips", "podcast_standard", "sensei_and_taro"]
821
+ [string] [choices: "akira_comic", "ani", "business", "characters",
822
+ "children_book", "coding", "comic_strips", "documentary", "drslump_comic",
823
+ "ghibli_comic", "ghibli_comic_strips", "ghost_comic", "html", "image_prompt",
824
+ "leda", "onepiece_comic", "portrait_movie", "realistic_movie",
825
+ "sensei_and_taro", "shorts", "sifi_story", "trailer", "vision"]
779
826
  ```
780
827
 
781
828
  ```
@@ -792,18 +839,23 @@ Options:
792
839
  ```
793
840
  mulmo tool complete <file>
794
841
 
795
- Complete partial MulmoScript with schema defaults and optional style/template
842
+ Complete MulmoScript with schema defaults and optional style
796
843
 
797
844
  Positionals:
798
- file Input beats file path (JSON) [string] [required]
845
+ file Input beats file path (JSON) [string] [required]
799
846
 
800
847
  Options:
801
- --version Show version number [boolean]
802
- -v, --verbose verbose log [boolean] [required] [default: false]
803
- -h, --help Show help [boolean]
804
- -o, --output Output file path (default: <file>_completed.json) [string]
805
- -t, --template Template name to apply [string]
806
- -s, --style Style name or file path (.json) [string]
848
+ --version Show version number [boolean]
849
+ -v, --verbose verbose log [boolean] [required] [default: false]
850
+ -h, --help Show help [boolean]
851
+ -o, --output Output file path (default: <file>_completed.json) [string]
852
+ -t, --template Template name to apply
853
+ [string] [choices: "akira_comic", "ani", "business", "characters",
854
+ "children_book", "coding", "comic_strips", "documentary", "drslump_comic",
855
+ "ghibli_comic", "ghibli_comic_strips", "ghost_comic", "html", "image_prompt",
856
+ "leda", "onepiece_comic", "portrait_movie", "realistic_movie",
857
+ "sensei_and_taro", "shorts", "sifi_story", "trailer", "vision"]
858
+ -s, --style Style name or file path (.json) [string]
807
859
 
808
860
  Examples:
809
861
  # Complete minimal script with schema defaults
@@ -822,17 +874,21 @@ Examples:
822
874
  ```
823
875
  mulmo tool info [category]
824
876
 
825
- Show available options for MulmoScript configuration
877
+ Show available options (styles, bgm, templates, voices, images, movies, llm,
878
+ themes, config, merged)
826
879
 
827
880
  Positionals:
828
881
  category Category to show info for
829
- [string] [choices: "styles", "bgm", "templates", "voices", "images", "movies", "llm"]
882
+ [string] [choices: "styles", "bgm", "templates", "voices", "images", "movies",
883
+ "llm", "themes", "config", "merged"]
830
884
 
831
885
  Options:
832
886
  --version Show version number [boolean]
833
887
  -v, --verbose verbose log [boolean] [required] [default: false]
834
888
  -h, --help Show help [boolean]
835
- -F, --format Output format [string] [choices: "text", "json", "yaml"]
889
+ -F, --format Output format
890
+ [string] [choices: "text", "json", "yaml"] [default: "text"]
891
+ -S, --script Script file path (required for 'merged' category) [string]
836
892
 
837
893
  Examples:
838
894
  # Show all available categories
@@ -5,7 +5,7 @@ import { fileWriteAgent } from "@graphai/vanilla_node_agents";
5
5
  import { ttsOpenaiAgent, ttsGoogleAgent, ttsGeminiAgent, ttsElevenlabsAgent, ttsKotodamaAgent, addBGMAgent, combineAudioFilesAgent, mediaMockAgent, } from "../agents/index.js";
6
6
  import { text2SpeechProviderSchema } from "../types/index.js";
7
7
  import { fileCacheAgentFilter } from "../utils/filters.js";
8
- import { getAudioArtifactFilePath, getAudioFilePath, getOutputStudioFilePath, resolveDirPath, defaultBGMPath, mkdir, writingMessage } from "../utils/file.js";
8
+ import { getAudioArtifactFilePath, getAudioFilePath, getGroupedAudioFilePath, 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";
11
11
  import { provider2TTSAgent } from "../types/provider2agent.js";
@@ -43,7 +43,9 @@ export const getBeatAudioPathOrUrl = (text, context, beat, lang) => {
43
43
  ].join(":");
44
44
  GraphAILogger.log(`getBeatAudioPathOrUrl [${hash_string}]`);
45
45
  const audioFileName = `${context.studio.filename}_${text2hash(hash_string)}`;
46
- const maybeAudioFile = getAudioFilePath(audioDirPath, context.studio.filename, audioFileName, lang);
46
+ const maybeAudioFile = context.fileDirs.grouped
47
+ ? getGroupedAudioFilePath(audioDirPath, audioFileName, lang)
48
+ : getAudioFilePath(audioDirPath, context.studio.filename, audioFileName, lang);
47
49
  return getAudioPathOrUrl(context, beat, maybeAudioFile);
48
50
  };
49
51
  // for lipSync
@@ -241,7 +243,7 @@ export const generateBeatAudio = async (index, context, args) => {
241
243
  const fileName = MulmoStudioContextMethods.getFileName(context);
242
244
  const audioDirPath = MulmoStudioContextMethods.getAudioDirPath(context);
243
245
  const outDirPath = MulmoStudioContextMethods.getOutDirPath(context);
244
- const audioSegmentDirPath = resolveDirPath(audioDirPath, fileName);
246
+ const audioSegmentDirPath = context.fileDirs.grouped ? audioDirPath : resolveDirPath(audioDirPath, fileName);
245
247
  mkdir(outDirPath);
246
248
  mkdir(audioSegmentDirPath);
247
249
  const config = settings2GraphAIConfig(settings);
@@ -279,8 +281,10 @@ export const audio = async (context, args) => {
279
281
  const audioDirPath = MulmoStudioContextMethods.getAudioDirPath(context);
280
282
  const outDirPath = MulmoStudioContextMethods.getOutDirPath(context);
281
283
  const audioArtifactFilePath = getAudioArtifactFilePath(context);
282
- const audioSegmentDirPath = resolveDirPath(audioDirPath, fileName);
283
- const audioCombinedFilePath = getAudioFilePath(audioDirPath, fileName, fileName, context.lang);
284
+ const audioSegmentDirPath = context.fileDirs.grouped ? audioDirPath : resolveDirPath(audioDirPath, fileName);
285
+ const audioCombinedFilePath = context.fileDirs.grouped
286
+ ? getGroupedAudioFilePath(audioDirPath, fileName, context.lang)
287
+ : getAudioFilePath(audioDirPath, fileName, fileName, context.lang);
284
288
  const outputStudioFilePath = getOutputStudioFilePath(outDirPath, fileName);
285
289
  mkdir(outDirPath);
286
290
  mkdir(audioSegmentDirPath);
@@ -61,8 +61,8 @@ export const mulmoViewerBundle = async (context, options = {}) => {
61
61
  const baseDir = context.fileDirs.baseDirPath;
62
62
  const filename = context.studio.filename;
63
63
  mkdir(outDir);
64
- // Bundle directory: output/<script_name>/
65
- const bundleDir = path.resolve(outDir, filename);
64
+ // Bundle directory: when grouped, outDir is already output/<script_name>/
65
+ const bundleDir = context.fileDirs.grouped ? outDir : path.resolve(outDir, filename);
66
66
  mkdir(bundleDir);
67
67
  const zipper = skipZip ? undefined : new ZipBuilder(path.resolve(bundleDir, zipFileName));
68
68
  // text
@@ -1,6 +1,6 @@
1
1
  import { GraphAILogger } from "graphai";
2
2
  import { MulmoPresentationStyleMethods, MulmoStudioContextMethods, MulmoBeatMethods, MulmoMediaSourceMethods } from "../methods/index.js";
3
- import { getBeatPngImagePath, getBeatMoviePaths, getAudioFilePath } from "../utils/file.js";
3
+ import { getBeatPngImagePath, getBeatMoviePaths, getAudioFilePath, getGroupedAudioFilePath } from "../utils/file.js";
4
4
  import { imagePrompt, htmlImageSystemPrompt } from "../utils/prompt.js";
5
5
  import { renderHTMLToImage } from "../utils/html_render.js";
6
6
  import { beatId } from "../utils/utils.js";
@@ -54,8 +54,10 @@ export const imagePreprocessAgent = async (namedInputs) => {
54
54
  returnValue.bgmFile = MulmoMediaSourceMethods.resolve(context.studio.script.audioParams.bgm, context);
55
55
  const folderName = MulmoStudioContextMethods.getFileName(context);
56
56
  const audioDirPath = MulmoStudioContextMethods.getAudioDirPath(context);
57
- const fileName = `${beatId(beat.id, index)}_trimmed.mp3`;
58
- returnValue.audioFile = getAudioFilePath(audioDirPath, folderName, fileName);
57
+ const trimmedName = `${beatId(beat.id, index)}_trimmed`;
58
+ returnValue.audioFile = context.fileDirs.grouped
59
+ ? getGroupedAudioFilePath(audioDirPath, trimmedName)
60
+ : getAudioFilePath(audioDirPath, folderName, trimmedName);
59
61
  }
60
62
  else {
61
63
  // Audio file will be set from the beat's audio file when available
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<import("yargs").Omit<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const commonOptions: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ g: boolean;
10
12
  } & {
11
13
  backup: boolean;
12
14
  } & {
package/lib/cli/common.js CHANGED
@@ -25,6 +25,12 @@ export const commonOptions = (yargs) => {
25
25
  describe: "Force regenerate",
26
26
  type: "boolean",
27
27
  default: false,
28
+ })
29
+ .option("g", {
30
+ alias: "grouped",
31
+ describe: "Output all files under output/<basename>/ directory",
32
+ type: "boolean",
33
+ default: false,
28
34
  })
29
35
  .option("backup", {
30
36
  describe: "create backup media file",
@@ -10,5 +10,6 @@ export declare const getFileObject: (args: {
10
10
  presentationStyle?: string;
11
11
  file: string;
12
12
  nodeModuleRootPath?: string;
13
+ grouped?: boolean;
13
14
  }) => FileObject;
14
15
  export declare const initializeContext: (argv: CliArgs<InitOptions>, raiseError?: boolean) => Promise<MulmoStudioContext | null>;
@@ -30,9 +30,9 @@ export const setGraphAILogger = (verbose, logValues) => {
30
30
  }
31
31
  };
32
32
  export const getFileObject = (args) => {
33
- const { basedir, outdir, imagedir, audiodir, file, presentationStyle, nodeModuleRootPath } = args;
33
+ const { basedir, outdir, imagedir, audiodir, file, presentationStyle, nodeModuleRootPath, grouped = false } = args;
34
34
  const baseDirPath = getBaseDirPath(basedir);
35
- const outDirPath = getFullPath(baseDirPath, outdir ?? outDirName);
35
+ const baseOutDirPath = getFullPath(baseDirPath, outdir ?? outDirName);
36
36
  const { fileOrUrl, fileName } = (() => {
37
37
  if (file === "__clipboard") {
38
38
  // We generate a new unique script file from clipboard text in the output directory
@@ -40,8 +40,8 @@ export const getFileObject = (args) => {
40
40
  const clipboardText = clipboardy.readSync();
41
41
  const json = JSON.parse(clipboardText);
42
42
  const formattedText = JSON.stringify(json, null, 2);
43
- const resolvedFilePath = resolveDirPath(outDirPath, `${generatedFileName}.json`);
44
- mkdir(outDirPath);
43
+ const resolvedFilePath = resolveDirPath(baseOutDirPath, `${generatedFileName}.json`);
44
+ mkdir(baseOutDirPath);
45
45
  fs.writeFileSync(resolvedFilePath, formattedText, "utf8");
46
46
  return { fileOrUrl: resolvedFilePath, fileName: generatedFileName };
47
47
  }
@@ -49,6 +49,7 @@ export const getFileObject = (args) => {
49
49
  const parsedFileName = path.parse(resolvedFileOrUrl).name;
50
50
  return { fileOrUrl: resolvedFileOrUrl, fileName: parsedFileName };
51
51
  })();
52
+ const outDirPath = grouped ? getFullPath(baseOutDirPath, fileName) : baseOutDirPath;
52
53
  const isHttpPath = isHttp(fileOrUrl);
53
54
  const mulmoFilePath = isHttpPath ? "" : getFullPath(baseDirPath, fileOrUrl);
54
55
  const mulmoFileDirPath = path.dirname(isHttpPath ? baseDirPath : mulmoFilePath);
@@ -71,6 +72,7 @@ export const getFileObject = (args) => {
71
72
  presentationStylePath,
72
73
  fileName,
73
74
  nodeModuleRootPath,
75
+ grouped,
74
76
  };
75
77
  };
76
78
  export const initializeContext = async (argv, raiseError = false) => {
@@ -81,6 +83,7 @@ export const initializeContext = async (argv, raiseError = false) => {
81
83
  audiodir: argv.a,
82
84
  presentationStyle: argv.p,
83
85
  file: argv.file ?? "",
86
+ grouped: Boolean(argv.g),
84
87
  });
85
88
  setGraphAILogger(Boolean(argv.v), { files });
86
89
  return await initializeContextFromFiles(files, raiseError, Boolean(argv.f), Boolean(argv.backup), argv.c, argv.l);
@@ -44,6 +44,9 @@ export const MulmoStudioContextMethods = {
44
44
  },
45
45
  getImageProjectDirPath(context) {
46
46
  const imageDirPath = MulmoStudioContextMethods.getImageDirPath(context);
47
+ if (context.fileDirs.grouped) {
48
+ return imageDirPath;
49
+ }
47
50
  return `${imageDirPath}/${context.studio.filename}`;
48
51
  },
49
52
  getOutDirPath(context) {
@@ -128,6 +128,7 @@ export interface FileObject {
128
128
  outputMultilingualFilePath: string;
129
129
  presentationStylePath: string | undefined;
130
130
  fileName: string;
131
+ grouped: boolean;
131
132
  }
132
133
  export type InitOptions = {
133
134
  b?: string;
@@ -138,6 +139,7 @@ export type InitOptions = {
138
139
  l?: string;
139
140
  c?: string;
140
141
  p?: string;
142
+ g?: boolean;
141
143
  };
142
144
  export type PublicAPIArgs = {
143
145
  settings?: Record<string, string>;
@@ -20,7 +20,9 @@ export declare const fetchMulmoScriptFile: (url: string) => Promise<{
20
20
  export declare const getOutputStudioFilePath: (outDirPath: string, fileName: string) => string;
21
21
  export declare const getOutputMultilingualFilePath: (outDirPath: string, fileName: string) => string;
22
22
  export declare const resolveDirPath: (dirPath: string, studioFileName: string) => string;
23
+ export declare const formatAudioFileName: (name: string, lang?: string) => string;
23
24
  export declare const getAudioFilePath: (audioDirPath: string, dirName: string, fileName: string, lang?: string) => string;
25
+ export declare const getGroupedAudioFilePath: (audioDirPath: string, fileName: string, lang?: string) => string;
24
26
  export declare const getAudioArtifactFilePath: (context: MulmoStudioContext) => string;
25
27
  export declare const getOutputVideoFilePath: (outDirPath: string, fileName: string, lang?: string, caption?: string) => string;
26
28
  export declare const imageSuffix = "p";
package/lib/utils/file.js CHANGED
@@ -68,11 +68,15 @@ export const resolveDirPath = (dirPath, studioFileName) => {
68
68
  return path.resolve(dirPath, studioFileName);
69
69
  };
70
70
  // audio
71
+ export const formatAudioFileName = (name, lang) => {
72
+ const suffix = lang ? `_${lang}` : "";
73
+ return `${name}${suffix}.mp3`;
74
+ };
71
75
  export const getAudioFilePath = (audioDirPath, dirName, fileName, lang) => {
72
- if (lang) {
73
- return path.resolve(audioDirPath, dirName, `${fileName}_${lang}.mp3`);
74
- }
75
- return path.resolve(audioDirPath, dirName, fileName + ".mp3");
76
+ return path.resolve(audioDirPath, dirName, formatAudioFileName(fileName, lang));
77
+ };
78
+ export const getGroupedAudioFilePath = (audioDirPath, fileName, lang) => {
79
+ return path.resolve(audioDirPath, formatAudioFileName(fileName, lang));
76
80
  };
77
81
  export const getAudioArtifactFilePath = (context) => {
78
82
  const suffix = context.lang ? `_${context.lang}` : "";
@@ -4,21 +4,21 @@ import { type PartialMulmoScript } from "../tools/complete_script.js";
4
4
  * Returns the first found path, or null if not found.
5
5
  */
6
6
  export declare const findConfigFile: (baseDirPath: string) => string | null;
7
- /**
8
- * Resolve all kind:"path" references in config relative to the config file directory.
9
- */
10
- export declare const resolveConfigPaths: (config: PartialMulmoScript, configDirPath: string) => PartialMulmoScript;
11
7
  export type MulmoConfigResult = {
12
8
  defaults: PartialMulmoScript;
13
9
  override: PartialMulmoScript | null;
14
10
  };
15
11
  /**
16
12
  * Load mulmo.config.json from baseDirPath or home directory.
17
- * Resolves kind:"path" entries relative to the config file location.
18
13
  * Returns { defaults, override } or null if no config file is found.
19
14
  *
20
15
  * - defaults: applied as low-priority base (script wins)
21
16
  * - override: applied after script merge (wins over script)
17
+ *
18
+ * Note: kind:"path" entries are NOT resolved here.
19
+ * They remain as relative paths and are resolved at runtime
20
+ * relative to the script file directory (mulmoFileDirPath),
21
+ * consistent with all other path resolution in MulmoScript.
22
22
  */
23
23
  export declare const loadMulmoConfig: (baseDirPath: string) => MulmoConfigResult | null;
24
24
  /**
@@ -3,7 +3,6 @@ import path from "path";
3
3
  import os from "os";
4
4
  import { GraphAILogger } from "graphai";
5
5
  import { mergeScripts } from "../tools/complete_script.js";
6
- import { getFullPath } from "./file.js";
7
6
  const CONFIG_FILE_NAME = "mulmo.config.json";
8
7
  /**
9
8
  * Search for mulmo.config.json in CWD → ~ order.
@@ -18,48 +17,17 @@ export const findConfigFile = (baseDirPath) => {
18
17
  }
19
18
  return null;
20
19
  };
21
- /**
22
- * Resolve kind:"path" entries in config to absolute paths relative to config file location.
23
- */
24
- const resolveMediaSourcePath = (source, configDirPath) => {
25
- if (source.kind === "path" && typeof source.path === "string") {
26
- return { ...source, path: getFullPath(configDirPath, source.path) };
27
- }
28
- return source;
29
- };
30
- /**
31
- * Immutably resolve a nested kind:"path" source at the given key path.
32
- * e.g. ["audioParams", "bgm"] resolves config.audioParams.bgm
33
- */
34
- const resolveNestedPath = (obj, keys, configDirPath) => {
35
- const [head, ...tail] = keys;
36
- const child = obj[head];
37
- if (!child || typeof child !== "object") {
38
- return obj;
39
- }
40
- const childObj = child;
41
- const resolved = tail.length === 0 ? resolveMediaSourcePath(childObj, configDirPath) : resolveNestedPath(childObj, tail, configDirPath);
42
- return resolved === child ? obj : { ...obj, [head]: resolved };
43
- };
44
- /** Key paths to kind:"path" sources that need resolution */
45
- const MEDIA_SOURCE_PATHS = [
46
- ["audioParams", "bgm"],
47
- ["slideParams", "branding", "logo", "source"],
48
- ["slideParams", "branding", "backgroundImage", "source"],
49
- ];
50
- /**
51
- * Resolve all kind:"path" references in config relative to the config file directory.
52
- */
53
- export const resolveConfigPaths = (config, configDirPath) => {
54
- return MEDIA_SOURCE_PATHS.reduce((acc, keys) => resolveNestedPath(acc, keys, configDirPath), config);
55
- };
56
20
  /**
57
21
  * Load mulmo.config.json from baseDirPath or home directory.
58
- * Resolves kind:"path" entries relative to the config file location.
59
22
  * Returns { defaults, override } or null if no config file is found.
60
23
  *
61
24
  * - defaults: applied as low-priority base (script wins)
62
25
  * - override: applied after script merge (wins over script)
26
+ *
27
+ * Note: kind:"path" entries are NOT resolved here.
28
+ * They remain as relative paths and are resolved at runtime
29
+ * relative to the script file directory (mulmoFileDirPath),
30
+ * consistent with all other path resolution in MulmoScript.
63
31
  */
64
32
  export const loadMulmoConfig = (baseDirPath) => {
65
33
  const configPath = findConfigFile(baseDirPath);
@@ -69,10 +37,9 @@ export const loadMulmoConfig = (baseDirPath) => {
69
37
  try {
70
38
  const content = readFileSync(configPath, "utf-8");
71
39
  const raw = JSON.parse(content);
72
- const configDirPath = path.dirname(configPath);
73
40
  const { override: rawOverride, ...rest } = raw;
74
- const defaults = resolveConfigPaths(rest, configDirPath);
75
- const override = rawOverride ? resolveConfigPaths(rawOverride, configDirPath) : null;
41
+ const defaults = rest;
42
+ const override = rawOverride ? rawOverride : null;
76
43
  return { defaults, override };
77
44
  }
78
45
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",