mulmocast 0.0.1 → 0.0.3
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 +129 -15
- package/assets/font/NotoSansJP-Regular.ttf +0 -0
- package/assets/html/chart.html +38 -0
- package/assets/html/mermaid.html +51 -0
- package/assets/templates/business.json +57 -14
- package/assets/templates/children_book.json +1 -3
- package/assets/templates/coding.json +140 -0
- package/lib/actions/audio.d.ts +2 -2
- package/lib/actions/audio.js +88 -101
- package/lib/actions/images.d.ts +1 -1
- package/lib/actions/images.js +50 -88
- package/lib/actions/index.d.ts +5 -0
- package/lib/actions/index.js +5 -0
- package/lib/actions/movie.d.ts +9 -1
- package/lib/actions/movie.js +124 -65
- package/lib/actions/pdf.d.ts +2 -0
- package/lib/actions/pdf.js +211 -0
- package/lib/actions/pdf2.d.ts +2 -0
- package/lib/actions/pdf2.js +203 -0
- package/lib/actions/translate.d.ts +1 -1
- package/lib/actions/translate.js +38 -61
- package/lib/agents/add_bgm_agent.d.ts +1 -1
- package/lib/agents/add_bgm_agent.js +10 -14
- package/lib/agents/anthropic_agent.d.ts +23 -0
- package/lib/agents/anthropic_agent.js +162 -0
- package/lib/agents/combine_audio_files_agent.d.ts +1 -1
- package/lib/agents/combine_audio_files_agent.js +33 -32
- package/lib/agents/image_google_agent.d.ts +1 -1
- package/lib/agents/image_google_agent.js +8 -11
- package/lib/agents/image_openai_agent.js +7 -14
- package/lib/agents/index.d.ts +8 -8
- package/lib/agents/index.js +13 -30
- package/lib/agents/mulmo_prompts_agent.d.ts +1 -1
- package/lib/agents/mulmo_prompts_agent.js +7 -11
- package/lib/agents/nested_agent.d.ts +9 -0
- package/lib/agents/nested_agent.js +138 -0
- package/lib/agents/prompts_data.js +1 -4
- package/lib/agents/tts_nijivoice_agent.d.ts +1 -1
- package/lib/agents/tts_nijivoice_agent.js +8 -12
- package/lib/agents/tts_openai_agent.js +9 -16
- package/lib/agents/validate_mulmo_script_agent.d.ts +1 -1
- package/lib/agents/validate_mulmo_script_agent.js +6 -10
- package/lib/cli/args.d.ts +5 -2
- package/lib/cli/args.js +52 -35
- package/lib/cli/cli.d.ts +14 -0
- package/lib/cli/cli.js +74 -57
- package/lib/cli/common.js +1 -5
- package/lib/cli/tool-args.d.ts +4 -1
- package/lib/cli/tool-args.js +29 -18
- package/lib/cli/tool-cli.js +34 -51
- package/lib/methods/index.d.ts +4 -3
- package/lib/methods/index.js +4 -19
- package/lib/methods/mulmo_media_source.d.ts +4 -0
- package/lib/methods/mulmo_media_source.js +21 -0
- package/lib/methods/mulmo_script.d.ts +6 -5
- package/lib/methods/mulmo_script.js +29 -16
- package/lib/methods/mulmo_script_template.d.ts +1 -1
- package/lib/methods/mulmo_script_template.js +4 -10
- package/lib/methods/mulmo_studio_context.d.ts +1 -1
- package/lib/methods/mulmo_studio_context.js +3 -9
- package/lib/tools/create_mulmo_script_from_url.d.ts +3 -0
- package/lib/tools/create_mulmo_script_from_url.js +152 -0
- package/lib/tools/create_mulmo_script_interactively.d.ts +3 -0
- package/lib/tools/create_mulmo_script_interactively.js +258 -0
- package/lib/tools/dump_prompt.js +5 -8
- package/lib/tools/prompt.js +9 -11
- package/lib/tools/seed_from_url2.d.ts +3 -0
- package/lib/tools/seed_from_url2.js +154 -0
- package/lib/types/index.d.ts +2 -1
- package/lib/types/index.js +2 -17
- package/lib/types/schema.d.ts +3624 -2798
- package/lib/types/schema.js +172 -123
- package/lib/types/type.d.ts +34 -3
- package/lib/types/type.js +1 -2
- package/lib/utils/const.d.ts +4 -1
- package/lib/utils/const.js +6 -6
- package/lib/utils/file.d.ts +22 -4
- package/lib/utils/file.js +100 -79
- package/lib/utils/filters.d.ts +1 -0
- package/lib/utils/filters.js +47 -26
- package/lib/utils/image_plugins/chart.d.ts +3 -0
- package/lib/utils/image_plugins/chart.js +18 -0
- package/lib/utils/image_plugins/image.d.ts +2 -0
- package/lib/utils/image_plugins/image.js +3 -0
- package/lib/utils/image_plugins/index.d.ts +7 -0
- package/lib/utils/image_plugins/index.js +7 -0
- package/lib/utils/image_plugins/markdown.d.ts +3 -0
- package/lib/utils/image_plugins/markdown.js +11 -0
- package/lib/utils/image_plugins/mermaid.d.ts +3 -0
- package/lib/utils/image_plugins/mermaid.js +21 -0
- package/lib/utils/image_plugins/movie.d.ts +2 -0
- package/lib/utils/image_plugins/movie.js +3 -0
- package/lib/utils/image_plugins/source.d.ts +4 -0
- package/lib/utils/image_plugins/source.js +15 -0
- package/lib/utils/image_plugins/text_slide.d.ts +3 -0
- package/lib/utils/image_plugins/text_slide.js +12 -0
- package/lib/utils/image_plugins/type_guards.d.ts +6 -0
- package/lib/utils/image_plugins/type_guards.js +21 -0
- package/lib/utils/image_preprocess.d.ts +14 -0
- package/lib/utils/image_preprocess.js +52 -0
- package/lib/utils/inquirer.d.ts +2 -0
- package/lib/utils/inquirer.js +33 -0
- package/lib/utils/markdown.d.ts +3 -1
- package/lib/utils/markdown.js +20 -19
- package/lib/utils/pdf.d.ts +8 -0
- package/lib/utils/pdf.js +75 -0
- package/lib/utils/plugins.d.ts +5 -0
- package/lib/utils/plugins.js +11 -0
- package/lib/utils/preprocess.d.ts +70 -123
- package/lib/utils/preprocess.js +37 -43
- package/lib/utils/string.js +4 -10
- package/lib/utils/text_hash.js +2 -39
- package/lib/utils/utils.d.ts +12 -0
- package/lib/utils/utils.js +34 -0
- package/package.json +23 -8
package/lib/actions/movie.js
CHANGED
|
@@ -1,51 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import ffmpeg from "fluent-ffmpeg";
|
|
2
|
+
import { GraphAILogger } from "graphai";
|
|
3
|
+
import { MulmoScriptMethods } from "../methods/index.js";
|
|
4
|
+
import { getAudioArtifactFilePath, getOutputVideoFilePath, writingMessage } from "../utils/file.js";
|
|
5
|
+
const isMac = process.platform === "darwin";
|
|
6
|
+
const videoCodec = isMac ? "h264_videotoolbox" : "libx264";
|
|
7
|
+
export const getVideoPart = (inputIndex, mediaType, duration, canvasInfo) => {
|
|
8
|
+
const videoId = `v${inputIndex}`;
|
|
9
|
+
return {
|
|
10
|
+
videoId,
|
|
11
|
+
videoPart: `[${inputIndex}:v]` +
|
|
12
|
+
[
|
|
13
|
+
mediaType === "image" ? "loop=loop=-1:size=1:start=0" : "",
|
|
14
|
+
`trim=duration=${duration}`,
|
|
15
|
+
"fps=30",
|
|
16
|
+
"setpts=PTS-STARTPTS",
|
|
17
|
+
`scale=${canvasInfo.width}:${canvasInfo.height}`,
|
|
18
|
+
"setsar=1",
|
|
19
|
+
"format=yuv420p",
|
|
20
|
+
]
|
|
21
|
+
.filter((a) => a)
|
|
22
|
+
.join(",") +
|
|
23
|
+
`[${videoId}]`,
|
|
24
|
+
};
|
|
4
25
|
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
studio.beats.forEach((beat) => {
|
|
19
|
-
command = command.input(beat.imageFile); // HACK
|
|
20
|
-
});
|
|
21
|
-
const imageCount = studio.beats.length;
|
|
22
|
-
const canvasInfo = methods_1.MulmoScriptMethods.getCanvasSize(studio.script);
|
|
23
|
-
const filterComplexParts = [];
|
|
24
|
-
studio.beats.forEach((beat, index) => {
|
|
25
|
-
// Resize background image to match canvas dimensions
|
|
26
|
-
const duration = beat.duration + (index === 0 ? methods_1.MulmoScriptMethods.getPadding(studio.script) / 1000 : 0);
|
|
27
|
-
const parts = `[${index}:v]loop=loop=-1:size=1:start=0,` +
|
|
28
|
-
`trim=duration=${duration},` +
|
|
29
|
-
`fps=30,` +
|
|
30
|
-
`setpts=PTS-STARTPTS,` +
|
|
31
|
-
`scale=${canvasInfo.width}:${canvasInfo.height},` +
|
|
32
|
-
`setsar=1,format=yuv420p` +
|
|
33
|
-
`[v${index}]`;
|
|
34
|
-
// console.log(parts);
|
|
35
|
-
filterComplexParts.push(parts);
|
|
36
|
-
});
|
|
37
|
-
// Concatenate the trimmed images
|
|
38
|
-
const concatInput = studio.beats.map((_, index) => `[v${index}]`).join("");
|
|
39
|
-
filterComplexParts.push(`${concatInput}concat=n=${imageCount}:v=1:a=0[v]`);
|
|
40
|
-
// Apply the filter complex for concatenation and map audio input
|
|
41
|
-
command
|
|
42
|
-
.complexFilter(filterComplexParts)
|
|
43
|
-
.input(audioPath) // Add audio input
|
|
44
|
-
.outputOptions([
|
|
26
|
+
export const getAudioPart = (inputIndex, duration, delay) => {
|
|
27
|
+
const audioId = `a${inputIndex}`;
|
|
28
|
+
return {
|
|
29
|
+
audioId,
|
|
30
|
+
audioPart: `[${inputIndex}:a]` +
|
|
31
|
+
`atrim=duration=${duration},` + // Trim to beat duration
|
|
32
|
+
`adelay=${delay}|${delay},` +
|
|
33
|
+
`aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo` +
|
|
34
|
+
`[${audioId}]`,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const getOutputOption = (audioId) => {
|
|
38
|
+
return [
|
|
45
39
|
"-preset veryfast", // Faster encoding
|
|
46
40
|
"-map [v]", // Map the video stream
|
|
47
|
-
`-map ${
|
|
48
|
-
|
|
41
|
+
`-map ${audioId}`, // Map the audio stream
|
|
42
|
+
`-c:v ${videoCodec}`, // Set video codec
|
|
49
43
|
"-threads 8",
|
|
50
44
|
"-filter_threads 8",
|
|
51
45
|
"-b:v 5M", // bitrate (only for videotoolbox)
|
|
@@ -55,27 +49,92 @@ const createVideo = (audioPath, outputVideoPath, studio) => {
|
|
|
55
49
|
"7M", // Maximum bitrate
|
|
56
50
|
"-r 30", // Set frame rate
|
|
57
51
|
"-pix_fmt yuv420p", // Set pixel format for better compatibility
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
52
|
+
];
|
|
53
|
+
};
|
|
54
|
+
const createVideo = (audioArtifactFilePath, outputVideoPath, studio) => {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const start = performance.now();
|
|
57
|
+
const ffmpegContext = {
|
|
58
|
+
command: ffmpeg(),
|
|
59
|
+
inputCount: 0,
|
|
60
|
+
};
|
|
61
|
+
function addInput(input) {
|
|
62
|
+
ffmpegContext.command = ffmpegContext.command.input(input);
|
|
63
|
+
ffmpegContext.inputCount++;
|
|
64
|
+
return ffmpegContext.inputCount - 1; // returned the index of the input
|
|
65
|
+
}
|
|
66
|
+
if (studio.beats.some((beat) => !beat.imageFile)) {
|
|
67
|
+
GraphAILogger.info("beat.imageFile is not set. Please run `yarn run images ${file}` ");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const canvasInfo = MulmoScriptMethods.getCanvasSize(studio.script);
|
|
71
|
+
const padding = MulmoScriptMethods.getPadding(studio.script) / 1000;
|
|
72
|
+
// Add each image input
|
|
73
|
+
const filterComplexParts = [];
|
|
74
|
+
const filterComplexVideoIds = [];
|
|
75
|
+
const filterComplexAudioIds = [];
|
|
76
|
+
studio.beats.reduce((timestamp, beat, index) => {
|
|
77
|
+
if (!beat.imageFile || !beat.duration) {
|
|
78
|
+
throw new Error(`beat.imageFile is not set: index=${index}`);
|
|
79
|
+
}
|
|
80
|
+
const inputIndex = addInput(beat.imageFile);
|
|
81
|
+
const mediaType = MulmoScriptMethods.getImageType(studio.script, studio.script.beats[index]);
|
|
82
|
+
const headOrTail = index === 0 || index === studio.beats.length - 1;
|
|
83
|
+
const duration = beat.duration + (headOrTail ? padding : 0);
|
|
84
|
+
const { videoId, videoPart } = getVideoPart(inputIndex, mediaType, duration, canvasInfo);
|
|
85
|
+
filterComplexVideoIds.push(videoId);
|
|
86
|
+
filterComplexParts.push(videoPart);
|
|
87
|
+
if (mediaType === "movie") {
|
|
88
|
+
const { audioId, audioPart } = getAudioPart(inputIndex, duration, timestamp * 1000);
|
|
89
|
+
filterComplexAudioIds.push(audioId);
|
|
90
|
+
filterComplexParts.push(audioPart);
|
|
91
|
+
}
|
|
92
|
+
return timestamp + duration;
|
|
93
|
+
}, 0);
|
|
94
|
+
// console.log("*** images", images.audioIds);
|
|
95
|
+
// Concatenate the trimmed images
|
|
96
|
+
filterComplexParts.push(`${filterComplexVideoIds.map((id) => `[${id}]`).join("")}concat=n=${studio.beats.length}:v=1:a=0[v]`);
|
|
97
|
+
const audioIndex = addInput(audioArtifactFilePath); // Add audio input
|
|
98
|
+
const artifactAudioId = `${audioIndex}:a`;
|
|
99
|
+
const ffmpegContextAudioId = (() => {
|
|
100
|
+
if (filterComplexAudioIds.length > 0) {
|
|
101
|
+
const mainAudioId = "mainaudio";
|
|
102
|
+
const compositeAudioId = "composite";
|
|
103
|
+
const audioIds = filterComplexAudioIds.map((id) => `[${id}]`).join("");
|
|
104
|
+
filterComplexParts.push(`[${artifactAudioId}]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo[${mainAudioId}]`);
|
|
105
|
+
filterComplexParts.push(`[${mainAudioId}]${audioIds}amix=inputs=${filterComplexAudioIds.length + 1}:duration=first:dropout_transition=2[${compositeAudioId}]`);
|
|
106
|
+
return `[${compositeAudioId}]`; // notice that we need to use [mainaudio] instead of mainaudio
|
|
107
|
+
}
|
|
108
|
+
return artifactAudioId;
|
|
109
|
+
})();
|
|
110
|
+
// Apply the filter complex for concatenation and map audio input
|
|
111
|
+
ffmpegContext.command
|
|
112
|
+
.complexFilter(filterComplexParts)
|
|
113
|
+
.outputOptions(getOutputOption(ffmpegContextAudioId))
|
|
114
|
+
.on("start", (__cmdLine) => {
|
|
115
|
+
GraphAILogger.log("Started FFmpeg ..."); // with command:', cmdLine);
|
|
116
|
+
})
|
|
117
|
+
.on("error", (err, stdout, stderr) => {
|
|
118
|
+
GraphAILogger.error("Error occurred:", err);
|
|
119
|
+
GraphAILogger.error("FFmpeg stdout:", stdout);
|
|
120
|
+
GraphAILogger.error("FFmpeg stderr:", stderr);
|
|
121
|
+
GraphAILogger.info("Video creation failed. An unexpected error occurred.");
|
|
122
|
+
reject();
|
|
123
|
+
})
|
|
124
|
+
.on("end", () => {
|
|
125
|
+
const end = performance.now();
|
|
126
|
+
GraphAILogger.info(`Video created successfully! ${Math.round(end - start) / 1000} sec`);
|
|
127
|
+
resolve(0);
|
|
128
|
+
})
|
|
129
|
+
.output(outputVideoPath)
|
|
130
|
+
.run();
|
|
131
|
+
});
|
|
73
132
|
};
|
|
74
|
-
const movie = async (context) => {
|
|
133
|
+
export const movie = async (context) => {
|
|
75
134
|
const { studio, fileDirs } = context;
|
|
76
135
|
const { outDirPath } = fileDirs;
|
|
77
|
-
const
|
|
78
|
-
const outputVideoPath =
|
|
79
|
-
createVideo(
|
|
136
|
+
const audioArtifactFilePath = getAudioArtifactFilePath(outDirPath, studio.filename);
|
|
137
|
+
const outputVideoPath = getOutputVideoFilePath(outDirPath, studio.filename);
|
|
138
|
+
await createVideo(audioArtifactFilePath, outputVideoPath, studio);
|
|
139
|
+
writingMessage(outputVideoPath);
|
|
80
140
|
};
|
|
81
|
-
exports.movie = movie;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { rgb, PDFDocument } from "pdf-lib";
|
|
4
|
+
import fontkit from "@pdf-lib/fontkit";
|
|
5
|
+
import { chunkArray, isHttp } from "../utils/utils.js";
|
|
6
|
+
import { getOutputPdfFilePath, writingMessage } from "../utils/file.js";
|
|
7
|
+
import { MulmoScriptMethods } from "../methods/index.js";
|
|
8
|
+
import { fontSize, textMargin, drawSize, wrapText } from "../utils/pdf.js";
|
|
9
|
+
const imagesPerPage = 4;
|
|
10
|
+
const offset = 10;
|
|
11
|
+
const handoutImageRatio = 0.5;
|
|
12
|
+
const readImage = async (imagePath, pdfDoc) => {
|
|
13
|
+
const imageBytes = await (async () => {
|
|
14
|
+
if (isHttp(imagePath)) {
|
|
15
|
+
const res = await fetch(imagePath);
|
|
16
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
17
|
+
return Buffer.from(arrayBuffer);
|
|
18
|
+
}
|
|
19
|
+
return fs.readFileSync(imagePath);
|
|
20
|
+
})();
|
|
21
|
+
const ext = path.extname(imagePath).toLowerCase();
|
|
22
|
+
return ext === ".jpg" || ext === ".jpeg" ? await pdfDoc.embedJpg(imageBytes) : await pdfDoc.embedPng(imageBytes);
|
|
23
|
+
};
|
|
24
|
+
const pdfSlide = async (pageWidth, pageHeight, imagePaths, pdfDoc) => {
|
|
25
|
+
const cellRatio = pageHeight / pageWidth;
|
|
26
|
+
for (const imagePath of imagePaths) {
|
|
27
|
+
const image = await readImage(imagePath, pdfDoc);
|
|
28
|
+
const { width: origWidth, height: origHeight } = image.scale(1);
|
|
29
|
+
const originalRatio = origHeight / origWidth;
|
|
30
|
+
const fitWidth = originalRatio / cellRatio < 1;
|
|
31
|
+
const { drawWidth, drawHeight } = drawSize(fitWidth, pageWidth, pageHeight, origWidth, origHeight);
|
|
32
|
+
const page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
33
|
+
page.drawImage(image, {
|
|
34
|
+
x: 0,
|
|
35
|
+
y: 0,
|
|
36
|
+
width: drawWidth,
|
|
37
|
+
height: drawHeight,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const pdfTalk = async (pageWidth, pageHeight, imagePaths, texts, pdfDoc, font) => {
|
|
42
|
+
const imageRatio = 0.7;
|
|
43
|
+
const textMargin = 8;
|
|
44
|
+
const textY = pageHeight * (1 - imageRatio) - textMargin;
|
|
45
|
+
const targetWidth = pageWidth - offset;
|
|
46
|
+
const targetHeight = pageHeight * imageRatio - offset;
|
|
47
|
+
const cellRatio = targetHeight / targetWidth;
|
|
48
|
+
for (const [index, imagePath] of imagePaths.entries()) {
|
|
49
|
+
const text = texts[index];
|
|
50
|
+
const image = await readImage(imagePath, pdfDoc);
|
|
51
|
+
const { width: origWidth, height: origHeight } = image.scale(1);
|
|
52
|
+
const originalRatio = origHeight / origWidth;
|
|
53
|
+
const fitWidth = originalRatio / cellRatio < 1;
|
|
54
|
+
const { drawWidth, drawHeight } = drawSize(fitWidth, targetWidth, targetHeight, origWidth, origHeight);
|
|
55
|
+
const x = (pageWidth - drawWidth) / 2;
|
|
56
|
+
const y = pageHeight - drawHeight - offset;
|
|
57
|
+
const page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
58
|
+
const pos = {
|
|
59
|
+
x,
|
|
60
|
+
y,
|
|
61
|
+
width: drawWidth,
|
|
62
|
+
height: drawHeight,
|
|
63
|
+
};
|
|
64
|
+
page.drawImage(image, pos);
|
|
65
|
+
page.drawRectangle({
|
|
66
|
+
...pos,
|
|
67
|
+
borderColor: rgb(0, 0, 0),
|
|
68
|
+
borderWidth: 1,
|
|
69
|
+
});
|
|
70
|
+
const lines = wrapText(text, font, fontSize, pageWidth - textMargin * 2);
|
|
71
|
+
for (const [index, line] of lines.entries()) {
|
|
72
|
+
page.drawText(line, {
|
|
73
|
+
x: textMargin,
|
|
74
|
+
y: textY - fontSize - (fontSize + 2) * index,
|
|
75
|
+
size: fontSize,
|
|
76
|
+
color: rgb(0, 0, 0),
|
|
77
|
+
maxWidth: pageWidth - 2 * textMargin,
|
|
78
|
+
font,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const pdfHandout = async (pageWidth, pageHeight, imagePaths, texts, pdfDoc, font, isLandscapeImage) => {
|
|
84
|
+
const cellRatio = (pageHeight / imagesPerPage - offset) / (pageWidth * handoutImageRatio - offset);
|
|
85
|
+
let index = 0;
|
|
86
|
+
for (const chunkPaths of chunkArray(imagePaths, imagesPerPage)) {
|
|
87
|
+
const page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
88
|
+
for (let i = 0; i < chunkPaths.length; i++) {
|
|
89
|
+
const imagePath = chunkPaths[i];
|
|
90
|
+
const image = await readImage(imagePath, pdfDoc);
|
|
91
|
+
const { width: origWidth, height: origHeight } = image.scale(1);
|
|
92
|
+
const originalRatio = origHeight / origWidth;
|
|
93
|
+
const fitWidth = originalRatio / cellRatio < 1;
|
|
94
|
+
const pos = (() => {
|
|
95
|
+
if (isLandscapeImage) {
|
|
96
|
+
const cellHeight = pageHeight / imagesPerPage - offset;
|
|
97
|
+
const { drawWidth, drawHeight } = drawSize(fitWidth, (pageWidth - offset) * handoutImageRatio, cellHeight - offset, origWidth, origHeight);
|
|
98
|
+
const x = offset;
|
|
99
|
+
const y = pageHeight - (i + 1) * cellHeight + (cellHeight - drawHeight) * handoutImageRatio;
|
|
100
|
+
return {
|
|
101
|
+
x,
|
|
102
|
+
y,
|
|
103
|
+
width: drawWidth,
|
|
104
|
+
height: drawHeight,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const cellWidth = pageWidth / imagesPerPage;
|
|
109
|
+
const { drawWidth, drawHeight } = drawSize(fitWidth, cellWidth - offset, (pageHeight - offset) * handoutImageRatio, origWidth, origHeight);
|
|
110
|
+
const x = pageWidth - (imagesPerPage - i) * cellWidth + (cellWidth - drawWidth) * handoutImageRatio;
|
|
111
|
+
const y = pageHeight - drawHeight - offset;
|
|
112
|
+
return {
|
|
113
|
+
x,
|
|
114
|
+
y,
|
|
115
|
+
width: drawWidth,
|
|
116
|
+
height: drawHeight,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
page.drawRectangle({
|
|
121
|
+
...pos,
|
|
122
|
+
borderColor: rgb(0, 0, 0),
|
|
123
|
+
borderWidth: 1,
|
|
124
|
+
});
|
|
125
|
+
page.drawImage(image, pos);
|
|
126
|
+
if (isLandscapeImage) {
|
|
127
|
+
const lines = wrapText(texts[index], font, fontSize, pos.width - textMargin * 2);
|
|
128
|
+
for (const [index, line] of lines.entries()) {
|
|
129
|
+
page.drawText(line, {
|
|
130
|
+
...pos,
|
|
131
|
+
x: pos.x + pos.width + textMargin,
|
|
132
|
+
y: pos.y + pos.height - fontSize - (fontSize + 2) * index,
|
|
133
|
+
size: fontSize,
|
|
134
|
+
font,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/*
|
|
138
|
+
page.drawRectangle({
|
|
139
|
+
...pos,
|
|
140
|
+
x: pos.x + pos.width ,
|
|
141
|
+
borderColor: rgb(0, 0, 0),
|
|
142
|
+
borderWidth: 1,
|
|
143
|
+
});
|
|
144
|
+
*/
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const lines = wrapText(texts[index], font, fontSize, pos.width - textMargin * 2);
|
|
148
|
+
for (const [index, line] of lines.entries()) {
|
|
149
|
+
page.drawText(line, {
|
|
150
|
+
...pos,
|
|
151
|
+
x: pos.x,
|
|
152
|
+
y: textMargin + pos.height - fontSize - (fontSize + textMargin) * index - 2 * textMargin,
|
|
153
|
+
size: fontSize,
|
|
154
|
+
font,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/*
|
|
158
|
+
page.drawRectangle({
|
|
159
|
+
...pos,
|
|
160
|
+
x: pos.x,
|
|
161
|
+
y: textMargin,
|
|
162
|
+
height: pos.height - 2 * textMargin,
|
|
163
|
+
borderColor: rgb(0, 0, 0),
|
|
164
|
+
borderWidth: 1,
|
|
165
|
+
});
|
|
166
|
+
*/
|
|
167
|
+
}
|
|
168
|
+
index = index + 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const outputSize = (pdfSize, isLandscapeImage, isRotate) => {
|
|
173
|
+
if (pdfSize === "a4") {
|
|
174
|
+
if (isLandscapeImage !== isRotate) {
|
|
175
|
+
return { width: 841.89, height: 595.28 };
|
|
176
|
+
}
|
|
177
|
+
return { width: 595.28, height: 841.89 };
|
|
178
|
+
}
|
|
179
|
+
// letter
|
|
180
|
+
if (isLandscapeImage !== isRotate) {
|
|
181
|
+
return { width: 792, height: 612 };
|
|
182
|
+
}
|
|
183
|
+
return { width: 612, height: 792 };
|
|
184
|
+
};
|
|
185
|
+
export const pdf = async (context, pdfMode, pdfSize) => {
|
|
186
|
+
const { studio, fileDirs } = context;
|
|
187
|
+
const { outDirPath } = fileDirs;
|
|
188
|
+
const { width: imageWidth, height: imageHeight } = MulmoScriptMethods.getCanvasSize(studio.script);
|
|
189
|
+
const isLandscapeImage = imageWidth > imageHeight;
|
|
190
|
+
const isRotate = pdfMode === "handout";
|
|
191
|
+
const { width: pageWidth, height: pageHeight } = outputSize(pdfSize, isLandscapeImage, isRotate);
|
|
192
|
+
const imagePaths = studio.beats.map((beat) => beat.imageFile);
|
|
193
|
+
const texts = studio.script.beats.map((beat) => beat.text);
|
|
194
|
+
const outputPdfPath = getOutputPdfFilePath(outDirPath, studio.filename, pdfMode);
|
|
195
|
+
const pdfDoc = await PDFDocument.create();
|
|
196
|
+
pdfDoc.registerFontkit(fontkit);
|
|
197
|
+
const fontBytes = fs.readFileSync("assets/font/NotoSansJP-Regular.ttf");
|
|
198
|
+
const customFont = await pdfDoc.embedFont(fontBytes);
|
|
199
|
+
if (pdfMode === "handout") {
|
|
200
|
+
await pdfHandout(pageWidth, pageHeight, imagePaths, texts, pdfDoc, customFont, isLandscapeImage);
|
|
201
|
+
}
|
|
202
|
+
if (pdfMode === "slide") {
|
|
203
|
+
await pdfSlide(pageWidth, pageHeight, imagePaths, pdfDoc);
|
|
204
|
+
}
|
|
205
|
+
if (pdfMode === "talk") {
|
|
206
|
+
await pdfTalk(pageWidth, pageHeight, imagePaths, texts, pdfDoc, customFont);
|
|
207
|
+
}
|
|
208
|
+
const pdfBytes = await pdfDoc.save();
|
|
209
|
+
fs.writeFileSync(outputPdfPath, pdfBytes);
|
|
210
|
+
writingMessage(outputPdfPath);
|
|
211
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { rgb, PDFDocument } from "pdf-lib";
|
|
4
|
+
import { chunkArray, isHttp } from "../utils/utils.js";
|
|
5
|
+
import { getOutputPdfFilePath, writingMessage } from "../utils/file.js";
|
|
6
|
+
import { MulmoScriptMethods } from "../methods/index.js";
|
|
7
|
+
const imagesPerPage = 4;
|
|
8
|
+
const offset = 10;
|
|
9
|
+
const handoutImageRatio = 0.5;
|
|
10
|
+
const readImage = async (imagePath, pdfDoc) => {
|
|
11
|
+
const imageBytes = await (async () => {
|
|
12
|
+
if (isHttp(imagePath)) {
|
|
13
|
+
const res = await fetch(imagePath);
|
|
14
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
15
|
+
return Buffer.from(arrayBuffer);
|
|
16
|
+
}
|
|
17
|
+
return fs.readFileSync(imagePath);
|
|
18
|
+
})();
|
|
19
|
+
const ext = path.extname(imagePath).toLowerCase();
|
|
20
|
+
return ext === ".jpg" || ext === ".jpeg" ? await pdfDoc.embedJpg(imageBytes) : await pdfDoc.embedPng(imageBytes);
|
|
21
|
+
};
|
|
22
|
+
const pdfSlide = async (pageWidth, pageHeight, imagePaths, pdfDoc) => {
|
|
23
|
+
const cellRatio = pageHeight / pageWidth;
|
|
24
|
+
for (const imagePath of imagePaths) {
|
|
25
|
+
const image = await readImage(imagePath, pdfDoc);
|
|
26
|
+
const { width: origWidth, height: origHeight } = image.scale(1);
|
|
27
|
+
const originalRatio = origHeight / origWidth;
|
|
28
|
+
const fitWidth = originalRatio / cellRatio < 1;
|
|
29
|
+
const { drawWidth, drawHeight } = drawSize(fitWidth, pageWidth, pageHeight, origWidth, origHeight);
|
|
30
|
+
const page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
31
|
+
page.drawImage(image, {
|
|
32
|
+
x: 0,
|
|
33
|
+
y: 0,
|
|
34
|
+
width: drawWidth,
|
|
35
|
+
height: drawHeight,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const pdfTalk = async (pageWidth, pageHeight, imagePaths, texts, pdfDoc) => {
|
|
40
|
+
const imageRatio = 0.7;
|
|
41
|
+
const textMargin = 20;
|
|
42
|
+
const textY = textMargin + (pageHeight * (1 - imageRatio)) / 2;
|
|
43
|
+
const targetWidth = pageWidth - offset;
|
|
44
|
+
const targetHeight = pageHeight * imageRatio - offset;
|
|
45
|
+
const cellRatio = targetHeight / targetWidth;
|
|
46
|
+
for (const [index, imagePath] of imagePaths.entries()) {
|
|
47
|
+
const text = texts[index];
|
|
48
|
+
const image = await readImage(imagePath, pdfDoc);
|
|
49
|
+
const { width: origWidth, height: origHeight } = image.scale(1);
|
|
50
|
+
const originalRatio = origHeight / origWidth;
|
|
51
|
+
const fitWidth = originalRatio / cellRatio < 1;
|
|
52
|
+
const { drawWidth, drawHeight } = drawSize(fitWidth, targetWidth, targetHeight, origWidth, origHeight);
|
|
53
|
+
const x = (pageWidth - drawWidth) / 2;
|
|
54
|
+
const y = pageHeight - drawHeight - offset;
|
|
55
|
+
const page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
56
|
+
const pos = {
|
|
57
|
+
x,
|
|
58
|
+
y,
|
|
59
|
+
width: drawWidth,
|
|
60
|
+
height: drawHeight,
|
|
61
|
+
};
|
|
62
|
+
page.drawImage(image, pos);
|
|
63
|
+
page.drawRectangle({
|
|
64
|
+
...pos,
|
|
65
|
+
borderColor: rgb(0, 0, 0),
|
|
66
|
+
borderWidth: 1,
|
|
67
|
+
});
|
|
68
|
+
page.drawText(text, {
|
|
69
|
+
x: textMargin,
|
|
70
|
+
y: textY,
|
|
71
|
+
size: 24,
|
|
72
|
+
color: rgb(0, 0, 0),
|
|
73
|
+
maxWidth: pageWidth - 2 * textMargin,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const drawSize = (fitWidth, expectWidth, expectHeight, origWidth, origHeight) => {
|
|
78
|
+
if (fitWidth) {
|
|
79
|
+
const drawWidth = expectWidth;
|
|
80
|
+
const scale = drawWidth / origWidth;
|
|
81
|
+
const drawHeight = origHeight * scale;
|
|
82
|
+
return {
|
|
83
|
+
drawWidth,
|
|
84
|
+
drawHeight,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const drawHeight = expectHeight;
|
|
88
|
+
const scale = drawHeight / origHeight;
|
|
89
|
+
const drawWidth = origWidth * scale;
|
|
90
|
+
return {
|
|
91
|
+
drawWidth,
|
|
92
|
+
drawHeight,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const pdfHandout = async (pageWidth, pageHeight, imagePaths, texts, pdfDoc, isLandscapeImage) => {
|
|
96
|
+
const cellRatio = (pageHeight / imagesPerPage - offset) / (pageWidth * handoutImageRatio - offset);
|
|
97
|
+
let i = 0;
|
|
98
|
+
for (const chunkPaths of chunkArray(imagePaths, imagesPerPage)) {
|
|
99
|
+
const page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
100
|
+
for (let i = 0; i < chunkPaths.length; i++) {
|
|
101
|
+
const imagePath = chunkPaths[i];
|
|
102
|
+
const image = await readImage(imagePath, pdfDoc);
|
|
103
|
+
const { width: origWidth, height: origHeight } = image.scale(1);
|
|
104
|
+
const originalRatio = origHeight / origWidth;
|
|
105
|
+
const fitWidth = originalRatio / cellRatio < 1;
|
|
106
|
+
// console.log({handoutImageRatio, cellRatio, ratio, imageHeight, origHeight});
|
|
107
|
+
const pos = (() => {
|
|
108
|
+
if (isLandscapeImage) {
|
|
109
|
+
const cellHeight = pageHeight / imagesPerPage - offset;
|
|
110
|
+
const { drawWidth, drawHeight } = drawSize(fitWidth, (pageWidth - offset) * handoutImageRatio, cellHeight - offset, origWidth, origHeight);
|
|
111
|
+
const x = offset;
|
|
112
|
+
const y = pageHeight - (i + 1) * cellHeight + (cellHeight - drawHeight) * handoutImageRatio;
|
|
113
|
+
return {
|
|
114
|
+
x,
|
|
115
|
+
y,
|
|
116
|
+
width: drawWidth,
|
|
117
|
+
height: drawHeight,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const cellWidth = pageWidth / imagesPerPage;
|
|
122
|
+
const { drawWidth, drawHeight } = drawSize(fitWidth, cellWidth - offset, (pageHeight - offset) * handoutImageRatio, origWidth, origHeight);
|
|
123
|
+
const x = pageWidth - (imagesPerPage - i) * cellWidth + (cellWidth - drawWidth) * handoutImageRatio;
|
|
124
|
+
const y = pageHeight - drawHeight - offset;
|
|
125
|
+
return {
|
|
126
|
+
x,
|
|
127
|
+
y,
|
|
128
|
+
width: drawWidth,
|
|
129
|
+
height: drawHeight,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
page.drawRectangle({
|
|
134
|
+
...pos,
|
|
135
|
+
borderColor: rgb(0, 0, 0),
|
|
136
|
+
borderWidth: 1,
|
|
137
|
+
});
|
|
138
|
+
page.drawImage(image, pos);
|
|
139
|
+
const text = texts[i];
|
|
140
|
+
const textMargin = 20;
|
|
141
|
+
if (isLandscapeImage) {
|
|
142
|
+
const textX = pos.x + pos.width + textMargin; // 画像の右端より少し右
|
|
143
|
+
const textY = pos.y + pos.height - 12; // 画像の上に揃える or 必要に応じて調整
|
|
144
|
+
page.drawText(text, {
|
|
145
|
+
x: textX,
|
|
146
|
+
y: textY,
|
|
147
|
+
size: 12,
|
|
148
|
+
color: rgb(0, 0, 0),
|
|
149
|
+
maxWidth: pageWidth - textX - textMargin,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const textY = pos.y - textMargin - 12;
|
|
154
|
+
page.drawText(text, {
|
|
155
|
+
x: pos.x,
|
|
156
|
+
y: textY,
|
|
157
|
+
size: 12,
|
|
158
|
+
color: rgb(0, 0, 0),
|
|
159
|
+
maxWidth: pos.width,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
i++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const outputSize = (pdfSize, isLandscapeImage, isRotate) => {
|
|
167
|
+
// console.log(pdfSize);
|
|
168
|
+
if (pdfSize === "a4") {
|
|
169
|
+
if (isLandscapeImage !== isRotate) {
|
|
170
|
+
return { width: 841.89, height: 595.28 };
|
|
171
|
+
}
|
|
172
|
+
return { width: 595.28, height: 841.89 };
|
|
173
|
+
}
|
|
174
|
+
// letter
|
|
175
|
+
if (isLandscapeImage !== isRotate) {
|
|
176
|
+
return { width: 792, height: 612 };
|
|
177
|
+
}
|
|
178
|
+
return { width: 612, height: 792 };
|
|
179
|
+
};
|
|
180
|
+
export const pdf = async (context, pdfMode, pdfSize) => {
|
|
181
|
+
const { studio, fileDirs } = context;
|
|
182
|
+
const { outDirPath } = fileDirs;
|
|
183
|
+
const { width: imageWidth, height: imageHeight } = MulmoScriptMethods.getCanvasSize(studio.script);
|
|
184
|
+
const isLandscapeImage = imageWidth > imageHeight;
|
|
185
|
+
const isRotate = pdfMode === "handout";
|
|
186
|
+
const { width: pageWidth, height: pageHeight } = outputSize(pdfSize, isLandscapeImage, isRotate);
|
|
187
|
+
const imagePaths = studio.beats.map((beat) => beat.imageFile);
|
|
188
|
+
const texts = studio.script.beats.map((beat) => beat.text);
|
|
189
|
+
const outputPdfPath = getOutputPdfFilePath(outDirPath, studio.filename, pdfMode);
|
|
190
|
+
const pdfDoc = await PDFDocument.create();
|
|
191
|
+
if (pdfMode === "handout") {
|
|
192
|
+
await pdfHandout(pageWidth, pageHeight, imagePaths, texts, pdfDoc, isLandscapeImage);
|
|
193
|
+
}
|
|
194
|
+
if (pdfMode === "slide") {
|
|
195
|
+
await pdfSlide(pageWidth, pageHeight, imagePaths, pdfDoc);
|
|
196
|
+
}
|
|
197
|
+
if (pdfMode === "talk") {
|
|
198
|
+
await pdfTalk(pageWidth, pageHeight, imagePaths, texts, pdfDoc);
|
|
199
|
+
}
|
|
200
|
+
const pdfBytes = await pdfDoc.save();
|
|
201
|
+
fs.writeFileSync(outputPdfPath, pdfBytes);
|
|
202
|
+
writingMessage(outputPdfPath);
|
|
203
|
+
};
|