@aigne/doc-smith 0.9.8 → 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.
- package/CHANGELOG.md +19 -0
- package/agents/create/aggregate-document-structure.mjs +21 -0
- package/agents/create/analyze-diagram-type-llm.yaml +1 -2
- package/agents/create/analyze-diagram-type.mjs +160 -2
- package/agents/create/generate-diagram-image.yaml +31 -0
- package/agents/create/generate-structure.yaml +1 -12
- package/agents/create/replace-d2-with-image.mjs +12 -27
- package/agents/create/utils/merge-document-structures.mjs +9 -3
- package/agents/localize/index.yaml +4 -0
- package/agents/localize/save-doc-translation-or-skip.mjs +18 -0
- package/agents/localize/set-review-content.mjs +58 -0
- package/agents/localize/translate-diagram.yaml +62 -0
- package/agents/localize/translate-document-wrapper.mjs +34 -0
- package/agents/localize/translate-multilingual.yaml +15 -9
- package/agents/localize/translate-or-skip-diagram.mjs +52 -0
- package/agents/publish/translate-meta.mjs +58 -6
- package/agents/update/generate-diagram.yaml +25 -8
- package/agents/update/index.yaml +1 -8
- package/agents/update/save-and-translate-document.mjs +5 -1
- package/agents/update/update-single/update-single-document-detail.mjs +52 -10
- package/agents/utils/analyze-feedback-intent.mjs +197 -80
- package/agents/utils/check-detail-result.mjs +14 -1
- package/agents/utils/choose-docs.mjs +3 -43
- package/agents/utils/save-doc-translation.mjs +2 -33
- package/agents/utils/save-doc.mjs +3 -37
- package/aigne.yaml +2 -2
- package/package.json +1 -1
- package/prompts/detail/diagram/generate-image-user.md +49 -0
- package/utils/d2-utils.mjs +10 -3
- package/utils/delete-diagram-images.mjs +3 -3
- package/utils/diagram-version-utils.mjs +14 -0
- package/utils/file-utils.mjs +40 -5
- package/utils/image-compress.mjs +1 -1
- package/utils/sync-diagram-to-translations.mjs +3 -3
- package/utils/translate-diagram-images.mjs +790 -0
- package/agents/update/check-sync-image-flag.mjs +0 -55
- package/agents/update/sync-images-and-exit.mjs +0 -148
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
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
|
+
|
|
15
|
+
## [0.9.9-beta](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.8...v0.9.9-beta) (2025-12-09)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* implement robust error handling for token calculation ([#352](https://github.com/AIGNE-io/aigne-doc-smith/issues/352)) ([e7ba726](https://github.com/AIGNE-io/aigne-doc-smith/commit/e7ba726e226c05a1ac2b40c74b101e1a2d972091))
|
|
21
|
+
|
|
3
22
|
## [0.9.8](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.8-beta.1...v0.9.8) (2025-12-07)
|
|
4
23
|
|
|
5
24
|
## [0.9.8-beta.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.8-beta...v0.9.8-beta.1) (2025-12-06)
|
|
@@ -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 document
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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 --><!-- DIAGRAM_IMAGE_END -->
|
|
308
|
+
// Format: <!-- DIAGRAM_IMAGE_START:type:aspectRatio:timestamp --><!-- DIAGRAM_IMAGE_END -->
|
|
308
309
|
const diagramTypeTag = diagramType || "unknown";
|
|
309
310
|
const aspectRatioTag = aspectRatio || "unknown";
|
|
310
|
-
|
|
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\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
|
|
10
|
-
|
|
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
|
-
|
|
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";
|