@aigne/doc-smith 0.8.12-beta.7 → 0.8.12-beta.8

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 (47) hide show
  1. package/.aigne/doc-smith/config.yaml +1 -1
  2. package/.aigne/doc-smith/media-description.yaml +91 -0
  3. package/.aigne/doc-smith/upload-cache.yaml +6 -69
  4. package/.release-please-manifest.json +1 -1
  5. package/CHANGELOG.md +12 -0
  6. package/agents/clear/choose-contents.mjs +14 -1
  7. package/agents/clear/clear-media-description.mjs +129 -0
  8. package/agents/clear/index.yaml +3 -1
  9. package/agents/evaluate/code-snippet.mjs +28 -24
  10. package/agents/evaluate/document-structure.yaml +0 -4
  11. package/agents/evaluate/document.yaml +1 -5
  12. package/agents/generate/index.yaml +1 -0
  13. package/agents/init/index.mjs +10 -0
  14. package/agents/media/batch-generate-media-description.yaml +44 -0
  15. package/agents/media/generate-media-description.yaml +47 -0
  16. package/agents/media/load-media-description.mjs +238 -0
  17. package/agents/update/index.yaml +1 -0
  18. package/agents/utils/load-sources.mjs +103 -53
  19. package/aigne.yaml +6 -0
  20. package/assets/report-template/report.html +34 -34
  21. package/docs/configuration-initial-setup.md +74 -55
  22. package/docs/configuration.ja.md +59 -86
  23. package/docs/configuration.md +59 -86
  24. package/docs/configuration.zh-TW.md +59 -86
  25. package/docs/configuration.zh.md +59 -86
  26. package/docs/getting-started.ja.md +43 -24
  27. package/docs/getting-started.md +29 -10
  28. package/docs/getting-started.zh-TW.md +42 -23
  29. package/docs/getting-started.zh.md +39 -20
  30. package/docs/guides-cleaning-up.md +19 -18
  31. package/docs/guides-evaluating-documents.md +70 -29
  32. package/docs/guides-generating-documentation.md +59 -121
  33. package/docs/guides-interactive-chat.md +34 -26
  34. package/docs/guides-managing-history.md +18 -13
  35. package/docs/guides-publishing-your-docs.md +40 -35
  36. package/docs/guides-translating-documentation.md +39 -34
  37. package/docs/guides-updating-documentation.md +11 -9
  38. package/docs/overview.md +2 -2
  39. package/docs/release-notes.md +60 -27
  40. package/package.json +2 -1
  41. package/prompts/evaluate/document-structure.md +6 -7
  42. package/prompts/evaluate/document.md +16 -25
  43. package/prompts/media/media-description/system-prompt.md +35 -0
  44. package/prompts/media/media-description/user-prompt.md +8 -0
  45. package/utils/constants/index.mjs +0 -107
  46. package/utils/file-utils.mjs +41 -0
  47. package/media.md +0 -19
