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 +99 -43
- package/assets/html/tailwind.html +1 -0
- package/assets/html/tailwind_animated.html +259 -0
- package/lib/actions/image_agents.js +23 -2
- package/lib/actions/images.js +2 -2
- package/lib/agents/combine_audio_files_agent.js +5 -0
- package/lib/methods/mulmo_beat.d.ts +7 -0
- package/lib/methods/mulmo_beat.js +18 -0
- package/lib/types/schema.d.ts +23 -0
- package/lib/types/schema.js +10 -0
- package/lib/utils/context.d.ts +8 -0
- package/lib/utils/ffmpeg_utils.d.ts +10 -0
- package/lib/utils/ffmpeg_utils.js +27 -0
- package/lib/utils/file.d.ts +1 -0
- package/lib/utils/file.js +6 -0
- package/lib/utils/html_render.d.ts +11 -0
- package/lib/utils/html_render.js +84 -34
- package/lib/utils/image_plugins/html_tailwind.js +78 -6
- package/lib/utils/mulmo_config.d.ts +5 -5
- package/lib/utils/mulmo_config.js +7 -40
- package/package.json +3 -3
- package/scripts/test/test_html_animation.json +563 -0
- package/scripts/test/test_vocab_animation.json +226 -0
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.
|
|
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
|
|
533
|
-
-v, --verbose
|
|
534
|
-
-h, --help
|
|
535
|
-
-o, --outdir
|
|
536
|
-
-b, --basedir
|
|
537
|
-
-l, --lang
|
|
538
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
701
|
-
mulmo tool
|
|
702
|
-
mulmo tool
|
|
703
|
-
mulmo tool
|
|
704
|
-
mulmo tool
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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
|
-
|
|
778
|
-
|
|
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
|
|
842
|
+
Complete MulmoScript with schema defaults and optional style
|
|
796
843
|
|
|
797
844
|
Positionals:
|
|
798
|
-
file Input beats file path (JSON)
|
|
845
|
+
file Input beats file path (JSON) [string] [required]
|
|
799
846
|
|
|
800
847
|
Options:
|
|
801
|
-
--version Show version number
|
|
802
|
-
-v, --verbose verbose log
|
|
803
|
-
-h, --help Show help
|
|
804
|
-
-o, --output Output file path (default: <file>_completed.json)
|
|
805
|
-
-t, --template Template name to apply
|
|
806
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -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
|
}
|
package/lib/actions/images.js
CHANGED
|
@@ -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"], //
|
|
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) => {
|