mulmocast 2.1.0 → 2.1.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.
@@ -1,7 +1,7 @@
1
- import { MulmoStudioContext, MulmoBeat, MulmoTransition, MulmoCanvasDimension, MulmoFillOption } from "../types/index.js";
1
+ import { MulmoStudioContext, MulmoBeat, MulmoTransition, MulmoCanvasDimension, MulmoFillOption, MulmoVideoFilter } from "../types/index.js";
2
2
  import { FfmpegContext } from "../utils/ffmpeg_utils.js";
3
3
  type VideoId = string | undefined;
4
- export declare const getVideoPart: (inputIndex: number, isMovie: boolean, duration: number, canvasInfo: MulmoCanvasDimension, fillOption: MulmoFillOption, speed: number) => {
4
+ export declare const getVideoPart: (inputIndex: number, isMovie: boolean, duration: number, canvasInfo: MulmoCanvasDimension, fillOption: MulmoFillOption, speed: number, filters?: MulmoVideoFilter[]) => {
5
5
  videoId: string;
6
6
  videoPart: string;
7
7
  };
@@ -1,13 +1,14 @@
1
1
  import { GraphAILogger, assert } from "graphai";
2
- import { mulmoFillOptionSchema } from "../types/index.js";
2
+ import { mulmoFillOptionSchema, } from "../types/index.js";
3
3
  import { MulmoPresentationStyleMethods } from "../methods/index.js";
4
4
  import { getAudioArtifactFilePath, getOutputVideoFilePath, writingMessage, isFile } from "../utils/file.js";
5
5
  import { createVideoFileError, createVideoSourceError } from "../utils/error_cause.js";
6
6
  import { FfmpegContextAddInput, FfmpegContextInit, FfmpegContextPushFormattedAudio, FfmpegContextGenerateOutput, } from "../utils/ffmpeg_utils.js";
7
7
  import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
8
+ import { convertVideoFilterToFFmpeg } from "../utils/video_filter.js";
8
9
  // const isMac = process.platform === "darwin";
9
10
  const videoCodec = "libx264"; // "h264_videotoolbox" (macOS only) is too noisy
10
- export const getVideoPart = (inputIndex, isMovie, duration, canvasInfo, fillOption, speed) => {
11
+ export const getVideoPart = (inputIndex, isMovie, duration, canvasInfo, fillOption, speed, filters) => {
11
12
  const videoId = `v${inputIndex}`;
12
13
  const videoFilters = [];
13
14
  // Handle different media types
@@ -41,6 +42,12 @@ export const getVideoPart = (inputIndex, isMovie, duration, canvasInfo, fillOpti
41
42
  `pad=${canvasInfo.width}:${canvasInfo.height}:(ow-iw)/2:(oh-ih)/2:color=black`);
42
43
  }
43
44
  videoFilters.push("setsar=1", "format=yuv420p");
45
+ // Apply custom video filters if specified
46
+ if (filters && filters.length > 0) {
47
+ filters.forEach((filter) => {
48
+ videoFilters.push(convertVideoFilterToFFmpeg(filter));
49
+ });
50
+ }
44
51
  return {
45
52
  videoId,
46
53
  videoPart: `[${inputIndex}:v]` + videoFilters.join(",") + `[${videoId}]`,
@@ -178,10 +185,13 @@ const addTransitionEffects = (ffmpegContext, captionedVideoId, context, transiti
178
185
  // Cannot apply wipe without first frame
179
186
  return prevVideoId;
180
187
  }
181
- // Use xfade offset instead of trimming to avoid framerate issues
182
- // The static frames are created with proper duration, use offset to start transition at the right time
183
- const prevBeatDuration = context.studio.beats[beatIndex - 1].duration ?? 0;
184
- const xfadeOffset = prevBeatDuration - duration;
188
+ // Calculate the actual duration of the static frames
189
+ // The _last frame was generated with the previous beat's actual duration
190
+ const prevStudioBeat = context.studio.beats[beatIndex - 1];
191
+ const prevBeatActualDuration = Math.max(prevStudioBeat.duration + getExtraPadding(context, beatIndex - 1), prevStudioBeat.movieDuration ?? 0);
192
+ // xfade offset must be non-negative and within the first video's duration
193
+ // Start the transition at the end of the first video minus the transition duration
194
+ const xfadeOffset = Math.max(0, prevBeatActualDuration - duration);
185
195
  // Apply xfade with explicit pixel format
186
196
  const xfadeOutputId = `${transitionVideoId}_xfade`;
187
197
  ffmpegContext.filterComplex.push(`[${transitionVideoId}]format=yuv420p[${transitionVideoId}_fmt]`);
@@ -345,7 +355,8 @@ export const createVideo = async (audioArtifactFilePath, outputVideoPath, contex
345
355
  studioBeat.movieFile ||
346
356
  MulmoPresentationStyleMethods.getImageType(context.presentationStyle, beat) === "movie");
347
357
  const speed = beat.movieParams?.speed ?? 1.0;
348
- const { videoId, videoPart } = getVideoPart(inputIndex, isMovie, duration, canvasInfo, getFillOption(context, beat), speed);
358
+ const filters = beat.movieParams?.filters;
359
+ const { videoId, videoPart } = getVideoPart(inputIndex, isMovie, duration, canvasInfo, getFillOption(context, beat), speed, filters);
349
360
  ffmpegContext.filterComplex.push(videoPart);
350
361
  // for transition
351
362
  const needFirst = needsFirstFrame[index]; // This beat has slidein
@@ -25,6 +25,152 @@ export declare const MulmoPresentationStyleMethods: {
25
25
  type: "fade" | "slideout_left" | "slideout_right" | "slideout_up" | "slideout_down" | "slidein_left" | "slidein_right" | "slidein_up" | "slidein_down" | "wipeleft" | "wiperight" | "wipeup" | "wipedown" | "wipetl" | "wipetr" | "wipebl" | "wipebr";
26
26
  duration: number;
27
27
  } | undefined;
28
+ filters?: ({
29
+ type: "mono";
30
+ } | {
31
+ type: "sepia";
32
+ } | {
33
+ type: "brightness_contrast";
34
+ brightness: number;
35
+ contrast: number;
36
+ } | {
37
+ type: "hue";
38
+ hue: number;
39
+ saturation: number;
40
+ brightness: number;
41
+ } | {
42
+ type: "colorbalance";
43
+ rs: number;
44
+ gs: number;
45
+ bs: number;
46
+ rm: number;
47
+ gm: number;
48
+ bm: number;
49
+ rh: number;
50
+ gh: number;
51
+ bh: number;
52
+ } | {
53
+ type: "vibrance";
54
+ intensity: number;
55
+ } | {
56
+ type: "negate";
57
+ negate_alpha: boolean;
58
+ } | {
59
+ type: "colorhold";
60
+ color: string;
61
+ similarity: number;
62
+ blend: number;
63
+ } | {
64
+ type: "colorkey";
65
+ color: string;
66
+ similarity: number;
67
+ blend: number;
68
+ } | {
69
+ type: "blur";
70
+ radius: number;
71
+ power: number;
72
+ } | {
73
+ type: "gblur";
74
+ sigma: number;
75
+ } | {
76
+ type: "avgblur";
77
+ sizeX: number;
78
+ sizeY: number;
79
+ } | {
80
+ type: "unsharp";
81
+ luma_msize_x: number;
82
+ luma_msize_y: number;
83
+ luma_amount: number;
84
+ chroma_msize_x: number;
85
+ chroma_msize_y: number;
86
+ chroma_amount: number;
87
+ } | {
88
+ type: "edgedetect";
89
+ low: number;
90
+ high: number;
91
+ mode: "wires" | "colormix" | "canny";
92
+ } | {
93
+ type: "sobel";
94
+ planes: number;
95
+ scale: number;
96
+ delta: number;
97
+ } | {
98
+ type: "emboss";
99
+ } | {
100
+ type: "glitch";
101
+ intensity: number;
102
+ style: "blend" | "noise";
103
+ } | {
104
+ type: "grain";
105
+ intensity: number;
106
+ } | {
107
+ type: "hflip";
108
+ } | {
109
+ type: "vflip";
110
+ } | {
111
+ type: "rotate";
112
+ angle: number;
113
+ fillcolor: string;
114
+ } | {
115
+ type: "transpose";
116
+ dir: "cclock" | "clock" | "cclock_flip" | "clock_flip";
117
+ } | {
118
+ type: "vignette";
119
+ angle: number;
120
+ mode: "forward" | "backward";
121
+ x0?: number | undefined;
122
+ y0?: number | undefined;
123
+ } | {
124
+ type: "fade";
125
+ mode: "in" | "out";
126
+ start_frame: number;
127
+ nb_frames: number;
128
+ alpha: boolean;
129
+ color: string;
130
+ } | {
131
+ type: "pixelize";
132
+ width: number;
133
+ height: number;
134
+ mode: "avg" | "min" | "max";
135
+ } | {
136
+ type: "pseudocolor";
137
+ preset: "magma" | "inferno" | "plasma" | "viridis" | "turbo" | "cividis" | "range1" | "range2" | "shadows" | "highlights" | "solar" | "nominal" | "preferred" | "total";
138
+ } | {
139
+ type: "tmix";
140
+ frames: number;
141
+ weights?: string | undefined;
142
+ } | {
143
+ type: "lagfun";
144
+ decay: number;
145
+ planes: number;
146
+ } | {
147
+ type: "threshold";
148
+ planes: number;
149
+ } | {
150
+ type: "elbg";
151
+ codebook_length: number;
152
+ } | {
153
+ type: "lensdistortion";
154
+ k1: number;
155
+ k2: number;
156
+ } | {
157
+ type: "chromashift";
158
+ cbh: number;
159
+ cbv: number;
160
+ crh: number;
161
+ crv: number;
162
+ edge: "smear" | "wrap";
163
+ } | {
164
+ type: "deflicker";
165
+ size: number;
166
+ mode: "gm" | "am" | "hm" | "qm" | "cm" | "pm" | "median";
167
+ } | {
168
+ type: "dctdnoiz";
169
+ sigma: number;
170
+ } | {
171
+ type: "custom";
172
+ filter: string;
173
+ })[] | undefined;
28
174
  speed?: number | undefined;
29
175
  };
30
176
  keyName: string;