@@ -0,0 +1,44 @@
1
+ type: team
2
+ name: batchGenerateMediaDescription
3
+ description: Batch generate media (image/video) descriptions with concurrency
4
+ skills:
5
+ - url: ./generate-media-description.yaml
6
+ task_render_mode: collapse
7
+ task_title: Generate Media Description
8
+ input_schema:
9
+ type: object
10
+ properties:
11
+ mediaToDescribe:
12
+ type: array
13
+ items:
14
+ type: object
15
+ properties:
16
+ name:
17
+ type: string
18
+ width:
19
+ type: number
20
+ height:
21
+ type: number
22
+ hash:
23
+ type: string
24
+ path:
25
+ type: string
26
+ type:
27
+ type: string
28
+ mediaFile:
29
+ type: array
30
+ items:
31
+ type: object
32
+ properties:
33
+ type:
34
+ type: string
35
+ path:
36
+ type: string
37
+ filename:
38
+ type: string
39
+ mimeType:
40
+ type: string
41
+ description: Array of media files (images/videos) that need descriptions
42
+ iterate_on: mediaToDescribe
43
+ concurrency: 5
44
+ mode: sequential
@@ -0,0 +1,47 @@
1
+ name: generateMediaDescription
2
+ description: Generate description for a single media file (image/video)
3
+ model: "google/gemini-2.5-flash"
4
+ modalities: ["text", "image"]
5
+ instructions:
6
+ - role: system
7
+ url: ../../prompts/media/media-description/system-prompt.md
8
+ - role: user
9
+ url: ../../prompts/media/media-description/user-prompt.md
10
+ input_file_key: mediaFile
11
+ include_input_in_output: true
12
+ input_schema:
13
+ type: object
14
+ properties:
15
+ name:
16
+ type: string
17
+ description: Media file name
18
+ width:
19
+ type: number
20
+ description: Media width in pixels
21
+ height:
22
+ type: number
23
+ description: Media height in pixels
24
+ hash:
25
+ type: string
26
+ path:
27
+ type: string
28
+ description: Media path
29
+ type:
30
+ type: string
31
+ description: Media type (image/video)
32
+ mediaFile:
33
+ type: array
34
+ items:
35
+ type: object
36
+ properties:
37
+ type:
38
+ type: string
39
+ path:
40
+ type: string
41
+ filename:
42
+ type: string
43
+ mimeType:
44
+ type: string
45
+ required:
46
+ - name
47
+ output_key: description
@@ -0,0 +1,238 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { parse, stringify } from "yaml";
6
+ import { getMediaDescriptionCachePath } from "../../utils/file-utils.mjs";
7
+
8
+ const SIZE_THRESHOLD = 10 * 1024 * 1024; // 10MB
9
+
10
+ // Supported MIME types for Gemini AI
11
+ const SUPPORTED_IMAGE_TYPES = new Set([
12
+ "image/png",
13
+ "image/jpeg",
14
+ "image/webp",
15
+ "image/heic",
16
+ "image/heif",
17
+ ]);
18
+
19
+ const SUPPORTED_VIDEO_TYPES = new Set([
20
+ "video/mp4",
21
+ "video/mpeg",
22
+ "video/mov",
23
+ "video/avi",
24
+ "video/x-flv",
25
+ "video/mpg",
26
+ "video/webm",
27
+ "video/wmv",
28
+ "video/3gpp",
29
+ ]);
30
+
31
+ /**
32
+ * Calculate hash for a media file
33
+ * For files < 10MB: use file content
34
+ * For files >= 10MB: use path + size + mtime to avoid memory issues
35
+ * @param {string} absolutePath - The absolute path to the media file
36
+ * @returns {Promise<string>} - The hash of the file
37
+ */
38
+ async function calculateMediaHash(absolutePath) {
39
+ const stats = await stat(absolutePath);
40
+
41
+ if (stats.size < SIZE_THRESHOLD) {
42
+ // Small file: use full content
43
+ const content = await readFile(absolutePath);
44
+ return createHash("sha256").update(content).digest("hex");
45
+ }
46
+
47
+ // Large file: use path + size + mtime
48
+ const hashInput = `${absolutePath}:${stats.size}:${stats.mtimeMs}`;
49
+ return createHash("sha256").update(hashInput).digest("hex");
50
+ }
51
+
52
+ /**
53
+ * Load media descriptions from cache and generate new ones if needed
54
+ * @param {Object} input - Input parameters
55
+ * @param {Array} input.mediaFiles - Array of media file objects from load-sources
56
+ * @param {string} input.docsDir - Base directory for documentation
57
+ * @param {Object} options - Agent options
58
+ * @returns {Promise<Object>} - Updated assetsContent with media descriptions
59
+ */
60
+ export default async function loadMediaDescription(input, options) {
61
+ const { mediaFiles = [], docsDir } = input;
62
+
63
+ // Filter to get image and video files with supported MIME types
64
+ const mediaFilesToProcess = mediaFiles.filter((file) => {
65
+ if (file.type === "image") {
66
+ return SUPPORTED_IMAGE_TYPES.has(file.mimeType);
67
+ }
68
+ if (file.type === "video") {
69
+ return SUPPORTED_VIDEO_TYPES.has(file.mimeType);
70
+ }
71
+ return false;
72
+ });
73
+
74
+ // Path to media description cache file
75
+ const cacheFilePath = getMediaDescriptionCachePath();
76
+
77
+ // Load existing cache
78
+ let cache = {};
79
+ if (existsSync(cacheFilePath)) {
80
+ try {
81
+ const cacheContent = await readFile(cacheFilePath, "utf8");
82
+ const parsedCache = parse(cacheContent);
83
+ cache = parsedCache?.descriptions || {};
84
+ } catch (error) {
85
+ console.warn("Failed to read media description cache:", error.message);
86
+ }
87
+ }
88
+
89
+ // Find media files without descriptions
90
+ const mediaToDescribe = [];
91
+ const mediaHashMap = new Map();
92
+
93
+ const absoluteDocsDir = path.resolve(process.cwd(), docsDir);
94
+
95
+ // Only process media files that need AI description
96
+ for (const mediaFile of mediaFilesToProcess) {
97
+ // Convert relative path to absolute path for consistent hashing
98
+ // mediaFiles.path is relative to docsDir
99
+ const absolutePath = path.join(absoluteDocsDir, mediaFile.path);
100
+ const mediaHash = await calculateMediaHash(absolutePath);
101
+ mediaHashMap.set(mediaFile.path, mediaHash);
102
+
103
+ if (!cache[mediaHash]) {
104
+ mediaToDescribe.push({
105
+ ...mediaFile,
106
+ hash: mediaHash,
107
+ path: mediaFile.path,
108
+ mediaFile: [
109
+ {
110
+ type: "local",
111
+ path: absolutePath,
112
+ filename: mediaFile.name,
113
+ mimeType: mediaFile.mimeType,
114
+ },
115
+ ],
116
+ });
117
+ }
118
+ }
119
+
120
+ // Generate descriptions for media files without cache - use team agent for concurrent processing
121
+ const newDescriptions = {};
122
+ if (mediaToDescribe.length > 0) {
123
+ try {
124
+ // Use batch team agent for concurrent processing
125
+ const results = await options.context.invoke(
126
+ options.context.agents["batchGenerateMediaDescription"],
127
+ {
128
+ mediaToDescribe,
129
+ },
130
+ );
131
+
132
+ // Process results - results is an array of individual results
133
+ if (Array.isArray(results?.mediaToDescribe)) {
134
+ for (const result of results.mediaToDescribe) {
135
+ if (result?.hash && result?.description) {
136
+ newDescriptions[result.hash] = {
137
+ path: result.path,
138
+ description: result.description,
139
+ generatedAt: new Date().toISOString(),
140
+ };
141
+ }
142
+ }
143
+ }
144
+
145
+ // Merge new descriptions into cache
146
+ Object.assign(cache, newDescriptions);
147
+
148
+ // Save updated cache
149
+ await mkdir(path.dirname(cacheFilePath), { recursive: true });
150
+ const cacheYaml = stringify({
151
+ descriptions: cache,
152
+ lastUpdated: new Date().toISOString(),
153
+ });
154
+ await writeFile(cacheFilePath, cacheYaml, "utf8");
155
+
156
+ console.log(`Generated descriptions for ${Object.keys(newDescriptions).length} media files`);
157
+ } catch (error) {
158
+ console.error("Failed to generate media descriptions:", error.message);
159
+ }
160
+ }
161
+
162
+ // Build enhanced assetsContent with descriptions
163
+ let enhancedAssetsContent = "# Available Media Assets for Documentation\n\n";
164
+
165
+ if (mediaFiles.length > 0) {
166
+ enhancedAssetsContent += "```yaml\n";
167
+ enhancedAssetsContent += "assets:\n";
168
+
169
+ for (const asset of mediaFiles) {
170
+ enhancedAssetsContent += ` - name: "${asset.name}"\n`;
171
+ enhancedAssetsContent += ` path: "${asset.path}"\n`;
172
+ enhancedAssetsContent += ` type: "${asset.type}"\n`;
173
+
174
+ // Add description for images and videos
175
+ if (asset.type === "image" || asset.type === "video") {
176
+ const mediaHash = mediaHashMap.get(asset.path);
177
+ const cachedDesc = cache[mediaHash];
178
+ if (cachedDesc?.description) {
179
+ enhancedAssetsContent += ` description: "${cachedDesc.description}"\n`;
180
+ }
181
+ }
182
+
183
+ // Add dimensions for images and videos
184
+ if (asset.width && asset.height) {
185
+ enhancedAssetsContent += ` width: ${asset.width}\n`;
186
+ enhancedAssetsContent += ` height: ${asset.height}\n`;
187
+ }
188
+ }
189
+
190
+ enhancedAssetsContent += "```\n";
191
+ }
192
+
193
+ return {
194
+ ...input,
195
+ assetsContent: enhancedAssetsContent,
196
+ mediaFiles,
197
+ };
198
+ }
199
+
200
+ loadMediaDescription.input_schema = {
201
+ type: "object",
202
+ properties: {
203
+ mediaFiles: {
204
+ type: "array",
205
+ items: {
206
+ type: "object",
207
+ properties: {
208
+ name: { type: "string" },
209
+ path: { type: "string" },
210
+ type: { type: "string" },
211
+ width: { type: "number" },
212
+ height: { type: "number" },
213
+ mimeType: { type: "string" },
214
+ },
215
+ },
216
+ description: "Array of media file objects (images/videos)",
217
+ },
218
+ docsDir: {
219
+ type: "string",
220
+ description: "Base directory for documentation",
221
+ },
222
+ },
223
+ required: ["mediaFiles", "docsDir"],
224
+ };
225
+
226
+ loadMediaDescription.output_schema = {
227
+ type: "object",
228
+ properties: {
229
+ assetsContent: {
230
+ type: "string",
231
+ description: "Enhanced assets content with media descriptions",
232
+ },
233
+ mediaFiles: {
234
+ type: "array",
235
+ description: "Array of media file objects",
236
+ },
237
+ },
238
+ };
@@ -30,6 +30,7 @@ skills:
30
30
  default_input:
