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.
Files changed (74) hide show
  1. package/README.md +27 -9
  2. package/assets/font/NotoSansJP-Regular.ttf +0 -0
  3. package/assets/html/chart.html +1 -10
  4. package/assets/html/mermaid.html +1 -13
  5. package/assets/templates/business.json +16 -27
  6. package/assets/templates/coding.json +58 -21
  7. package/lib/actions/audio.d.ts +1 -1
  8. package/lib/actions/audio.js +43 -27
  9. package/lib/actions/images.js +20 -26
  10. package/lib/actions/index.d.ts +5 -0
  11. package/lib/actions/index.js +5 -0
  12. package/lib/actions/movie.d.ts +9 -1
  13. package/lib/actions/movie.js +97 -38
  14. package/lib/actions/pdf.d.ts +2 -0
  15. package/lib/actions/pdf.js +211 -0
  16. package/lib/actions/pdf2.d.ts +2 -0
  17. package/lib/actions/pdf2.js +203 -0
  18. package/lib/actions/translate.js +22 -9
  19. package/lib/agents/anthropic_agent.d.ts +23 -0
  20. package/lib/agents/anthropic_agent.js +162 -0
  21. package/lib/agents/combine_audio_files_agent.js +13 -22
  22. package/lib/agents/nested_agent.d.ts +9 -0
  23. package/lib/agents/nested_agent.js +138 -0
  24. package/lib/cli/args.d.ts +3 -1
  25. package/lib/cli/args.js +49 -34
  26. package/lib/cli/cli.d.ts +14 -0
  27. package/lib/cli/cli.js +48 -46
  28. package/lib/cli/tool-args.d.ts +2 -0
  29. package/lib/cli/tool-args.js +12 -2
  30. package/lib/cli/tool-cli.js +6 -4
  31. package/lib/methods/index.d.ts +1 -0
  32. package/lib/methods/index.js +1 -0
  33. package/lib/methods/mulmo_media_source.d.ts +4 -0
  34. package/lib/methods/mulmo_media_source.js +21 -0
  35. package/lib/methods/mulmo_script.d.ts +2 -6
  36. package/lib/methods/mulmo_script.js +12 -5
  37. package/lib/tools/create_mulmo_script_interactively.d.ts +1 -1
  38. package/lib/tools/create_mulmo_script_interactively.js +61 -20
  39. package/lib/types/index.d.ts +1 -0
  40. package/lib/types/index.js +1 -0
  41. package/lib/types/schema.d.ts +3626 -3162
  42. package/lib/types/schema.js +75 -41
  43. package/lib/types/type.d.ts +28 -1
  44. package/lib/utils/const.d.ts +2 -0
  45. package/lib/utils/const.js +2 -0
  46. package/lib/utils/file.d.ts +4 -1
  47. package/lib/utils/file.js +15 -1
  48. package/lib/utils/filters.js +1 -1
  49. package/lib/utils/image_plugins/chart.d.ts +3 -0
  50. package/lib/utils/image_plugins/chart.js +18 -0
  51. package/lib/utils/image_plugins/image.d.ts +2 -0
  52. package/lib/utils/image_plugins/image.js +3 -0
  53. package/lib/utils/image_plugins/index.d.ts +7 -0
  54. package/lib/utils/image_plugins/index.js +7 -0
  55. package/lib/utils/image_plugins/markdown.d.ts +3 -0
  56. package/lib/utils/image_plugins/markdown.js +11 -0
  57. package/lib/utils/image_plugins/mermaid.d.ts +3 -0
  58. package/lib/utils/image_plugins/mermaid.js +21 -0
  59. package/lib/utils/image_plugins/movie.d.ts +2 -0
  60. package/lib/utils/image_plugins/movie.js +3 -0
  61. package/lib/utils/image_plugins/source.d.ts +4 -0
  62. package/lib/utils/image_plugins/source.js +15 -0
  63. package/lib/utils/image_plugins/text_slide.d.ts +3 -0
  64. package/lib/utils/image_plugins/text_slide.js +12 -0
  65. package/lib/utils/image_plugins/type_guards.d.ts +6 -0
  66. package/lib/utils/image_plugins/type_guards.js +21 -0
  67. package/lib/utils/markdown.js +4 -1
  68. package/lib/utils/pdf.d.ts +8 -0
  69. package/lib/utils/pdf.js +75 -0
  70. package/lib/utils/preprocess.d.ts +58 -128
  71. package/lib/utils/preprocess.js +37 -37
  72. package/lib/utils/utils.d.ts +12 -0
  73. package/lib/utils/utils.js +34 -0
  74. package/package.json +13 -4
@@ -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
- let command = ffmpeg();
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
- studio.beats.forEach((beat, index) => {
21
- // Resize background image to match canvas dimensions
22
- const duration = beat.duration + (index === 0 ? MulmoScriptMethods.getPadding(studio.script) / 1000 : 0);
23
- const parts = `[${index}:v]loop=loop=-1:size=1:start=0,` +
24
- `trim=duration=${duration},` +
25
- `fps=30,` +
26
- `setpts=PTS-STARTPTS,` +
27
- `scale=${canvasInfo.width}:${canvasInfo.height},` +
28
- `setsar=1,format=yuv420p` +
29
- `[v${index}]`;
30
- // console.log(parts);
31
- filterComplexParts.push(parts);
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
- const concatInput = studio.beats.map((_, index) => `[v${index}]`).join("");
35
- filterComplexParts.push(`${concatInput}concat=n=${imageCount}:v=1:a=0[v]`);
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
- .input(audioArtifactFilePath) // Add audio input
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,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
+ };
@@ -32,7 +32,8 @@ const translateGraph = {
32
32
  agent: "mapAgent",
33
33
  inputs: {
34
34
  targetLangs: ":targetLangs",
35
- rows: ":studio.beats",
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: [":beat", { multiLingualTexts: ":mergeLocalizedText" }],
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 (beat.multiLingualTexts &&
162
- beat.multiLingualTexts[lang] &&
163
- beat.multiLingualTexts[lang].text === beat.text &&
164
- beat.multiLingualTexts[targetLang] &&
165
- beat.multiLingualTexts[targetLang].text) {
166
- return { text: beat.multiLingualTexts[targetLang].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;