mulmocast 2.3.1 → 2.4.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 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
@@ -43,5 +43,6 @@
43
43
  </head>
44
44
  <body class="bg-white text-gray-800 h-full flex flex-col">
45
45
  ${html_body}
46
+ ${user_script}
46
47
  </body>
47
48
  </html>
@@ -0,0 +1,259 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <script src="https://cdn.tailwindcss.com"></script>
7
+ <style>
8
+ /* Disable all CSS animations/transitions for deterministic frame-based rendering */
9
+ *, *::before, *::after {
10
+ animation-play-state: paused !important;
11
+ transition: none !important;
12
+ }
13
+ ${custom_style}
14
+ </style>
15
+ </head>
16
+ <body class="bg-white text-gray-800 h-full flex flex-col">
17
+ ${html_body}
18
+
19
+ <script>
20
+ // === MulmoCast Animation Helpers ===
21
+
22
+ /**
23
+ * Easing functions for non-linear interpolation.
24
+ */
25
+ const Easing = {
26
+ linear: (t) => t,
27
+ easeIn: (t) => t * t,
28
+ easeOut: (t) => 1 - (1 - t) * (1 - t),
29
+ easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
30
+ };
31
+
32
+ /**
33
+ * Interpolation with clamping and optional easing.
34
+ *
35
+ * @param {number} value - Current value (typically frame number)
36
+ * @param {Object} opts - { input: { inMin, inMax }, output: { outMin, outMax }, easing?: string | function }
37
+ * @returns {number} Interpolated and clamped value
38
+ *
39
+ * @example
40
+ * interpolate(frame, { input: { inMin: 0, inMax: 30 }, output: { outMin: 0, outMax: 1 } })
41
+ * interpolate(frame, { input: { inMin: 0, inMax: 30 }, output: { outMin: 0, outMax: 1 }, easing: 'easeOut' })
42
+ */
43
+ function interpolate(value, opts) {
44
+ const { inMin, inMax } = opts.input;
45
+ const { outMin, outMax } = opts.output;
46
+ if (inMax === inMin) {
47
+ return outMin;
48
+ }
49
+ const easing = !opts.easing ? Easing.linear
50
+ : typeof opts.easing === 'function' ? opts.easing
51
+ : Easing[opts.easing] || Easing.linear;
52
+ const progress = Math.max(0, Math.min(1, (value - inMin) / (inMax - inMin)));
53
+ return outMin + easing(progress) * (outMax - outMin);
54
+ }
55
+
56
+ // === MulmoAnimation Helper Class ===
57
+
58
+ const TRANSFORM_PROPS = { translateX: 'px', translateY: 'px', scale: '', rotate: 'deg' };
59
+ const SVG_PROPS = ['r', 'cx', 'cy', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'rx', 'ry',
60
+ 'width', 'height', 'stroke-width', 'stroke-dashoffset', 'stroke-dasharray', 'opacity'];
61
+
62
+ function MulmoAnimation() {
63
+ this._entries = [];
64
+ }
65
+
66
+ /**
67
+ * Register a property animation on a single element.
68
+ * @param {string} selector - CSS selector (e.g. '#title')
69
+ * @param {Object} props - { opacity: [0, 1], translateY: [30, 0], width: [0, 80, '%'] }
70
+ * @param {Object} opts - { start, end, easing } (start/end in seconds)
71
+ */
72
+ MulmoAnimation.prototype.animate = function(selector, props, opts) {
73
+ this._entries.push({ kind: 'animate', selector, props, opts: opts || {} });
74
+ return this;
75
+ };
76
+
77
+ /**
78
+ * Stagger animation across numbered elements.
79
+ * Selector must contain {i} placeholder (e.g. '#item{i}').
80
+ * @param {string} selector - e.g. '#item{i}'
81
+ * @param {number} count - number of elements (0-indexed)
82
+ * @param {Object} props - same as animate()
83
+ * @param {Object} opts - { start, stagger, duration, easing }
84
+ */
85
+ MulmoAnimation.prototype.stagger = function(selector, count, props, opts) {
86
+ this._entries.push({ kind: 'stagger', selector, count, props, opts: opts || {} });
87
+ return this;
88
+ };
89
+
90
+ /**
91
+ * Typewriter effect — reveal text character by character.
92
+ * @param {string} selector - target element selector
93
+ * @param {string} text - full text to reveal
94
+ * @param {Object} opts - { start, end }
95
+ */
96
+ MulmoAnimation.prototype.typewriter = function(selector, text, opts) {
97
+ this._entries.push({ kind: 'typewriter', selector, text, opts: opts || {} });
98
+ return this;
99
+ };
100
+
101
+ /**
102
+ * Animated counter — interpolate a number and display with optional prefix/suffix.
103
+ * @param {string} selector - target element selector
104
+ * @param {[number, number]} range - [from, to]
105
+ * @param {Object} opts - { start, end, prefix, suffix, decimals }
106
+ */
107
+ MulmoAnimation.prototype.counter = function(selector, range, opts) {
108
+ this._entries.push({ kind: 'counter', selector, range, opts: opts || {} });
109
+ return this;
110
+ };
111
+
112
+ /**
113
+ * Code reveal — show lines of code one by one (line-level typewriter).
114
+ * @param {string} selector - target element selector
115
+ * @param {string[]} lines - array of code lines
116
+ * @param {Object} opts - { start, end }
117
+ */
118
+ MulmoAnimation.prototype.codeReveal = function(selector, lines, opts) {
119
+ this._entries.push({ kind: 'codeReveal', selector, lines, opts: opts || {} });
120
+ return this;
121
+ };
122
+
123
+ /**
124
+ * Blink — periodic show/hide toggle (e.g. cursor blinking).
125
+ * @param {string} selector - target element selector
126
+ * @param {Object} opts - { interval } (half-cycle seconds, default 0.5)
127
+ */
128
+ MulmoAnimation.prototype.blink = function(selector, opts) {
129
+ this._entries.push({ kind: 'blink', selector, opts: opts || {} });
130
+ return this;
131
+ };
132
+
133
+ /** Resolve easing name string or function to an easing function */
134
+ MulmoAnimation.prototype._resolveEasing = function(e) {
135
+ if (!e) return Easing.linear;
136
+ if (typeof e === 'function') return e;
137
+ return Easing[e] || Easing.linear;
138
+ };
139
+
140
+ /** Apply props to element at a given progress (0-1) with easing */
141
+ MulmoAnimation.prototype._applyProps = function(el, props, progress, easingFn) {
142
+ if (!el) return;
143
+ const transforms = [];
144
+ Object.keys(props).forEach((prop) => {
145
+ const spec = props[prop];
146
+ const from = spec[0], to = spec[1];
147
+ const unit = (spec.length > 2) ? spec[2] : null;
148
+ const val = from + easingFn(progress) * (to - from);
149
+
150
+ if (TRANSFORM_PROPS.hasOwnProperty(prop)) {
151
+ const tUnit = unit || TRANSFORM_PROPS[prop];
152
+ transforms.push(prop === 'scale' ? 'scale(' + val + ')' : prop + '(' + val + tUnit + ')');
153
+ } else if (el instanceof SVGElement && SVG_PROPS.indexOf(prop) !== -1) {
154
+ el.setAttribute(prop, val);
155
+ } else if (prop === 'opacity') {
156
+ el.style.opacity = val;
157
+ } else {
158
+ const cssUnit = unit || 'px';
159
+ el.style[prop] = val + cssUnit;
160
+ }
161
+ });
162
+ if (transforms.length > 0) {
163
+ el.style.transform = transforms.join(' ');
164
+ }
165
+ };
166
+
167
+ /**
168
+ * Update all registered animations for the given frame.
169
+ * @param {number} frame - current frame number
170
+ * @param {number} fps - frames per second
171
+ */
172
+ MulmoAnimation.prototype.update = function(frame, fps) {
173
+ this._entries.forEach((entry) => {
174
+ const opts = entry.opts;
175
+ const easingFn = this._resolveEasing(opts.easing);
176
+
177
+ if (entry.kind === 'animate') {
178
+ const startFrame = (opts.start || 0) * fps;
179
+ const endFrame = (opts.end || 0) * fps;
180
+ const progress = Math.max(0, Math.min(1, endFrame === startFrame ? 1 : (frame - startFrame) / (endFrame - startFrame)));
181
+ const el = document.querySelector(entry.selector);
182
+ this._applyProps(el, entry.props, progress, easingFn);
183
+
184
+ } else if (entry.kind === 'stagger') {
185
+ const baseStart = (opts.start || 0) * fps;
186
+ const staggerDelay = (opts.stagger || 0.2) * fps;
187
+ const dur = (opts.duration || 0.5) * fps;
188
+ for (let j = 0; j < entry.count; j++) {
189
+ const sel = entry.selector.replace(/\{i\}/g, j);
190
+ const sEl = document.querySelector(sel);
191
+ const sStart = baseStart + j * staggerDelay;
192
+ const sEnd = sStart + dur;
193
+ const sProgress = Math.max(0, Math.min(1, sEnd === sStart ? 1 : (frame - sStart) / (sEnd - sStart)));
194
+ this._applyProps(sEl, entry.props, sProgress, easingFn);
195
+ }
196
+
197
+ } else if (entry.kind === 'typewriter') {
198
+ const twStart = (opts.start || 0) * fps;
199
+ const twEnd = (opts.end || 0) * fps;
200
+ const twProgress = Math.max(0, Math.min(1, twEnd === twStart ? 1 : (frame - twStart) / (twEnd - twStart)));
201
+ const charCount = Math.floor(twProgress * entry.text.length);
202
+ const twEl = document.querySelector(entry.selector);
203
+ if (twEl) twEl.textContent = entry.text.substring(0, charCount);
204
+
205
+ } else if (entry.kind === 'counter') {
206
+ const cStart = (opts.start || 0) * fps;
207
+ const cEnd = (opts.end || 0) * fps;
208
+ const cProgress = Math.max(0, Math.min(1, cEnd === cStart ? 1 : (frame - cStart) / (cEnd - cStart)));
209
+ const cVal = entry.range[0] + easingFn(cProgress) * (entry.range[1] - entry.range[0]);
210
+ const decimals = opts.decimals || 0;
211
+ const display = (opts.prefix || '') + cVal.toFixed(decimals) + (opts.suffix || '');
212
+ const cEl = document.querySelector(entry.selector);
213
+ if (cEl) cEl.textContent = display;
214
+
215
+ } else if (entry.kind === 'codeReveal') {
216
+ const crStart = (opts.start || 0) * fps;
217
+ const crEnd = (opts.end || 0) * fps;
218
+ const crProgress = Math.max(0, Math.min(1, crEnd === crStart ? 1 : (frame - crStart) / (crEnd - crStart)));
219
+ const lineCount = Math.floor(crProgress * entry.lines.length);
220
+ const crEl = document.querySelector(entry.selector);
221
+ if (crEl) crEl.textContent = entry.lines.slice(0, lineCount).join('\n');
222
+
223
+ } else if (entry.kind === 'blink') {
224
+ const interval_s = opts.interval || 0.5;
225
+ const blinkEl = document.querySelector(entry.selector);
226
+ if (blinkEl) {
227
+ const cycle = (frame / fps) / interval_s;
228
+ blinkEl.style.opacity = (Math.floor(cycle) % 2 === 0) ? 1 : 0;
229
+ }
230
+ }
231
+ });
232
+ };
233
+
234
+ // === MulmoCast Frame State (updated by Puppeteer per frame) ===
235
+ window.__MULMO = {
236
+ frame: 0,
237
+ totalFrames: ${totalFrames},
238
+ fps: ${fps},
239
+ };
240
+ </script>
241
+
242
+ ${user_script}
243
+
244
+ <script>
245
+ // Auto-render: if MulmoAnimation is used but render() is not defined, generate it
246
+ if (typeof render !== 'function' && typeof animation !== 'undefined' && animation instanceof MulmoAnimation) {
247
+ window.render = function(frame, totalFrames, fps) { animation.update(frame, fps); };
248
+ }
249
+
250
+ // Initial render (frame 0)
251
+ if (typeof render === 'function') {
252
+ const result = render(0, window.__MULMO.totalFrames, window.__MULMO.fps);
253
+ if (result && typeof result.then === 'function') {
254
+ result.catch(console.error);
255
+ }
256
+ }
257
+ </script>
258
+ </body>
259
+ </html>
@@ -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, getGroupedAudioFilePath } from "../utils/file.js";
3
+ import { getBeatPngImagePath, getBeatMoviePaths, getBeatAnimatedVideoPath, 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";
@@ -72,6 +72,24 @@ export const imagePreprocessAgent = async (namedInputs) => {
72
72
  const markdown = plugin.markdown ? plugin.markdown({ beat, context, imagePath, ...htmlStyle(context, beat) }) : undefined;
73
73
  const html = plugin.html ? await plugin.html({ beat, context, imagePath, ...htmlStyle(context, beat) }) : undefined;
74
74
  const isTypeMovie = beat.image.type === "movie";
75
+ const isAnimatedHtml = MulmoBeatMethods.isAnimatedHtmlTailwind(beat);
76
+ // animation and moviePrompt cannot be used together
77
+ if (isAnimatedHtml && beat.moviePrompt) {
78
+ throw new Error("html_tailwind animation and moviePrompt cannot be used together on the same beat. Use either animation or moviePrompt, not both.");
79
+ }
80
+ if (isAnimatedHtml) {
81
+ const animatedVideoPath = getBeatAnimatedVideoPath(context, index);
82
+ // ImagePluginPreprocessAgentResponse
83
+ return {
84
+ ...returnValue,
85
+ imagePath, // for thumbnail extraction
86
+ movieFile: animatedVideoPath, // .mp4 path for the pipeline
87
+ imageFromMovie: true, // triggers extractImageFromMovie
88
+ referenceImageForMovie: pluginPath,
89
+ markdown,
90
+ html,
91
+ };
92
+ }
75
93
  // undefined prompt indicates that image generation is not needed
76
94
  // ImagePluginPreprocessAgentResponse
77
95
  return {
@@ -99,9 +117,12 @@ export const imagePluginAgent = async (namedInputs) => {
99
117
  const { context, beat, index, imageRefs } = namedInputs;
100
118
  const { imagePath } = getBeatPngImagePath(context, index);
101
119
  const plugin = MulmoBeatMethods.getPlugin(beat);
120
+ // For animated html_tailwind, use the .mp4 path so the plugin writes video there
121
+ const isAnimatedHtml = MulmoBeatMethods.isAnimatedHtmlTailwind(beat);
122
+ const effectiveImagePath = isAnimatedHtml ? getBeatAnimatedVideoPath(context, index) : imagePath;
102
123
  try {
103
124
  MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, true);
104
- const processorParams = { beat, context, imagePath, imageRefs, ...htmlStyle(context, beat) };
125
+ const processorParams = { beat, context, imagePath: effectiveImagePath, imageRefs, ...htmlStyle(context, beat) };
105
126
  await plugin.process(processorParams);
106
127
  MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, false);
107
128
  }
@@ -190,7 +190,7 @@ export const beat_graph_data = {
190
190
  return await extractImageFromMovie(namedInputs.movieFile, namedInputs.imageFile);
191
191
  },
192
192
  inputs: {
193
- onComplete: [":movieGenerator"], // to wait for movieGenerator to finish
193
+ onComplete: [":movieGenerator", ":imagePlugin"], // :imagePlugin for animated html_tailwind video generation
194
194
  imageFile: ":preprocessor.imagePath",
195
195
  movieFile: ":preprocessor.movieFile",
196
196
  },
@@ -219,7 +219,7 @@ export const beat_graph_data = {
219
219
  }
220
220
  },
221
221
  inputs: {
222
- onComplete: [":movieGenerator", ":htmlImageGenerator", ":soundEffectGenerator"],
222
+ onComplete: [":movieGenerator", ":htmlImageGenerator", ":soundEffectGenerator", ":imagePlugin"], // :imagePlugin for animated html_tailwind video generation
223
223
  movieFile: ":preprocessor.movieFile",
224
224
  imageFile: ":preprocessor.imagePath",
225
225
  soundEffectFile: ":preprocessor.soundEffectFile",
@@ -2,6 +2,7 @@ import { assert, GraphAILogger } from "graphai";
2
2
  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
+ import { MulmoBeatMethods } from "../methods/index.js";
5
6
  import { userAssert } from "../utils/utils.js";
6
7
  import { getAudioInputIdsError } from "../utils/error_cause.js";
7
8
  const getMovieDuration = async (context, beat) => {
@@ -13,6 +14,10 @@ const getMovieDuration = async (context, beat) => {
13
14
  return { duration: duration / speed, hasAudio };
14
15
  }
15
16
  }
17
+ // Animated html_tailwind beats with explicit duration act as movie-like for voice_over grouping
18
+ if (MulmoBeatMethods.isAnimatedHtmlTailwind(beat) && beat.duration !== undefined) {
19
+ return { duration: beat.duration, hasAudio: false };
20
+ }
16
21
  return { duration: 0, hasAudio: false };
17
22
  };
18
23
  export const getPadding = (context, beat, index) => {