@aigne/doc-smith 0.9.9-beta → 0.9.9-beta.1

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 (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/agents/create/aggregate-document-structure.mjs +21 -0
  3. package/agents/create/analyze-diagram-type-llm.yaml +1 -2
  4. package/agents/create/analyze-diagram-type.mjs +160 -2
  5. package/agents/create/generate-diagram-image.yaml +31 -0
  6. package/agents/create/generate-structure.yaml +1 -12
  7. package/agents/create/replace-d2-with-image.mjs +12 -27
  8. package/agents/create/utils/merge-document-structures.mjs +9 -3
  9. package/agents/localize/index.yaml +4 -0
  10. package/agents/localize/save-doc-translation-or-skip.mjs +18 -0
  11. package/agents/localize/set-review-content.mjs +58 -0
  12. package/agents/localize/translate-diagram.yaml +62 -0
  13. package/agents/localize/translate-document-wrapper.mjs +34 -0
  14. package/agents/localize/translate-multilingual.yaml +15 -9
  15. package/agents/localize/translate-or-skip-diagram.mjs +52 -0
  16. package/agents/publish/translate-meta.mjs +58 -6
  17. package/agents/update/generate-diagram.yaml +25 -8
  18. package/agents/update/index.yaml +1 -8
  19. package/agents/update/save-and-translate-document.mjs +5 -1
  20. package/agents/update/update-single/update-single-document-detail.mjs +52 -10
  21. package/agents/utils/analyze-feedback-intent.mjs +197 -80
  22. package/agents/utils/check-detail-result.mjs +14 -1
  23. package/agents/utils/choose-docs.mjs +3 -43
  24. package/agents/utils/save-doc-translation.mjs +2 -33
  25. package/agents/utils/save-doc.mjs +3 -37
  26. package/aigne.yaml +2 -2
  27. package/package.json +1 -1
  28. package/prompts/detail/diagram/generate-image-user.md +49 -0
  29. package/utils/d2-utils.mjs +10 -3
  30. package/utils/delete-diagram-images.mjs +3 -3
  31. package/utils/diagram-version-utils.mjs +14 -0
  32. package/utils/image-compress.mjs +1 -1
  33. package/utils/sync-diagram-to-translations.mjs +3 -3
  34. package/utils/translate-diagram-images.mjs +790 -0
  35. package/agents/update/check-sync-image-flag.mjs +0 -55
  36. package/agents/update/sync-images-and-exit.mjs +0 -148
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.9-beta.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.9-beta...v0.9.9-beta.1) (2025-12-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * **diagram:** enhance localization with translated images ([#354](https://github.com/AIGNE-io/aigne-doc-smith/issues/354)) ([7abe041](https://github.com/AIGNE-io/aigne-doc-smith/commit/7abe04123005f72d919731b9a69ecbdfff794fb3))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * ensure original document structure is initialized to avoid errors ([#356](https://github.com/AIGNE-io/aigne-doc-smith/issues/356)) ([885a46e](https://github.com/AIGNE-io/aigne-doc-smith/commit/885a46e83036d59df5ec41e7f034725d4ad781b0))
14
+
3
15
  ## [0.9.9-beta](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.8...v0.9.9-beta) (2025-12-09)
4
16
 
5
17
 
@@ -0,0 +1,21 @@
1
+ import { saveValueToConfig } from "../../utils/utils.mjs";
2
+
3
+ export default async function aggregateDocumentStructure(input, options) {
4
+ options.context.userContext.originalDocumentStructure ??= [];
5
+ const projectName = input.projectName || options.context.userContext.projectName;
6
+ const projectDesc = input.projectDesc || options.context.userContext.projectDesc;
7
+
8
+ if (!input.projectDesc && projectDesc) {
9
+ await saveValueToConfig("projectDesc", projectDesc, "Project description");
10
+ }
11
+
12
+ return {
13
+ documentStructure: options.context.userContext.originalDocumentStructure.map((i, index) => ({
14
+ ...i,
15
+ id: i.title.toLowerCase().replace(/\s+/g, "-"),
16
+ index,
17
+ })),
18
+ projectName,
19
+ projectDesc,
20
+ };
21
+ }
@@ -16,7 +16,7 @@ instructions: |
16
16
 
17
17
  Your responsibilities:
18
18
 
19
- 1. **Analyze Context**: Understand the documents content, structure, and its purpose, especially around where the diagram will be inserted.
19
+ 1. **Analyze Context**: Understand the document's content, structure, and its purpose, especially around where the diagram will be inserted.
20
20
 
21
21
  2. **Generate Document Summary**:
22
22
  **CRITICAL**: The documentSummary will be the **only input** passed to the image generation model. Preserve as much information as possible, only removing content that is truly useless for diagram generation.
@@ -157,4 +157,3 @@ output_schema:
157
157
  - diagramType
158
158
  - diagramStyle
159
159
  - aspectRatio
160
-
@@ -1,4 +1,6 @@
1
1
  import { DIAGRAM_STYLES } from "../../utils/constants/index.mjs";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
2
4
 
3
5
  const DEFAULT_DIAGRAM_STYLE = "modern";
4
6
  const DEFAULT_DIAGRAM_TYPE = "flowchart";
@@ -76,10 +78,80 @@ const STYLE_REQUIREMENTS = {
76
78
  - Clear depth cues and perspective`,
77
79
  };
78
80
 
81
+ /**
82
+ * Convert diagramInfo from analyzeFeedbackIntent to mediaFile format
83
+ * @param {Object} diagramInfo - Diagram info from analyzeFeedbackIntent (contains path, index, markdown)
84
+ * @param {string} docPath - Document path
85
+ * @param {string} docsDir - Documentation directory
86
+ * @returns {Promise<Array|null>} - Array of mediaFile objects or null if conversion fails
87
+ * Note: Currently each document has only one diagram, so we always use the first (and only) image
88
+ */
89
+ async function convertDiagramInfoToMediaFile(diagramInfo, docPath, docsDir) {
90
+ if (!diagramInfo || !diagramInfo.path) {
91
+ return null;
92
+ }
93
+
94
+ try {
95
+ const imagePath = diagramInfo.path;
96
+ const docDir = path.dirname(docPath);
97
+
98
+ // Resolve absolute path
99
+ let absolutePath;
100
+ if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
101
+ // Remote URL, cannot convert to local file
102
+ return null;
103
+ } else if (path.isAbsolute(imagePath)) {
104
+ absolutePath = imagePath;
105
+ } else {
106
+ // Relative path - resolve from document location
107
+ const imageRelativePath = imagePath.startsWith("../")
108
+ ? imagePath
109
+ : path.join(docDir, imagePath).replace(/\\/g, "/");
110
+ absolutePath = path.join(process.cwd(), docsDir, imageRelativePath);
111
+ }
112
+
113
+ // Normalize path
114
+ const normalizedPath = path.normalize(absolutePath);
115
+
116
+ // Check if file exists
117
+ if (!(await fs.pathExists(normalizedPath))) {
118
+ return null;
119
+ }
120
+
121
+ // Get file info
122
+ const ext = path.extname(normalizedPath).toLowerCase().slice(1);
123
+ const filename = path.basename(normalizedPath);
124
+
125
+ // Determine MIME type
126
+ const mimeTypes = {
127
+ jpg: "image/jpeg",
128
+ jpeg: "image/jpeg",
129
+ png: "image/png",
130
+ webp: "image/webp",
131
+ gif: "image/gif",
132
+ };
133
+ const mimeType = mimeTypes[ext] || "image/png";
134
+
135
+ // Return mediaFile format (array as required by input_file_key)
136
+ return [
137
+ {
138
+ type: "local",
139
+ path: normalizedPath,
140
+ filename,
141
+ mimeType,
142
+ },
143
+ ];
144
+ } catch (error) {
145
+ console.warn(`Failed to convert diagramInfo to mediaFile: ${error.message}`);
146
+ return null;
147
+ }
148
+ }
149
+
79
150
  /**
80
151
  * Analyze document content to determine diagram type and select appropriate style
81
152
  * Uses LLM analysis to determine diagram type and style
82
153
  * Supports extracting style and type preferences from user feedback
154
+ * Now also detects existing images and determines if image-to-image generation should be used
83
155
  */
84
156
  export default async function analyzeDiagramType(
85
157
  {
@@ -89,6 +161,10 @@ export default async function analyzeDiagramType(
89
161
  diagramming,
90
162
  locale = "en",
91
163
  feedback = "",
164
+ path,
165
+ docsDir,
166
+ // Analysis results from first layer (analyzeFeedbackIntent)
167
+ intentAnalysis,
92
168
  },
93
169
  options,
94
170
  ) {
@@ -97,7 +173,7 @@ export default async function analyzeDiagramType(
97
173
  defaultStyle = diagramming.style;
98
174
  }
99
175
 
100
- // Step 1: Use LLM to analyze and make final decision (LLM will analyze feedback directly)
176
+ // Step 1: Use LLM to analyze document content (chart detection and intent analysis already done in first layer)
101
177
  const llmAgent = options.context?.agents?.["analyzeDiagramTypeLLM"];
102
178
  let llmResult = null;
103
179
 
@@ -129,6 +205,8 @@ export default async function analyzeDiagramType(
129
205
  locale,
130
206
  feedback: feedback || "",
131
207
  defaultStyle: defaultStyle || null,
208
+ // Note: We only analyze current documentContent, no originalContent comparison
209
+ // User feedback is the only indicator for whether diagram needs update
132
210
  };
133
211
 
134
212
  llmResult = await options.context.invoke(llmAgent, llmInput);
@@ -187,7 +265,22 @@ export default async function analyzeDiagramType(
187
265
  aspectRatio = "4:3";
188
266
  }
189
267
 
190
- // Step 7: Return document content and summary for image generation
268
+ // Step 2: Use analysis results from first layer (analyzeFeedbackIntent)
269
+ // Get generationMode and diagramInfo from intentAnalysis
270
+ const generationMode = intentAnalysis?.generationMode || "add-new";
271
+ const diagramInfo = intentAnalysis?.diagramInfo || null;
272
+
273
+ // Step 3: Convert diagramInfo to mediaFile format if needed for image-to-image generation
274
+ let existingImage = null;
275
+ let useImageToImage = false;
276
+
277
+ if (diagramInfo && generationMode === "image-to-image" && path && docsDir) {
278
+ // Convert diagramInfo to the format expected by image generation agent
279
+ existingImage = await convertDiagramInfoToMediaFile(diagramInfo, path, docsDir);
280
+ useImageToImage = existingImage !== null;
281
+ }
282
+
283
+ // Step 3: Return document content and summary for image generation
191
284
  return {
192
285
  diagramType,
193
286
  diagramStyle,
@@ -197,6 +290,10 @@ export default async function analyzeDiagramType(
197
290
  diagramTypeRequirements,
198
291
  diagramStyleRequirements,
199
292
  negativePromptExclusions,
293
+ // Image-to-image generation support (from LLM analysis)
294
+ existingImage, // Array of mediaFile objects or null
295
+ useImageToImage, // Boolean indicating if image-to-image mode should be used
296
+ generationMode, // Generation mode from LLM: "text-only", "image-to-image", "remove-image", "add-new"
200
297
  };
201
298
  }
202
299
 
@@ -241,6 +338,15 @@ analyzeDiagramType.input_schema = {
241
338
  "User feedback that may contain style or type preferences (e.g., 'use anthropomorphic style', 'create architecture diagram')",
242
339
  default: "",
243
340
  },
341
+ path: {
342
+ type: "string",
343
+ description:
344
+ "Document path (e.g., 'guides/getting-started.md') used for extracting existing images",
345
+ },
346
+ docsDir: {
347
+ type: "string",
348
+ description: "Documentation directory where diagram images are stored",
349
+ },
244
350
  },
245
351
  required: ["documentContent"],
246
352
  };
@@ -283,6 +389,57 @@ analyzeDiagramType.output_schema = {
283
389
  description:
284
390
  "A concise summary of the document content focusing on key elements needed for diagram generation. This summary is generated by the analysis LLM to ensure consistent understanding between analysis and image generation models.",
285
391
  },
392
+ existingImage: {
393
+ type: "array",
394
+ nullable: true,
395
+ description:
396
+ "Array of mediaFile objects for existing diagram image (for image-to-image generation). Null if no existing image or text-only regeneration requested.",
397
+ items: {
398
+ type: "object",
399
+ properties: {
400
+ type: { type: "string" },
401
+ path: { type: "string" },
402
+ filename: { type: "string" },
403
+ mimeType: { type: "string" },
404
+ },
405
+ },
406
+ },
407
+ useImageToImage: {
408
+ type: "boolean",
409
+ description:
410
+ "Whether to use image-to-image generation mode. True if existing image found and generationMode is 'image-to-image'.",
411
+ },
412
+ generationMode: {
413
+ type: "string",
414
+ description: "Generation mode determined from intentAnalysis (from analyzeFeedbackIntent).",
415
+ },
416
+ intentAnalysis: {
417
+ type: "object",
418
+ description:
419
+ "Analysis results from analyzeFeedbackIntent containing intentType, diagramInfo, generationMode, and changes.",
420
+ properties: {
421
+ intentType: {
422
+ type: "string",
423
+ enum: ["addDiagram", "updateDiagram", "deleteDiagram", "updateDocument"],
424
+ },
425
+ diagramInfo: {
426
+ type: "object",
427
+ nullable: true,
428
+ properties: {
429
+ path: { type: "string" },
430
+ index: { type: "number" },
431
+ markdown: { type: "string" },
432
+ },
433
+ },
434
+ generationMode: {
435
+ type: "string",
436
+ },
437
+ changes: {
438
+ type: "array",
439
+ items: { type: "string" },
440
+ },
441
+ },
442
+ },
286
443
  },
287
444
  required: [
288
445
  "diagramType",
@@ -293,5 +450,6 @@ analyzeDiagramType.output_schema = {
293
450
  "diagramStyleRequirements",
294
451
  "negativePromptExclusions",
295
452
  "documentContent",
453
+ "useImageToImage",
296
454
  ],
297
455
  };
@@ -20,6 +20,10 @@ instructions:
20
20
  - role: user
21
21
  url: ../../prompts/detail/diagram/generate-image-user.md
22
22
 
23
+ # Support image-to-image generation by passing existing image via input_file_key
24
+ # If existingImage is provided (array of mediaFile objects), it will be passed to the model
25
+ input_file_key: existingImage
26
+
23
27
  input_schema:
24
28
  type: object
25
29
  properties:
@@ -51,6 +55,33 @@ input_schema:
51
55
  type: string
52
56
  description: Aspect ratio of the generated image (alias for ratio, used in prompt templates)
53
57
  enum: ["1:1", "5:4", "4:3", "3:2", "16:9", "21:9"]
58
+ existingImage:
59
+ type: array
60
+ nullable: true
61
+ description: Array of mediaFile objects for existing diagram image (for image-to-image generation). If provided, the model will use this as reference and update based on document content and feedback.
62
+ items:
63
+ type: object
64
+ properties:
65
+ type:
66
+ type: string
67
+ description: File type, should be "local"
68
+ path:
69
+ type: string
70
+ description: Absolute path to the image file
71
+ filename:
72
+ type: string
73
+ description: Image filename
74
+ mimeType:
75
+ type: string
76
+ description: MIME type of the image (e.g., "image/png", "image/jpeg")
77
+ useImageToImage:
78
+ type: boolean
79
+ description: Whether to use image-to-image generation mode. If true and existingImage is provided, the model will update the existing image based on document content and feedback.
80
+ default: false
81
+ feedback:
82
+ type: string
83
+ description: User feedback for diagram updates. Used in image-to-image mode to guide modifications.
84
+ default: ""
54
85
  required:
55
86
  - documentContent
56
87
  - diagramType
@@ -65,18 +65,7 @@ skills:
65
65
 
66
66
  - ./utils/merge-document-structures.mjs
67
67
 
68
- - type: function
69
- name: aggregateDocumentStructure
70
- process: |
71
- return {
72
- documentStructure: options.context.userContext.originalDocumentStructure.map((i, index) => ({
73
- ...i,
74
- id: i.title.toLowerCase().replace(/\s+/g, '-'),
75
- index
76
- })),
77
- projectName: options.context.userContext.projectName,
78
- projectDesc: options.context.userContext.projectDesc,
79
- }
68
+ - ./aggregate-document-structure.mjs
80
69
 
81
70
  - type: ai
82
71
  name: refineStructure
@@ -13,6 +13,7 @@ import { getContentHash, getFileName } from "../../utils/utils.mjs";
13
13
  import { getExtnameFromContentType } from "../../utils/file-utils.mjs";
14
14
  import { debug } from "../../utils/debug.mjs";
15
15
  import { compressImage } from "../../utils/image-compress.mjs";
16
+ import { calculateImageTimestamp } from "../../utils/diagram-version-utils.mjs";
16
17
 
17
18
  const SIZE_THRESHOLD = 1 * 1024 * 1024; // 1MB
18
19
 
@@ -304,10 +305,19 @@ export default async function replaceD2WithImage({
304
305
  }
305
306
 
306
307
  // Create markdown image reference with markers for easy replacement
307
- // Format: <!-- DIAGRAM_IMAGE_START:type:aspectRatio -->![alt](path)<!-- DIAGRAM_IMAGE_END -->
308
+ // Format: <!-- DIAGRAM_IMAGE_START:type:aspectRatio:timestamp -->![alt](path)<!-- DIAGRAM_IMAGE_END -->
308
309
  const diagramTypeTag = diagramType || "unknown";
309
310
  const aspectRatioTag = aspectRatio || "unknown";
310
- const imageMarkdown = `<!-- DIAGRAM_IMAGE_START:${diagramTypeTag}:${aspectRatioTag} -->\n![${altText}](${relativePath})\n<!-- DIAGRAM_IMAGE_END -->`;
311
+
312
+ // Calculate timestamp for the saved image (for version tracking)
313
+ let imageTimestamp = "0";
314
+ try {
315
+ imageTimestamp = await calculateImageTimestamp(destPath);
316
+ } catch (error) {
317
+ debug(`Failed to calculate image timestamp: ${error.message}, using default 0`);
318
+ }
319
+
320
+ const imageMarkdown = `<!-- DIAGRAM_IMAGE_START:${diagramTypeTag}:${aspectRatioTag}:${imageTimestamp} -->\n![${altText}](${relativePath})\n<!-- DIAGRAM_IMAGE_END -->`;
311
321
 
312
322
  // Note: diagramLocations was already found above for filename generation, reuse it
313
323
 
@@ -373,31 +383,6 @@ export default async function replaceD2WithImage({
373
383
  finalContent = trimmedContent + separator + imageMarkdown;
374
384
  }
375
385
 
376
- // Sync diagram images to translation files
377
- // Only sync if we actually replaced/added a diagram (not just returned original content)
378
- if (finalContent !== (originalContent || documentContent || content || "")) {
379
- try {
380
- const { syncDiagramToTranslations } = await import(
381
- "../../utils/sync-diagram-to-translations.mjs"
382
- );
383
- const syncResult = await syncDiagramToTranslations(
384
- finalContent,
385
- finalDocPath,
386
- finalDocsDir,
387
- locale,
388
- "update", // Operation type: update - skip if main has 0 diagrams
389
- );
390
- if (syncResult.updated > 0) {
391
- debug(
392
- `✅ Synced diagram images to ${syncResult.updated} translation file(s)${syncResult.errors.length > 0 ? ` (${syncResult.errors.length} error(s))` : ""}`,
393
- );
394
- }
395
- } catch (error) {
396
- // Don't fail the whole operation if sync fails
397
- debug(`⚠️ Failed to sync diagram to translations: ${error.message}`);
398
- }
399
- }
400
-
401
386
  return { content: finalContent };
402
387
  }
403
388
 
@@ -1,4 +1,5 @@
1
1
  export default async function mergeDocumentStructures(input, options) {
2
+ // Save projectName and projectDesc to userContext if provided
2
3
  if (input.projectName) {
3
4
  options.context.userContext.projectName = input.projectName;
4
5
  }
@@ -6,8 +7,9 @@ export default async function mergeDocumentStructures(input, options) {
6
7
  options.context.userContext.projectDesc = input.projectDesc;
7
8
  }
8
9
 
9
- input.projectName = options.context.userContext.projectName;
10
- input.projectDesc = options.context.userContext.projectDesc;
10
+ // Get the final values (either from input or existing userContext)
11
+ const projectName = options.context.userContext.projectName;
12
+ const projectDesc = options.context.userContext.projectDesc;
11
13
 
12
14
  options.context.userContext.originalDocumentStructure ??= [];
13
15
 
@@ -26,5 +28,9 @@ export default async function mergeDocumentStructures(input, options) {
26
28
 
27
29
  options.context.userContext.originalDocumentStructure = originalStructures;
28
30
 
29
- return {};
31
+ // Return projectName and projectDesc to ensure they flow through the pipeline
32
+ return {
33
+ projectName,
34
+ projectDesc,
35
+ };
30
36
  }
@@ -19,6 +19,7 @@ skills:
19
19
  'documentStructure': originalDocumentStructure
20
20
  }
21
21
  ])
22
+ - url: ../update/check-diagram-flag.mjs
22
23
  - url: ../utils/choose-docs.mjs
23
24
  default_input:
24
25
  isTranslate: true
@@ -55,4 +56,7 @@ input_schema:
55
56
  feedback:
56
57
  type: string
57
58
  description: Tell us how to improve the translation style
59
+ diagram:
60
+ type: boolean
61
+ description: Translate only diagram images without translating document content (use --diagram flag)
58
62
  mode: sequential
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Save translated document
3
+ * Always saves the translation content, regardless of --diagram flag.
4
+ * In --diagram mode, the translation content may only have updated diagram images,
5
+ * but it still needs to be saved to persist the image changes.
6
+ */
7
+ export default async function saveDocTranslationOrSkip(input, options) {
8
+ // Always save translation content, whether it's a full translation or just diagram image updates
9
+ // The translation content (from set-review-content.mjs) already contains the updated diagram images
10
+ const saveDocTranslationAgent = options.context?.agents?.["saveDocTranslation"];
11
+ if (saveDocTranslationAgent) {
12
+ return await options.context.invoke(saveDocTranslationAgent, input);
13
+ }
14
+
15
+ return {};
16
+ }
17
+
18
+ saveDocTranslationOrSkip.task_render_mode = "hide";
@@ -0,0 +1,58 @@
1
+ import { debug } from "../../utils/debug.mjs";
2
+ import { d2CodeBlockRegex, diagramImageFullRegex } from "../../utils/d2-utils.mjs";
3
+
4
+ /**
5
+ * Replace cached diagram images in translated document and set reviewContent
6
+ * This step runs after translate-document-wrapper.mjs to:
7
+ * 1. Insert cached image translations into the translated document
8
+ * 2. Set reviewContent from the final translation
9
+ * Note: Each document has at most one diagram image
10
+ */
11
+ export default async function setReviewContent(input) {
12
+ let translation = input.translation || "";
13
+ const cachedImages = input.cachedDiagramImages || null;
14
+
15
+ // Replace cached diagram image if any (each document has at most one image)
16
+ if (cachedImages && cachedImages.length > 0) {
17
+ try {
18
+ const cachedImage = cachedImages[0]; // Only one image per document
19
+
20
+ // Find existing image in translated content
21
+ // Note: Translation process may copy content from main document, so we always need to
22
+ // replace with cached image to ensure the final document uses the correct language-specific image
23
+ const imageMatch = translation.match(diagramImageFullRegex);
24
+
25
+ if (imageMatch) {
26
+ translation = translation.replace(diagramImageFullRegex, cachedImage.translatedMarkdown);
27
+ debug(`✅ Replaced diagram image in translation with language-specific image`);
28
+ } else {
29
+ const d2Match = translation.match(d2CodeBlockRegex);
30
+ if (d2Match) {
31
+ translation = translation.replace(d2CodeBlockRegex, cachedImage.translatedMarkdown);
32
+ debug(`✅ Replaced D2 code block in translation with language-specific image`);
33
+ } else {
34
+ // No existing image, insert at the position from main document
35
+ const insertIndex = Math.min(cachedImage.mainImageIndex, translation.length);
36
+ translation =
37
+ translation.slice(0, insertIndex) +
38
+ "\n\n" +
39
+ cachedImage.translatedMarkdown +
40
+ "\n\n" +
41
+ translation.slice(insertIndex);
42
+ debug(`✅ Inserted diagram image at index ${insertIndex}`);
43
+ }
44
+ }
45
+ } catch (error) {
46
+ debug(`⚠️ Failed to replace cached diagram image: ${error.message}`);
47
+ // Continue with original translation if replacement fails
48
+ }
49
+ }
50
+
51
+ return {
52
+ ...input,
53
+ translation,
54
+ reviewContent: translation || input.content || "",
55
+ };
56
+ }
57
+
58
+ setReviewContent.task_render_mode = "hide";
@@ -0,0 +1,62 @@
1
+ type: image
2
+ name: translateDiagram
3
+ image_model:
4
+ model: google/gemini-3-pro-image-preview
5
+ imageConfig:
6
+ imageSize:
7
+ $get: size
8
+ aspectRatio:
9
+ $get: ratio
10
+
11
+ instructions: |
12
+ You are a multilingual diagram translator specializing in technical diagrams such as flowcharts, architecture diagrams, network topologies, and mind maps.
13
+
14
+ Your task is to regenerate the provided image by translating all label-style text into {{locale}}.
15
+
16
+ Requirements:
17
+ - Retain all command-style, technical, or protocol-specific terms (e.g., "API", "TCP/IP", "POST") in English;
18
+ - Maintain the original diagram’s structure, layout, visual style, and spatial relationships between elements;
19
+ - Only translate label or annotation text — all graphical elements should remain unchanged;
20
+ - **If {{locale}} is Simplified Chinese, Traditional Chinese, Japanese, or Korean, use the font "Microsoft YaHei" (微软雅黑)** to ensure optimal readability, proper character rendering, and consistent typographic style across Latin and CJK scripts;
21
+ - For other languages, use a clean sans-serif font that best fits the visual style of the original image;
22
+
23
+ Your final output should be a regenerated image identical in design but localized in text according to {{locale}}.
24
+
25
+ input_file_key: existingImage
26
+
27
+ input_schema:
28
+ type: object
29
+ properties:
30
+ existingImage:
31
+ type: array
32
+ description: Array of mediaFile objects for the source diagram image to translate
33
+ items:
34
+ type: object
35
+ properties:
36
+ type:
37
+ type: string
38
+ description: File type, should be "local"
39
+ path:
40
+ type: string
41
+ description: Absolute path to the image file
42
+ filename:
43
+ type: string
44
+ description: Image filename
45
+ mimeType:
46
+ type: string
47
+ description: MIME type of the image (e.g., "image/png", "image/jpeg")
48
+ ratio:
49
+ type: string
50
+ description: Aspect ratio of the image
51
+ size:
52
+ type: string
53
+ description: Size of the generated image
54
+ default: "1K"
55
+ locale:
56
+ type: string
57
+ description: Target language code for translation
58
+ required:
59
+ - existingImage
60
+ - ratio
61
+ - locale
62
+ include_input_in_output: true
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Conditionally call translateDocument agent
3
+ * If translation is already set and approved (--diagram mode with existing translation), skip calling translateDocument
4
+ * Otherwise, call translateDocument agent normally (including --diagram mode when translation doesn't exist)
5
+ */
6
+ export default async function translateDocumentWrapper(input, options) {
7
+ // If translation is already set and approved, skip document translation
8
+ // This happens in --diagram mode when translation document exists (translate-diagram-images.mjs sets isApproved: true)
9
+ // In --diagram mode when translation doesn't exist, translate-diagram-images.mjs sets isApproved: false to allow translation
10
+ if (input.translation !== undefined && input.translation !== null && input.isApproved === true) {
11
+ return input;
12
+ }
13
+
14
+ // Otherwise, call translateDocument agent (YAML-defined AI agent)
15
+ const translateAgent = options.context?.agents?.["translateDocument"];
16
+ if (!translateAgent) {
17
+ throw new Error("translateDocument agent not found");
18
+ }
19
+
20
+ try {
21
+ const result = await options.context.invoke(translateAgent, input);
22
+ return {
23
+ ...input,
24
+ ...result,
25
+ translation: result?.translation || result,
26
+ // Preserve shouldTranslateDiagramsOnly flag
27
+ shouldTranslateDiagramsOnly: input.shouldTranslateDiagramsOnly,
28
+ };
29
+ } catch (error) {
30
+ throw new Error(`Failed to invoke translateDocument agent: ${error.message}`);
31
+ }
32
+ }
33
+
34
+ translateDocumentWrapper.task_render_mode = "hide";