mulmocast 0.0.2 → 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 +27 -9
- package/assets/font/NotoSansJP-Regular.ttf +0 -0
- package/assets/html/chart.html +1 -10
- package/assets/html/mermaid.html +1 -13
- package/assets/templates/business.json +16 -27
- package/assets/templates/coding.json +58 -21
- package/lib/actions/audio.d.ts +1 -1
- package/lib/actions/audio.js +43 -27
- package/lib/actions/images.js +20 -26
- 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 +97 -38
- 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.js +22 -9
- package/lib/agents/anthropic_agent.d.ts +23 -0
- package/lib/agents/anthropic_agent.js +162 -0
- package/lib/agents/combine_audio_files_agent.js +13 -22
- package/lib/agents/nested_agent.d.ts +9 -0
- package/lib/agents/nested_agent.js +138 -0
- package/lib/cli/args.d.ts +3 -1
- package/lib/cli/args.js +49 -34
- package/lib/cli/cli.d.ts +14 -0
- package/lib/cli/cli.js +48 -46
- package/lib/cli/tool-args.d.ts +2 -0
- package/lib/cli/tool-args.js +12 -2
- package/lib/cli/tool-cli.js +6 -4
- package/lib/methods/index.d.ts +1 -0
- package/lib/methods/index.js +1 -0
- 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 +2 -6
- package/lib/methods/mulmo_script.js +12 -5
- package/lib/tools/create_mulmo_script_interactively.d.ts +1 -1
- package/lib/tools/create_mulmo_script_interactively.js +61 -20
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/schema.d.ts +3626 -3162
- package/lib/types/schema.js +75 -41
- package/lib/types/type.d.ts +28 -1
- package/lib/utils/const.d.ts +2 -0
- package/lib/utils/const.js +2 -0
- package/lib/utils/file.d.ts +4 -1
- package/lib/utils/file.js +15 -1
- package/lib/utils/filters.js +1 -1
- 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/markdown.js +4 -1
- package/lib/utils/pdf.d.ts +8 -0
- package/lib/utils/pdf.js +75 -0
- package/lib/utils/preprocess.d.ts +58 -128
- package/lib/utils/preprocess.js +37 -37
- package/lib/utils/utils.d.ts +12 -0
- package/lib/utils/utils.js +34 -0
- package/package.json +13 -4
package/lib/actions/movie.js
CHANGED
|
@@ -2,56 +2,115 @@ import ffmpeg from "fluent-ffmpeg";
|
|
|
2
2
|
import { GraphAILogger } from "graphai";
|
|
3
3
|
import { MulmoScriptMethods } from "../methods/index.js";
|
|
4
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
|
+
};
|
|
25
|
+
};
|
|
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 [
|
|
39
|
+
"-preset veryfast", // Faster encoding
|
|
40
|
+
"-map [v]", // Map the video stream
|
|
41
|
+
`-map ${audioId}`, // Map the audio stream
|
|
42
|
+
`-c:v ${videoCodec}`, // Set video codec
|
|
43
|
+
"-threads 8",
|
|
44
|
+
"-filter_threads 8",
|
|
45
|
+
"-b:v 5M", // bitrate (only for videotoolbox)
|
|
46
|
+
"-bufsize",
|
|
47
|
+
"10M", // Add buffer size for better quality
|
|
48
|
+
"-maxrate",
|
|
49
|
+
"7M", // Maximum bitrate
|
|
50
|
+
"-r 30", // Set frame rate
|
|
51
|
+
"-pix_fmt yuv420p", // Set pixel format for better compatibility
|
|
52
|
+
];
|
|
53
|
+
};
|
|
5
54
|
const createVideo = (audioArtifactFilePath, outputVideoPath, studio) => {
|
|
6
55
|
return new Promise((resolve, reject) => {
|
|
7
56
|
const start = performance.now();
|
|
8
|
-
|
|
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
|
+
}
|
|
9
66
|
if (studio.beats.some((beat) => !beat.imageFile)) {
|
|
10
67
|
GraphAILogger.info("beat.imageFile is not set. Please run `yarn run images ${file}` ");
|
|
11
68
|
return;
|
|
12
69
|
}
|
|
13
|
-
// Add each image input
|
|
14
|
-
studio.beats.forEach((beat) => {
|
|
15
|
-
command = command.input(beat.imageFile); // HACK
|
|
16
|
-
});
|
|
17
|
-
const imageCount = studio.beats.length;
|
|
18
70
|
const canvasInfo = MulmoScriptMethods.getCanvasSize(studio.script);
|
|
71
|
+
const padding = MulmoScriptMethods.getPadding(studio.script) / 1000;
|
|
72
|
+
// Add each image input
|
|
19
73
|
const filterComplexParts = [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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);
|
|
33
95
|
// Concatenate the trimmed images
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
})();
|
|
36
110
|
// Apply the filter complex for concatenation and map audio input
|
|
37
|
-
command
|
|
111
|
+
ffmpegContext.command
|
|
38
112
|
.complexFilter(filterComplexParts)
|
|
39
|
-
.
|
|
40
|
-
.outputOptions([
|
|
41
|
-
"-preset veryfast", // Faster encoding
|
|
42
|
-
"-map [v]", // Map the video stream
|
|
43
|
-
`-map ${imageCount /* + captionCount*/}:a`, // Map the audio stream (audio is the next input after all images)
|
|
44
|
-
"-c:v h264_videotoolbox", // Set video codec
|
|
45
|
-
"-threads 8",
|
|
46
|
-
"-filter_threads 8",
|
|
47
|
-
"-b:v 5M", // bitrate (only for videotoolbox)
|
|
48
|
-
"-bufsize",
|
|
49
|
-
"10M", // Add buffer size for better quality
|
|
50
|
-
"-maxrate",
|
|
51
|
-
"7M", // Maximum bitrate
|
|
52
|
-
"-r 30", // Set frame rate
|
|
53
|
-
"-pix_fmt yuv420p", // Set pixel format for better compatibility
|
|
54
|
-
])
|
|
113
|
+
.outputOptions(getOutputOption(ffmpegContextAudioId))
|
|
55
114
|
.on("start", (__cmdLine) => {
|
|
56
115
|
GraphAILogger.log("Started FFmpeg ..."); // with command:', cmdLine);
|
|
57
116
|
})
|
|
@@ -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
|
+
};
|
package/lib/actions/translate.js
CHANGED
|
@@ -32,7 +32,8 @@ const translateGraph = {
|
|
|
32
32
|
agent: "mapAgent",
|
|
33
33
|
inputs: {
|
|
34
34
|
targetLangs: ":targetLangs",
|
|
35
|
-
|
|
35
|
+
studio: ":studio",
|
|
36
|
+
rows: ":studio.script.beats",
|
|
36
37
|
lang: ":lang",
|
|
37
38
|
},
|
|
38
39
|
params: {
|
|
@@ -42,12 +43,23 @@ const translateGraph = {
|
|
|
42
43
|
graph: {
|
|
43
44
|
version: 0.5,
|
|
44
45
|
nodes: {
|
|
46
|
+
studioBeat: {
|
|
47
|
+
agent: (namedInputs) => {
|
|
48
|
+
return namedInputs.rows[namedInputs.index];
|
|
49
|
+
},
|
|
50
|
+
inputs: {
|
|
51
|
+
index: ":__mapIndex",
|
|
52
|
+
rows: ":studio.beats",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
45
55
|
preprocessBeats: {
|
|
46
56
|
agent: "mapAgent",
|
|
47
57
|
inputs: {
|
|
48
58
|
beat: ":beat",
|
|
59
|
+
studioBeat: ":studioBeat",
|
|
49
60
|
rows: ":targetLangs",
|
|
50
61
|
lang: ":lang.text",
|
|
62
|
+
studio: ":studio",
|
|
51
63
|
},
|
|
52
64
|
params: {
|
|
53
65
|
compositeResult: true,
|
|
@@ -60,6 +72,7 @@ const translateGraph = {
|
|
|
60
72
|
inputs: {
|
|
61
73
|
targetLang: ":targetLang",
|
|
62
74
|
beat: ":beat",
|
|
75
|
+
studioBeat: ":studioBeat",
|
|
63
76
|
lang: ":lang",
|
|
64
77
|
system: "Please translate the given text into the language specified in language (in locale format, like en, ja, fr, ch).",
|
|
65
78
|
prompt: ["## Original Language", ":lang", "", "## Language", ":targetLang", "", "## Target", ":beat.text"],
|
|
@@ -138,7 +151,7 @@ const translateGraph = {
|
|
|
138
151
|
isResult: true,
|
|
139
152
|
agent: "mergeObjectAgent",
|
|
140
153
|
inputs: {
|
|
141
|
-
items: [":
|
|
154
|
+
items: [":studioBeat", { multiLingualTexts: ":mergeLocalizedText" }],
|
|
142
155
|
},
|
|
143
156
|
},
|
|
144
157
|
},
|
|
@@ -156,14 +169,14 @@ const translateGraph = {
|
|
|
156
169
|
};
|
|
157
170
|
const localizedTextCacheAgentFilter = async (context, next) => {
|
|
158
171
|
const { namedInputs } = context;
|
|
159
|
-
const { targetLang, beat, lang } = namedInputs;
|
|
172
|
+
const { targetLang, beat, lang, studioBeat } = namedInputs;
|
|
160
173
|
// The original text is unchanged and the target language text is present
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return { text:
|
|
174
|
+
if (studioBeat.multiLingualTexts &&
|
|
175
|
+
studioBeat.multiLingualTexts[lang] &&
|
|
176
|
+
studioBeat.multiLingualTexts[lang].text === beat.text &&
|
|
177
|
+
studioBeat.multiLingualTexts[targetLang] &&
|
|
178
|
+
studioBeat.multiLingualTexts[targetLang].text) {
|
|
179
|
+
return { text: studioBeat.multiLingualTexts[targetLang].text };
|
|
167
180
|
}
|
|
168
181
|
// same language
|
|
169
182
|
if (targetLang === lang) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { AgentFunction, AgentFunctionInfo } from "graphai";
|
|
3
|
+
import { GraphAILLMInputBase } from "@graphai/llm_utils";
|
|
4
|
+
import type { GraphAIText, GraphAITool, GraphAIToolCalls, GraphAIMessage, GraphAIMessages } from "@graphai/agent_utils";
|
|
5
|
+
type AnthropicInputs = {
|
|
6
|
+
verbose?: boolean;
|
|
7
|
+
model?: string;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
max_tokens?: number;
|
|
10
|
+
tools?: any[];
|
|
11
|
+
tool_choice?: any;
|
|
12
|
+
messages?: Array<Anthropic.MessageParam>;
|
|
13
|
+
} & GraphAILLMInputBase;
|
|
14
|
+
type AnthropicConfig = {
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
stream?: boolean;
|
|
17
|
+
forWeb?: boolean;
|
|
18
|
+
};
|
|
19
|
+
type AnthropicParams = AnthropicInputs & AnthropicConfig;
|
|
20
|
+
type AnthropicResult = Partial<GraphAIText & GraphAITool & GraphAIToolCalls & GraphAIMessage<string | Anthropic.ContentBlockParam[]> & GraphAIMessages<string | Anthropic.ContentBlockParam[]>>;
|
|
21
|
+
export declare const anthropicAgent: AgentFunction<AnthropicParams, AnthropicResult, AnthropicInputs, AnthropicConfig>;
|
|
22
|
+
declare const anthropicAgentInfo: AgentFunctionInfo;
|
|
23
|
+
export default anthropicAgentInfo;
|