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.
Files changed (115) hide show
  1. package/README.md +129 -15
  2. package/assets/font/NotoSansJP-Regular.ttf +0 -0
  3. package/assets/html/chart.html +38 -0
  4. package/assets/html/mermaid.html +51 -0
  5. package/assets/templates/business.json +57 -14
  6. package/assets/templates/children_book.json +1 -3
  7. package/assets/templates/coding.json +140 -0
  8. package/lib/actions/audio.d.ts +2 -2
  9. package/lib/actions/audio.js +88 -101
  10. package/lib/actions/images.d.ts +1 -1
  11. package/lib/actions/images.js +50 -88
  12. package/lib/actions/index.d.ts +5 -0
  13. package/lib/actions/index.js +5 -0
  14. package/lib/actions/movie.d.ts +9 -1
  15. package/lib/actions/movie.js +124 -65
  16. package/lib/actions/pdf.d.ts +2 -0
  17. package/lib/actions/pdf.js +211 -0
  18. package/lib/actions/pdf2.d.ts +2 -0
  19. package/lib/actions/pdf2.js +203 -0
  20. package/lib/actions/translate.d.ts +1 -1
  21. package/lib/actions/translate.js +38 -61
  22. package/lib/agents/add_bgm_agent.d.ts +1 -1
  23. package/lib/agents/add_bgm_agent.js +10 -14
  24. package/lib/agents/anthropic_agent.d.ts +23 -0
  25. package/lib/agents/anthropic_agent.js +162 -0
  26. package/lib/agents/combine_audio_files_agent.d.ts +1 -1
  27. package/lib/agents/combine_audio_files_agent.js +33 -32
  28. package/lib/agents/image_google_agent.d.ts +1 -1
  29. package/lib/agents/image_google_agent.js +8 -11
  30. package/lib/agents/image_openai_agent.js +7 -14
  31. package/lib/agents/index.d.ts +8 -8
  32. package/lib/agents/index.js +13 -30
  33. package/lib/agents/mulmo_prompts_agent.d.ts +1 -1
  34. package/lib/agents/mulmo_prompts_agent.js +7 -11
  35. package/lib/agents/nested_agent.d.ts +9 -0
  36. package/lib/agents/nested_agent.js +138 -0
  37. package/lib/agents/prompts_data.js +1 -4
  38. package/lib/agents/tts_nijivoice_agent.d.ts +1 -1
  39. package/lib/agents/tts_nijivoice_agent.js +8 -12
  40. package/lib/agents/tts_openai_agent.js +9 -16
  41. package/lib/agents/validate_mulmo_script_agent.d.ts +1 -1
  42. package/lib/agents/validate_mulmo_script_agent.js +6 -10
  43. package/lib/cli/args.d.ts +5 -2
  44. package/lib/cli/args.js +52 -35
  45. package/lib/cli/cli.d.ts +14 -0
  46. package/lib/cli/cli.js +74 -57
  47. package/lib/cli/common.js +1 -5
  48. package/lib/cli/tool-args.d.ts +4 -1
  49. package/lib/cli/tool-args.js +29 -18
  50. package/lib/cli/tool-cli.js +34 -51
  51. package/lib/methods/index.d.ts +4 -3
  52. package/lib/methods/index.js +4 -19
  53. package/lib/methods/mulmo_media_source.d.ts +4 -0
  54. package/lib/methods/mulmo_media_source.js +21 -0
  55. package/lib/methods/mulmo_script.d.ts +6 -5
  56. package/lib/methods/mulmo_script.js +29 -16
  57. package/lib/methods/mulmo_script_template.d.ts +1 -1
  58. package/lib/methods/mulmo_script_template.js +4 -10
  59. package/lib/methods/mulmo_studio_context.d.ts +1 -1
  60. package/lib/methods/mulmo_studio_context.js +3 -9
  61. package/lib/tools/create_mulmo_script_from_url.d.ts +3 -0
  62. package/lib/tools/create_mulmo_script_from_url.js +152 -0
  63. package/lib/tools/create_mulmo_script_interactively.d.ts +3 -0
  64. package/lib/tools/create_mulmo_script_interactively.js +258 -0
  65. package/lib/tools/dump_prompt.js +5 -8
  66. package/lib/tools/prompt.js +9 -11
  67. package/lib/tools/seed_from_url2.d.ts +3 -0
  68. package/lib/tools/seed_from_url2.js +154 -0
  69. package/lib/types/index.d.ts +2 -1
  70. package/lib/types/index.js +2 -17
  71. package/lib/types/schema.d.ts +3624 -2798
  72. package/lib/types/schema.js +172 -123
  73. package/lib/types/type.d.ts +34 -3
  74. package/lib/types/type.js +1 -2
  75. package/lib/utils/const.d.ts +4 -1
  76. package/lib/utils/const.js +6 -6
  77. package/lib/utils/file.d.ts +22 -4
  78. package/lib/utils/file.js +100 -79
  79. package/lib/utils/filters.d.ts +1 -0
  80. package/lib/utils/filters.js +47 -26
  81. package/lib/utils/image_plugins/chart.d.ts +3 -0
  82. package/lib/utils/image_plugins/chart.js +18 -0
  83. package/lib/utils/image_plugins/image.d.ts +2 -0
  84. package/lib/utils/image_plugins/image.js +3 -0
  85. package/lib/utils/image_plugins/index.d.ts +7 -0
  86. package/lib/utils/image_plugins/index.js +7 -0
  87. package/lib/utils/image_plugins/markdown.d.ts +3 -0
  88. package/lib/utils/image_plugins/markdown.js +11 -0
  89. package/lib/utils/image_plugins/mermaid.d.ts +3 -0
  90. package/lib/utils/image_plugins/mermaid.js +21 -0
  91. package/lib/utils/image_plugins/movie.d.ts +2 -0
  92. package/lib/utils/image_plugins/movie.js +3 -0
  93. package/lib/utils/image_plugins/source.d.ts +4 -0
  94. package/lib/utils/image_plugins/source.js +15 -0
  95. package/lib/utils/image_plugins/text_slide.d.ts +3 -0
  96. package/lib/utils/image_plugins/text_slide.js +12 -0
  97. package/lib/utils/image_plugins/type_guards.d.ts +6 -0
  98. package/lib/utils/image_plugins/type_guards.js +21 -0
  99. package/lib/utils/image_preprocess.d.ts +14 -0
  100. package/lib/utils/image_preprocess.js +52 -0
  101. package/lib/utils/inquirer.d.ts +2 -0
  102. package/lib/utils/inquirer.js +33 -0
  103. package/lib/utils/markdown.d.ts +3 -1
  104. package/lib/utils/markdown.js +20 -19
  105. package/lib/utils/pdf.d.ts +8 -0
  106. package/lib/utils/pdf.js +75 -0
  107. package/lib/utils/plugins.d.ts +5 -0
  108. package/lib/utils/plugins.js +11 -0
  109. package/lib/utils/preprocess.d.ts +70 -123
  110. package/lib/utils/preprocess.js +37 -43
  111. package/lib/utils/string.js +4 -10
  112. package/lib/utils/text_hash.js +2 -39
  113. package/lib/utils/utils.d.ts +12 -0
  114. package/lib/utils/utils.js +34 -0
  115. package/package.json +23 -8