31
31
  requiredFeedback: false
32
32
  - ../utils/format-document-structure.mjs
33
+ - ../media/load-media-description.mjs
33
34
  - ../update/check-update-is-single.mjs
34
35
  - ../update/save-and-translate-document.mjs
35
36
  - url: ../utils/action-success.mjs
@@ -1,10 +1,12 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import imageSize from "image-size";
3
4
  import {
4
5
  buildSourcesContent,
5
6
  calculateFileStats,
6
7
  loadFilesFromPaths,
7
8
  readFileContents,
9
+ getMimeType,
8
10
  } from "../../utils/file-utils.mjs";
9
11
  import {
10
12
  getCurrentGitHead,
@@ -29,8 +31,10 @@ export default async function loadSources({
29
31
  useDefaultPatterns = true,
30
32
  lastGitHead,
31
33
  reset = false,
34
+ media,
32
35
  } = {}) {
33
36
  let files = Array.isArray(sources) ? [...sources] : [];
37
+ const { minImageWidth } = media || { minImageWidth: 800 };
34
38
 
35
39
  if (sourcesPath) {
36
40
  const allFiles = await loadFilesFromPaths(sourcesPath, {
@@ -58,36 +62,102 @@ export default async function loadSources({
58
62
  ".bmp",
59
63
  ".webp",
60
64
  ".svg",
65
+ ".heic",
66
+ ".heif",
61
67
  ".mp4",
68
+ ".mpeg",
69
+ ".mpg",
62
70
  ".mov",
63
71
  ".avi",
72
+ ".flv",
64
73
  ".mkv",
65
74
  ".webm",
75
+ ".wmv",
66
76
  ".m4v",
77
+ ".3gpp",
67
78
  ];
68
79
 
69
80
  // Separate source files from media files
70
81
  const sourceFilesPaths = [];
71
82
  const mediaFiles = [];
72
83
 
73
- for (const file of files) {
74
- const ext = path.extname(file).toLowerCase();
75
-
76
- if (mediaExtensions.includes(ext)) {
77
- // This is a media file
78
- const relativePath = path.relative(docsDir, file);
79
- const fileName = path.basename(file);
80
- const description = path.parse(fileName).name;
81
-
82
- mediaFiles.push({
83
- name: fileName,
84
- path: relativePath,
85
- description,
86
- });
87
- } else {
88
- // This is a source file
89
- sourceFilesPaths.push(file);
90
- }
84
+ // Helper function to determine file type from extension
85
+ const getFileType = (filePath) => {
86
+ const ext = path.extname(filePath).toLowerCase();
87
+ const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".heic", ".heif"];
88
+ const videoExts = [
89
+ ".mp4",
90
+ ".mpeg",
91
+ ".mpg",
92
+ ".mov",
93
+ ".avi",
94
+ ".flv",
95
+ ".mkv",
96
+ ".webm",
97
+ ".wmv",
98
+ ".m4v",
99
+ ".3gpp",
100
+ ];
101
+
102
+ if (imageExts.includes(ext)) return "image";
103
+ if (videoExts.includes(ext)) return "video";
104
+ return "media";
105
+ };
106
+
107
+ let filteredImageCount = 0;
108
+
109
+ await Promise.all(
110
+ files.map(async (file) => {
111
+ const ext = path.extname(file).toLowerCase();
112
+
113
+ if (mediaExtensions.includes(ext)) {
114
+ // This is a media file
115
+ const relativePath = path.relative(docsDir, file);
116
+ const fileName = path.basename(file);
117
+ const description = path.parse(fileName).name;
118
+
119
+ const mediaItem = {
120
+ name: fileName,
121
+ path: relativePath,
122
+ type: getFileType(relativePath),
123
+ description,
124
+ mimeType: getMimeType(file),
125
+ };
126
+
127
+ // For image files, get dimensions and filter by width
128
+ if (mediaItem.type === "image") {
129
+ try {
130
+ const buffer = await readFile(file);
131
+ const dimensions = imageSize(buffer);
132
+ mediaItem.width = dimensions.width;
133
+ mediaItem.height = dimensions.height;
134
+
135
+ // Filter out images with width less than minImageWidth
136
+ if (dimensions.width < minImageWidth) {
137
+ filteredImageCount++;
138
+ console.log(
139
+ `Filtered image: ${fileName} (${dimensions.width}x${dimensions.height}px < ${minImageWidth}px minimum)`,
140
+ );
141
+ return;
142
+ }
143
+ } catch (err) {
144
+ console.warn(`⚠️ Failed to get dimensions for ${fileName}: ${err.message}`);
145
+ }
146
+ }
147
+
148
+ mediaFiles.push(mediaItem);
149
+ } else {
150
+ // This is a source file
151
+ sourceFilesPaths.push(file);
152
+ }
153
+ }),
154
+ );
155
+
156
+ // Log summary of filtered images
157
+ if (filteredImageCount > 0) {
158
+ console.log(
159
+ `\nTotal ${filteredImageCount} low-resolution image(s) filtered for better documentation quality (minimum width: ${minImageWidth}px)\n`,
160
+ );
91
161
  }
92
162
 
93
163
  // Read all source files using the utility function
@@ -179,37 +249,6 @@ export default async function loadSources({
179
249
  }
180
250
  }
181
251
 
182
- // Generate assets content from media files
183
- let assetsContent = "# Available Media Assets for Documentation\n\n";
184
-
185
- if (mediaFiles.length > 0) {
186
- // Helper function to determine file type from extension
187
- const getFileType = (filePath) => {
188
- const ext = path.extname(filePath).toLowerCase();
189
- const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"];
190
- const videoExts = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
191
-
192
- if (imageExts.includes(ext)) return "image";
193
- if (videoExts.includes(ext)) return "video";
194
- return "media";
195
- };
196
-
197
- const mediaYaml = mediaFiles.map((file) => ({
198
- name: file.name,
199
- path: file.path,
200
- type: getFileType(file.path),
201
- }));
202
-
203
- assetsContent += "```yaml\n";
204
- assetsContent += "assets:\n";
205
- mediaYaml.forEach((asset) => {
206
- assetsContent += ` - name: "${asset.name}"\n`;
207
- assetsContent += ` path: "${asset.path}"\n`;
208
- assetsContent += ` type: "${asset.type}"\n`;
209
- });
210
- assetsContent += "```\n";
211
- }
212
-
213
252
  return {
214
253
  datasources: allSources,
215
254
  content,
@@ -218,7 +257,7 @@ export default async function loadSources({
218
257
  modifiedFiles,
219
258
  totalTokens,
220
259
  totalLines,
221
- assetsContent,
260
+ mediaFiles,
222
261
  isLargeContext,
223
262
  allFilesPaths,
224
263
  };
@@ -280,9 +319,20 @@ loadSources.output_schema = {
280
319
  items: { type: "string" },
281
320
  description: "Array of modified files since last generation",
282
321
  },
283
- assetsContent: {
284
- type: "string",
285
- description: "Markdown content for available media assets",
322
+ mediaFiles: {
323
+ type: "array",
324
+ items: {
325
+ type: "object",
326
+ properties: {
327
+ name: { type: "string" },
328
+ path: { type: "string" },
329
+ type: { type: "string" },
330
+ width: { type: "number" },
331
+ height: { type: "number" },
332
+ mimeType: { type: "string" },
333
+ },
334
+ },
335
+ description: "Array of media file objects (images/videos)",
286
336
  },
287
337
  },
288
338
  };
package/aigne.yaml CHANGED
@@ -49,6 +49,11 @@ agents:
49
49
  - ./agents/publish/publish-docs.mjs
50
50
  - ./agents/publish/index.yaml
51
51
 
52
+ # Media
53
+ - ./agents/media/load-media-description.mjs
54
+ - ./agents/media/batch-generate-media-description.yaml
55
+ - ./agents/media/generate-media-description.yaml
56
+
52
57
  # Clear/Cleanup
53
58
  - ./agents/clear/choose-contents.mjs
54
59
  - ./agents/clear/clear-document-structure.mjs
@@ -56,6 +61,7 @@ agents:
56
61
  - ./agents/clear/clear-document-config.mjs
57
62
  - ./agents/clear/clear-auth-tokens.mjs
58
63
  - ./agents/clear/clear-deployment-config.mjs
64
+ - ./agents/clear/clear-media-description.mjs
59
65
 
60
66
  # Utilities
61
67
  - ./agents/utils/load-sources.mjs