@@ -1,51 +1,45 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
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
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.movie = void 0;
7
- const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg"));
8
- const methods_1 = require("../methods");
9
- const file_1 = require("../utils/file");
10
- const createVideo = (audioPath, outputVideoPath, studio) => {
11
- const start = performance.now();
12
- let command = (0, fluent_ffmpeg_1.default)();
13
- if (studio.beats.some((beat) => !beat.imageFile)) {
14
- console.error("beat.imageFile is not set. Please run `yarn run images ${file}` ");
15
- return;
16
- }
17
- // Add each image input
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 ${imageCount /* + captionCount*/}:a`, // Map the audio stream (audio is the next input after all images)
48
- "-c:v h264_videotoolbox", // Set video codec
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
- .on("start", (__cmdLine) => {
60
- console.log("Started FFmpeg ..."); // with command:', cmdLine);
61
- })
62
- .on("error", (err, stdout, stderr) => {
63
- console.error("Error occurred:", err);
64
- console.error("FFmpeg stdout:", stdout);
65
- console.error("FFmpeg stderr:", stderr);
66
- })
67
- .on("end", () => {
68
- const end = performance.now();
69
- console.log(`Video created successfully! ${Math.round(end - start) / 1000} sec`);
70
- })
71
- .output(outputVideoPath)
72
- .run();
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 audioPath = (0, file_1.getOutputBGMFilePath)(outDirPath, studio.filename);
78
- const outputVideoPath = (0, file_1.getOutputVideoFilePath)(outDirPath, studio.filename);
79
- createVideo(audioPath, outputVideoPath, studio);
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,2 @@
1
+ import { MulmoStudioContext, PDFMode, PDFSize } from "../types/index.js";
2
+ export declare const pdf: (context: MulmoStudioContext, pdfMode: PDFMode, pdfSize: PDFSize) => Promise<void>;
@@ -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,2 @@
1
+ import { MulmoStudioContext, PDFMode, PDFSize } from "../types/index.js";
2
+ export declare const pdf: (context: MulmoStudioContext, pdfMode: PDFMode, pdfSize: PDFSize) => Promise<void>;
@@ -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
+ };
@@ -1,3 +1,3 @@
1
1
  import "dotenv/config";
2
- import { MulmoStudioContext } from "../types";
2
+ import { MulmoStudioContext } from "../types/index.js";
3
3
  export declare const translate: (context: MulmoStudioContext) => Promise<void>;