gbu-accessibility-package 3.1.3 → 3.2.0
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/ENHANCED_ALT_FEATURES.md +230 -0
- package/README-vi.md +195 -583
- package/README.md +179 -511
- package/cli.js +45 -2
- package/demo/enhanced-alt-test.html +150 -0
- package/index.js +2 -6
- package/lib/enhanced-alt-checker.js +632 -0
- package/lib/enhanced-alt-generator.js +741 -0
- package/lib/fixer.js +83 -9
- package/package.json +5 -1
- package/demo/advanced-test.html.backup +0 -44
- package/demo/backup-test.html +0 -18
- package/demo/backup-test2.html +0 -13
- package/demo/backup-test3.html +0 -12
- package/demo/comprehensive-test.html.backup +0 -21
- package/demo/no-backup-test.html +0 -12
- package/demo/no-backup-test.html.backup +0 -12
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Alt Text Generator
|
|
3
|
+
* Tạo alt text thông minh và đa dạng dựa trên AI và ngữ cảnh
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
class EnhancedAltGenerator {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.config = {
|
|
11
|
+
language: config.language || 'ja',
|
|
12
|
+
creativity: config.creativity || 'balanced', // conservative, balanced, creative
|
|
13
|
+
includeEmotions: config.includeEmotions || false,
|
|
14
|
+
includeBrandContext: config.includeBrandContext || true,
|
|
15
|
+
maxLength: config.maxLength || 125,
|
|
16
|
+
...config
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Từ điển đa ngôn ngữ
|
|
20
|
+
this.vocabulary = this.initializeVocabulary();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
initializeVocabulary() {
|
|
24
|
+
return {
|
|
25
|
+
ja: {
|
|
26
|
+
// Loại hình ảnh
|
|
27
|
+
types: {
|
|
28
|
+
person: ['人物', '人', '男性', '女性', '子供', '大人'],
|
|
29
|
+
object: ['物', '商品', 'アイテム', '製品'],
|
|
30
|
+
nature: ['自然', '風景', '景色', '環境'],
|
|
31
|
+
building: ['建物', '建築', '構造物', '施設'],
|
|
32
|
+
food: ['食べ物', '料理', '食品', 'グルメ'],
|
|
33
|
+
technology: ['技術', 'テクノロジー', '機器', 'デバイス'],
|
|
34
|
+
art: ['芸術', 'アート', '作品', 'デザイン'],
|
|
35
|
+
vehicle: ['乗り物', '車両', '交通手段']
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Cảm xúc và tông điệu
|
|
39
|
+
emotions: {
|
|
40
|
+
positive: ['明るい', '楽しい', '美しい', '素晴らしい', '魅力的な'],
|
|
41
|
+
neutral: ['シンプルな', '清潔な', '整然とした', 'プロフェッショナルな'],
|
|
42
|
+
dynamic: ['活気のある', 'エネルギッシュな', 'ダイナミックな', '力強い']
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Hành động
|
|
46
|
+
actions: {
|
|
47
|
+
showing: ['示している', '表示している', '見せている'],
|
|
48
|
+
working: ['作業している', '働いている', '取り組んでいる'],
|
|
49
|
+
enjoying: ['楽しんでいる', '満喫している', '味わっている'],
|
|
50
|
+
creating: ['作成している', '制作している', '開発している']
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Bối cảnh
|
|
54
|
+
contexts: {
|
|
55
|
+
business: ['ビジネス', '企業', '会社', '職場'],
|
|
56
|
+
education: ['教育', '学習', '研修', 'トレーニング'],
|
|
57
|
+
lifestyle: ['ライフスタイル', '日常', '生活', '暮らし'],
|
|
58
|
+
technology: ['IT', 'デジタル', 'オンライン', 'ウェブ']
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
en: {
|
|
63
|
+
types: {
|
|
64
|
+
person: ['person', 'people', 'individual', 'team', 'group'],
|
|
65
|
+
object: ['object', 'item', 'product', 'tool', 'equipment'],
|
|
66
|
+
nature: ['nature', 'landscape', 'scenery', 'environment'],
|
|
67
|
+
building: ['building', 'architecture', 'structure', 'facility'],
|
|
68
|
+
food: ['food', 'cuisine', 'dish', 'meal', 'delicacy'],
|
|
69
|
+
technology: ['technology', 'device', 'gadget', 'equipment'],
|
|
70
|
+
art: ['art', 'artwork', 'design', 'creation'],
|
|
71
|
+
vehicle: ['vehicle', 'transportation', 'automobile']
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
emotions: {
|
|
75
|
+
positive: ['bright', 'cheerful', 'beautiful', 'wonderful', 'attractive'],
|
|
76
|
+
neutral: ['simple', 'clean', 'organized', 'professional'],
|
|
77
|
+
dynamic: ['vibrant', 'energetic', 'dynamic', 'powerful']
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
actions: {
|
|
81
|
+
showing: ['showing', 'displaying', 'presenting'],
|
|
82
|
+
working: ['working', 'operating', 'engaging'],
|
|
83
|
+
enjoying: ['enjoying', 'experiencing', 'savoring'],
|
|
84
|
+
creating: ['creating', 'developing', 'building']
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
contexts: {
|
|
88
|
+
business: ['business', 'corporate', 'company', 'workplace'],
|
|
89
|
+
education: ['education', 'learning', 'training', 'academic'],
|
|
90
|
+
lifestyle: ['lifestyle', 'daily life', 'personal', 'casual'],
|
|
91
|
+
technology: ['technology', 'digital', 'online', 'web']
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
vi: {
|
|
96
|
+
types: {
|
|
97
|
+
person: ['người', 'con người', 'cá nhân', 'nhóm', 'đội ngũ'],
|
|
98
|
+
object: ['vật', 'đồ vật', 'sản phẩm', 'công cụ', 'thiết bị'],
|
|
99
|
+
nature: ['thiên nhiên', 'phong cảnh', 'cảnh quan', 'môi trường'],
|
|
100
|
+
building: ['tòa nhà', 'kiến trúc', 'công trình', 'cơ sở'],
|
|
101
|
+
food: ['thức ăn', 'món ăn', 'ẩm thực', 'đặc sản'],
|
|
102
|
+
technology: ['công nghệ', 'thiết bị', 'máy móc', 'kỹ thuật'],
|
|
103
|
+
art: ['nghệ thuật', 'tác phẩm', 'thiết kế', 'sáng tạo'],
|
|
104
|
+
vehicle: ['phương tiện', 'xe cộ', 'giao thông']
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
emotions: {
|
|
108
|
+
positive: ['tươi sáng', 'vui vẻ', 'đẹp đẽ', 'tuyệt vời', 'hấp dẫn'],
|
|
109
|
+
neutral: ['đơn giản', 'sạch sẽ', 'ngăn nắp', 'chuyên nghiệp'],
|
|
110
|
+
dynamic: ['sôi động', 'năng động', 'mạnh mẽ', 'đầy năng lượng']
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
actions: {
|
|
114
|
+
showing: ['đang hiển thị', 'đang trình bày', 'đang thể hiện'],
|
|
115
|
+
working: ['đang làm việc', 'đang hoạt động', 'đang thực hiện'],
|
|
116
|
+
enjoying: ['đang thưởng thức', 'đang tận hưởng', 'đang trải nghiệm'],
|
|
117
|
+
creating: ['đang tạo ra', 'đang phát triển', 'đang xây dựng']
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
contexts: {
|
|
121
|
+
business: ['kinh doanh', 'doanh nghiệp', 'công ty', 'nơi làm việc'],
|
|
122
|
+
education: ['giáo dục', 'học tập', 'đào tạo', 'học thuật'],
|
|
123
|
+
lifestyle: ['lối sống', 'cuộc sống', 'cá nhân', 'thường ngày'],
|
|
124
|
+
technology: ['công nghệ', 'số hóa', 'trực tuyến', 'web']
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Tạo alt text đa dạng và thông minh
|
|
132
|
+
*/
|
|
133
|
+
generateDiverseAltText(imgTag, htmlContent, analysis) {
|
|
134
|
+
const strategies = [
|
|
135
|
+
() => this.generateContextualAlt(analysis),
|
|
136
|
+
() => this.generateSemanticAlt(analysis),
|
|
137
|
+
() => this.generateEmotionalAlt(analysis),
|
|
138
|
+
() => this.generateActionBasedAlt(analysis),
|
|
139
|
+
() => this.generateBrandAwareAlt(analysis),
|
|
140
|
+
() => this.generateTechnicalAlt(analysis)
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Chọn strategy dựa trên creativity level
|
|
144
|
+
const selectedStrategies = this.selectStrategies(strategies, analysis);
|
|
145
|
+
|
|
146
|
+
for (const strategy of selectedStrategies) {
|
|
147
|
+
const result = strategy();
|
|
148
|
+
if (result && this.validateAltText(result)) {
|
|
149
|
+
return this.refineAltText(result, analysis);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return this.generateFallbackAlt(analysis);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Tạo alt text dựa trên ngữ cảnh HTML
|
|
158
|
+
*/
|
|
159
|
+
generateContextualAlt(analysis) {
|
|
160
|
+
const { context, structural, imageType } = analysis;
|
|
161
|
+
const lang = this.config.language;
|
|
162
|
+
|
|
163
|
+
// Phân tích ngữ cảnh HTML
|
|
164
|
+
const contextElements = this.extractContextElements(context);
|
|
165
|
+
|
|
166
|
+
// Tạo alt dựa trên cấu trúc
|
|
167
|
+
if (structural.figcaption) {
|
|
168
|
+
return this.enhanceWithVocabulary(structural.figcaption, imageType);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (structural.parentLink) {
|
|
172
|
+
const linkText = this.extractLinkText(structural.parentLink);
|
|
173
|
+
if (linkText) {
|
|
174
|
+
return this.createLinkAlt(linkText, imageType);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Sử dụng heading gần nhất
|
|
179
|
+
if (contextElements.nearbyHeading) {
|
|
180
|
+
return this.createHeadingBasedAlt(contextElements.nearbyHeading, imageType);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Sử dụng paragraph text
|
|
184
|
+
if (contextElements.surroundingText) {
|
|
185
|
+
return this.createTextBasedAlt(contextElements.surroundingText, imageType);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Tạo alt text dựa trên semantic analysis
|
|
193
|
+
*/
|
|
194
|
+
generateSemanticAlt(analysis) {
|
|
195
|
+
const { src, imageType, context } = analysis;
|
|
196
|
+
const lang = this.config.language;
|
|
197
|
+
const vocab = this.vocabulary[lang];
|
|
198
|
+
|
|
199
|
+
// Phân tích semantic từ src
|
|
200
|
+
const semanticInfo = this.analyzeSemanticContent(src, context);
|
|
201
|
+
|
|
202
|
+
if (!semanticInfo.mainSubject) return null;
|
|
203
|
+
|
|
204
|
+
// Xây dựng alt text semantic
|
|
205
|
+
let altParts = [];
|
|
206
|
+
|
|
207
|
+
// Chủ thể chính
|
|
208
|
+
const subjectWord = this.selectVocabularyWord(vocab.types[semanticInfo.category] || [], 'random');
|
|
209
|
+
if (subjectWord) {
|
|
210
|
+
altParts.push(subjectWord);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Thêm mô tả
|
|
214
|
+
if (semanticInfo.description) {
|
|
215
|
+
altParts.push(semanticInfo.description);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Thêm context nếu có
|
|
219
|
+
if (semanticInfo.context && this.config.includeBrandContext) {
|
|
220
|
+
const contextWord = this.selectVocabularyWord(vocab.contexts[semanticInfo.context] || [], 'first');
|
|
221
|
+
if (contextWord) {
|
|
222
|
+
altParts.push(`${contextWord}の`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return this.combineAltParts(altParts);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Tạo alt text có cảm xúc
|
|
231
|
+
*/
|
|
232
|
+
generateEmotionalAlt(analysis) {
|
|
233
|
+
if (!this.config.includeEmotions) return null;
|
|
234
|
+
|
|
235
|
+
const { imageType, context } = analysis;
|
|
236
|
+
const lang = this.config.language;
|
|
237
|
+
const vocab = this.vocabulary[lang];
|
|
238
|
+
|
|
239
|
+
// Phân tích tone của context
|
|
240
|
+
const emotionalTone = this.analyzeEmotionalTone(context);
|
|
241
|
+
|
|
242
|
+
if (!emotionalTone) return null;
|
|
243
|
+
|
|
244
|
+
const emotionWords = vocab.emotions[emotionalTone] || [];
|
|
245
|
+
const emotionWord = this.selectVocabularyWord(emotionWords, 'random');
|
|
246
|
+
|
|
247
|
+
if (!emotionWord) return null;
|
|
248
|
+
|
|
249
|
+
// Kết hợp với base alt
|
|
250
|
+
const baseAlt = this.generateBasicAlt(analysis);
|
|
251
|
+
|
|
252
|
+
return lang === 'ja' ?
|
|
253
|
+
`${emotionWord}${baseAlt}` :
|
|
254
|
+
`${emotionWord} ${baseAlt}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Tạo alt text dựa trên hành động
|
|
259
|
+
*/
|
|
260
|
+
generateActionBasedAlt(analysis) {
|
|
261
|
+
const { context, imageType } = analysis;
|
|
262
|
+
const lang = this.config.language;
|
|
263
|
+
const vocab = this.vocabulary[lang];
|
|
264
|
+
|
|
265
|
+
// Tìm hành động trong context
|
|
266
|
+
const detectedAction = this.detectAction(context);
|
|
267
|
+
|
|
268
|
+
if (!detectedAction) return null;
|
|
269
|
+
|
|
270
|
+
const actionWords = vocab.actions[detectedAction] || [];
|
|
271
|
+
const actionWord = this.selectVocabularyWord(actionWords, 'random');
|
|
272
|
+
|
|
273
|
+
if (!actionWord) return null;
|
|
274
|
+
|
|
275
|
+
const subject = this.detectSubject(context, imageType);
|
|
276
|
+
|
|
277
|
+
return lang === 'ja' ?
|
|
278
|
+
`${subject}${actionWord}様子` :
|
|
279
|
+
`${subject} ${actionWord}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Tạo alt text có nhận thức thương hiệu
|
|
284
|
+
*/
|
|
285
|
+
generateBrandAwareAlt(analysis) {
|
|
286
|
+
if (!this.config.includeBrandContext) return null;
|
|
287
|
+
|
|
288
|
+
const { context, src } = analysis;
|
|
289
|
+
|
|
290
|
+
// Tìm thông tin thương hiệu
|
|
291
|
+
const brandInfo = this.extractBrandInfo(context, src);
|
|
292
|
+
|
|
293
|
+
if (!brandInfo.name) return null;
|
|
294
|
+
|
|
295
|
+
const baseAlt = this.generateBasicAlt(analysis);
|
|
296
|
+
|
|
297
|
+
return `${brandInfo.name}の${baseAlt}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Tạo alt text kỹ thuật cho hình phức tạp
|
|
302
|
+
*/
|
|
303
|
+
generateTechnicalAlt(analysis) {
|
|
304
|
+
const { imageType, src, context } = analysis;
|
|
305
|
+
|
|
306
|
+
if (imageType !== 'data-visualization' && imageType !== 'complex') {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Phân tích dữ liệu kỹ thuật
|
|
311
|
+
const technicalInfo = this.extractTechnicalInfo(context, src);
|
|
312
|
+
|
|
313
|
+
if (!technicalInfo.type) return null;
|
|
314
|
+
|
|
315
|
+
let altParts = [technicalInfo.type];
|
|
316
|
+
|
|
317
|
+
if (technicalInfo.data) {
|
|
318
|
+
altParts.push(technicalInfo.data);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (technicalInfo.trend) {
|
|
322
|
+
altParts.push(technicalInfo.trend);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return this.combineAltParts(altParts);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Chọn strategies dựa trên creativity level
|
|
330
|
+
*/
|
|
331
|
+
selectStrategies(strategies, analysis) {
|
|
332
|
+
const { creativity } = this.config;
|
|
333
|
+
const { imageType } = analysis;
|
|
334
|
+
|
|
335
|
+
switch (creativity) {
|
|
336
|
+
case 'conservative':
|
|
337
|
+
return strategies.slice(0, 2); // Chỉ contextual và semantic
|
|
338
|
+
|
|
339
|
+
case 'creative':
|
|
340
|
+
return strategies; // Tất cả strategies
|
|
341
|
+
|
|
342
|
+
default: // balanced
|
|
343
|
+
if (imageType === 'decorative') {
|
|
344
|
+
return strategies.slice(0, 1);
|
|
345
|
+
} else if (imageType === 'data-visualization') {
|
|
346
|
+
return [strategies[0], strategies[5]]; // contextual + technical
|
|
347
|
+
} else {
|
|
348
|
+
return strategies.slice(0, 4); // Loại bỏ brand và technical
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Validate alt text quality
|
|
355
|
+
*/
|
|
356
|
+
validateAltText(altText) {
|
|
357
|
+
if (!altText || typeof altText !== 'string') return false;
|
|
358
|
+
|
|
359
|
+
const trimmed = altText.trim();
|
|
360
|
+
|
|
361
|
+
// Kiểm tra độ dài
|
|
362
|
+
if (trimmed.length < 2 || trimmed.length > this.config.maxLength) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Kiểm tra không chứa từ cấm
|
|
367
|
+
const forbiddenWords = ['image', 'picture', 'photo', '画像', '写真'];
|
|
368
|
+
const hasForbidenWord = forbiddenWords.some(word =>
|
|
369
|
+
trimmed.toLowerCase().includes(word.toLowerCase())
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
if (hasForbidenWord) return false;
|
|
373
|
+
|
|
374
|
+
// Kiểm tra không phải placeholder
|
|
375
|
+
const placeholders = ['[', ']', 'placeholder', 'dummy'];
|
|
376
|
+
const hasPlaceholder = placeholders.some(placeholder =>
|
|
377
|
+
trimmed.toLowerCase().includes(placeholder)
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return !hasPlaceholder;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Refine alt text cuối cùng
|
|
385
|
+
*/
|
|
386
|
+
refineAltText(altText, analysis) {
|
|
387
|
+
let refined = altText.trim();
|
|
388
|
+
|
|
389
|
+
// Loại bỏ ký tự đặc biệt
|
|
390
|
+
refined = refined.replace(/[<>]/g, '');
|
|
391
|
+
|
|
392
|
+
// Chuẩn hóa khoảng trắng
|
|
393
|
+
refined = refined.replace(/\s+/g, ' ');
|
|
394
|
+
|
|
395
|
+
// Giới hạn độ dài
|
|
396
|
+
if (refined.length > this.config.maxLength) {
|
|
397
|
+
refined = refined.substring(0, this.config.maxLength - 3) + '...';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Capitalize first letter cho English
|
|
401
|
+
if (this.config.language === 'en') {
|
|
402
|
+
refined = refined.charAt(0).toUpperCase() + refined.slice(1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return refined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Helper methods
|
|
409
|
+
extractContextElements(context) {
|
|
410
|
+
return {
|
|
411
|
+
nearbyHeading: this.findNearbyHeading(context),
|
|
412
|
+
surroundingText: this.extractSurroundingText(context),
|
|
413
|
+
listContext: this.findListContext(context),
|
|
414
|
+
tableContext: this.findTableContext(context)
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
analyzeSemanticContent(src, context) {
|
|
419
|
+
const srcLower = (src || '').toLowerCase();
|
|
420
|
+
const contextLower = context.toLowerCase();
|
|
421
|
+
|
|
422
|
+
// Xác định category
|
|
423
|
+
let category = 'object'; // default
|
|
424
|
+
|
|
425
|
+
if (this.containsKeywords(srcLower + ' ' + contextLower, ['person', 'people', 'man', 'woman', '人', '人物'])) {
|
|
426
|
+
category = 'person';
|
|
427
|
+
} else if (this.containsKeywords(srcLower + ' ' + contextLower, ['nature', 'landscape', '自然', '風景'])) {
|
|
428
|
+
category = 'nature';
|
|
429
|
+
} else if (this.containsKeywords(srcLower + ' ' + contextLower, ['building', 'architecture', '建物', '建築'])) {
|
|
430
|
+
category = 'building';
|
|
431
|
+
} else if (this.containsKeywords(srcLower + ' ' + contextLower, ['food', 'restaurant', '食べ物', '料理'])) {
|
|
432
|
+
category = 'food';
|
|
433
|
+
} else if (this.containsKeywords(srcLower + ' ' + contextLower, ['tech', 'computer', 'device', '技術', 'コンピューター'])) {
|
|
434
|
+
category = 'technology';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
category,
|
|
439
|
+
mainSubject: this.extractMainSubject(context),
|
|
440
|
+
description: this.extractDescription(context),
|
|
441
|
+
context: this.detectContextType(context)
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
analyzeEmotionalTone(context) {
|
|
446
|
+
const contextLower = context.toLowerCase();
|
|
447
|
+
|
|
448
|
+
// Positive indicators
|
|
449
|
+
if (this.containsKeywords(contextLower, ['success', 'happy', 'great', 'excellent', '成功', '素晴らしい', '優秀'])) {
|
|
450
|
+
return 'positive';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Dynamic indicators
|
|
454
|
+
if (this.containsKeywords(contextLower, ['action', 'energy', 'dynamic', 'power', 'アクション', 'エネルギー', 'ダイナミック'])) {
|
|
455
|
+
return 'dynamic';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Default to neutral
|
|
459
|
+
return 'neutral';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
detectAction(context) {
|
|
463
|
+
const contextLower = context.toLowerCase();
|
|
464
|
+
|
|
465
|
+
if (this.containsKeywords(contextLower, ['show', 'display', 'present', '表示', '示す'])) {
|
|
466
|
+
return 'showing';
|
|
467
|
+
} else if (this.containsKeywords(contextLower, ['work', 'operate', 'use', '作業', '操作', '使用'])) {
|
|
468
|
+
return 'working';
|
|
469
|
+
} else if (this.containsKeywords(contextLower, ['enjoy', 'experience', 'taste', '楽しむ', '体験', '味わう'])) {
|
|
470
|
+
return 'enjoying';
|
|
471
|
+
} else if (this.containsKeywords(contextLower, ['create', 'build', 'develop', '作成', '構築', '開発'])) {
|
|
472
|
+
return 'creating';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
detectSubject(context, imageType) {
|
|
479
|
+
const lang = this.config.language;
|
|
480
|
+
const vocab = this.vocabulary[lang];
|
|
481
|
+
|
|
482
|
+
// Dựa trên imageType để chọn subject phù hợp
|
|
483
|
+
const typeVocab = vocab.types[imageType] || vocab.types.object;
|
|
484
|
+
return this.selectVocabularyWord(typeVocab, 'first') || (lang === 'ja' ? '画像' : 'image');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
extractBrandInfo(context, src) {
|
|
488
|
+
// Tìm tên thương hiệu từ các pattern phổ biến
|
|
489
|
+
const brandPatterns = [
|
|
490
|
+
/company[^>]*>([^<]+)/i,
|
|
491
|
+
/brand[^>]*>([^<]+)/i,
|
|
492
|
+
/<title[^>]*>([^<]+)/i,
|
|
493
|
+
/alt\s*=\s*["']([^"']*logo[^"']*)["']/i
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
for (const pattern of brandPatterns) {
|
|
497
|
+
const match = context.match(pattern);
|
|
498
|
+
if (match) {
|
|
499
|
+
return { name: match[1].trim().replace(/\s*logo\s*/i, '') };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return { name: null };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
extractTechnicalInfo(context, src) {
|
|
507
|
+
const contextLower = context.toLowerCase();
|
|
508
|
+
const srcLower = (src || '').toLowerCase();
|
|
509
|
+
|
|
510
|
+
let type = null;
|
|
511
|
+
let data = null;
|
|
512
|
+
let trend = null;
|
|
513
|
+
|
|
514
|
+
// Xác định loại biểu đồ
|
|
515
|
+
if (this.containsKeywords(srcLower + ' ' + contextLower, ['chart', 'graph', 'グラフ', 'チャート'])) {
|
|
516
|
+
if (this.containsKeywords(contextLower, ['bar', 'column', '棒'])) {
|
|
517
|
+
type = this.config.language === 'ja' ? '棒グラフ' : 'Bar chart';
|
|
518
|
+
} else if (this.containsKeywords(contextLower, ['pie', '円'])) {
|
|
519
|
+
type = this.config.language === 'ja' ? '円グラフ' : 'Pie chart';
|
|
520
|
+
} else if (this.containsKeywords(contextLower, ['line', '線'])) {
|
|
521
|
+
type = this.config.language === 'ja' ? '線グラフ' : 'Line chart';
|
|
522
|
+
} else {
|
|
523
|
+
type = this.config.language === 'ja' ? 'グラフ' : 'Chart';
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Tìm dữ liệu số
|
|
528
|
+
const numberPattern = /(\d+(?:\.\d+)?)\s*%?/g;
|
|
529
|
+
const numbers = contextLower.match(numberPattern);
|
|
530
|
+
if (numbers && numbers.length > 0) {
|
|
531
|
+
data = numbers.slice(0, 3).join(', '); // Lấy tối đa 3 số đầu tiên
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Tìm xu hướng
|
|
535
|
+
if (this.containsKeywords(contextLower, ['increase', 'rise', 'up', '増加', '上昇', '向上'])) {
|
|
536
|
+
trend = this.config.language === 'ja' ? '増加傾向' : 'increasing trend';
|
|
537
|
+
} else if (this.containsKeywords(contextLower, ['decrease', 'fall', 'down', '減少', '下降', '低下'])) {
|
|
538
|
+
trend = this.config.language === 'ja' ? '減少傾向' : 'decreasing trend';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return { type, data, trend };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Utility methods
|
|
545
|
+
containsKeywords(text, keywords) {
|
|
546
|
+
return keywords.some(keyword => text.includes(keyword.toLowerCase()));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
selectVocabularyWord(words, strategy = 'random') {
|
|
550
|
+
if (!words || words.length === 0) return null;
|
|
551
|
+
|
|
552
|
+
switch (strategy) {
|
|
553
|
+
case 'first':
|
|
554
|
+
return words[0];
|
|
555
|
+
case 'random':
|
|
556
|
+
return words[Math.floor(Math.random() * words.length)];
|
|
557
|
+
case 'shortest':
|
|
558
|
+
return words.reduce((shortest, word) =>
|
|
559
|
+
word.length < shortest.length ? word : shortest
|
|
560
|
+
);
|
|
561
|
+
default:
|
|
562
|
+
return words[0];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
combineAltParts(parts) {
|
|
567
|
+
const lang = this.config.language;
|
|
568
|
+
const validParts = parts.filter(part => part && part.trim());
|
|
569
|
+
|
|
570
|
+
if (validParts.length === 0) return null;
|
|
571
|
+
|
|
572
|
+
// Kết hợp theo ngôn ngữ
|
|
573
|
+
if (lang === 'ja') {
|
|
574
|
+
return validParts.join('');
|
|
575
|
+
} else {
|
|
576
|
+
return validParts.join(' ');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
generateBasicAlt(analysis) {
|
|
581
|
+
const { imageType, src } = analysis;
|
|
582
|
+
const lang = this.config.language;
|
|
583
|
+
const vocab = this.vocabulary[lang];
|
|
584
|
+
|
|
585
|
+
const typeWords = vocab.types[imageType] || vocab.types.object;
|
|
586
|
+
return this.selectVocabularyWord(typeWords, 'first') || (lang === 'ja' ? '画像' : 'image');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
generateFallbackAlt(analysis) {
|
|
590
|
+
const { src } = analysis;
|
|
591
|
+
const lang = this.config.language;
|
|
592
|
+
|
|
593
|
+
if (src) {
|
|
594
|
+
const filename = src.split('/').pop().split('.')[0];
|
|
595
|
+
const cleaned = filename.replace(/[-_]/g, ' ').trim();
|
|
596
|
+
|
|
597
|
+
if (cleaned && cleaned.length > 0) {
|
|
598
|
+
return lang === 'ja' ?
|
|
599
|
+
cleaned.replace(/\b\w/g, l => l.toUpperCase()) :
|
|
600
|
+
cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return lang === 'ja' ? '画像' : 'Image';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
enhanceWithVocabulary(text, imageType) {
|
|
608
|
+
const lang = this.config.language;
|
|
609
|
+
const vocab = this.vocabulary[lang];
|
|
610
|
+
|
|
611
|
+
// Thêm từ vựng phù hợp với imageType
|
|
612
|
+
const typeWords = vocab.types[imageType];
|
|
613
|
+
if (typeWords && typeWords.length > 0) {
|
|
614
|
+
const typeWord = this.selectVocabularyWord(typeWords, 'random');
|
|
615
|
+
|
|
616
|
+
return lang === 'ja' ?
|
|
617
|
+
`${typeWord}:${text}` :
|
|
618
|
+
`${typeWord}: ${text}`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return text;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
createLinkAlt(linkText, imageType) {
|
|
625
|
+
const lang = this.config.language;
|
|
626
|
+
|
|
627
|
+
return lang === 'ja' ?
|
|
628
|
+
`${linkText}へのリンク` :
|
|
629
|
+
`Link to ${linkText}`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
createHeadingBasedAlt(heading, imageType) {
|
|
633
|
+
const lang = this.config.language;
|
|
634
|
+
const vocab = this.vocabulary[lang];
|
|
635
|
+
|
|
636
|
+
const typeWords = vocab.types[imageType] || [];
|
|
637
|
+
const typeWord = this.selectVocabularyWord(typeWords, 'first');
|
|
638
|
+
|
|
639
|
+
if (typeWord) {
|
|
640
|
+
return lang === 'ja' ?
|
|
641
|
+
`${heading}の${typeWord}` :
|
|
642
|
+
`${typeWord} of ${heading}`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return heading;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
createTextBasedAlt(text, imageType) {
|
|
649
|
+
// Rút gọn text và kết hợp với imageType
|
|
650
|
+
const words = text.split(/\s+/).filter(word => word.length > 2);
|
|
651
|
+
const keyWords = words.slice(0, 3).join(' ');
|
|
652
|
+
|
|
653
|
+
const lang = this.config.language;
|
|
654
|
+
const vocab = this.vocabulary[lang];
|
|
655
|
+
|
|
656
|
+
const typeWords = vocab.types[imageType] || [];
|
|
657
|
+
const typeWord = this.selectVocabularyWord(typeWords, 'first');
|
|
658
|
+
|
|
659
|
+
if (typeWord && keyWords) {
|
|
660
|
+
return lang === 'ja' ?
|
|
661
|
+
`${keyWords}の${typeWord}` :
|
|
662
|
+
`${typeWord} showing ${keyWords}`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return keyWords || (lang === 'ja' ? '画像' : 'Image');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
extractMainSubject(context) {
|
|
669
|
+
// Tìm chủ thể chính từ context
|
|
670
|
+
const sentences = context.split(/[.!?。!?]/);
|
|
671
|
+
const firstSentence = sentences[0];
|
|
672
|
+
|
|
673
|
+
if (firstSentence) {
|
|
674
|
+
const words = firstSentence.split(/\s+/);
|
|
675
|
+
return words.slice(0, 3).join(' ');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
extractDescription(context) {
|
|
682
|
+
// Tìm mô tả từ context
|
|
683
|
+
const descriptiveWords = context.match(/\b(beautiful|amazing|professional|modern|elegant|美しい|素晴らしい|プロフェッショナル|モダン|エレガント)\b/gi);
|
|
684
|
+
|
|
685
|
+
return descriptiveWords ? descriptiveWords[0] : null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
detectContextType(context) {
|
|
689
|
+
const contextLower = context.toLowerCase();
|
|
690
|
+
|
|
691
|
+
if (this.containsKeywords(contextLower, ['business', 'company', 'corporate', 'ビジネス', '企業', '会社'])) {
|
|
692
|
+
return 'business';
|
|
693
|
+
} else if (this.containsKeywords(contextLower, ['education', 'learning', 'school', '教育', '学習', '学校'])) {
|
|
694
|
+
return 'education';
|
|
695
|
+
} else if (this.containsKeywords(contextLower, ['technology', 'tech', 'digital', '技術', 'テクノロジー', 'デジタル'])) {
|
|
696
|
+
return 'technology';
|
|
697
|
+
} else if (this.containsKeywords(contextLower, ['lifestyle', 'personal', 'daily', 'ライフスタイル', '個人', '日常'])) {
|
|
698
|
+
return 'lifestyle';
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
findNearbyHeading(context) {
|
|
705
|
+
const headingRegex = /<h[1-6][^>]*>([^<]+)<\/h[1-6]>/gi;
|
|
706
|
+
const match = headingRegex.exec(context);
|
|
707
|
+
return match ? match[1].trim() : null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
extractSurroundingText(context) {
|
|
711
|
+
// Loại bỏ HTML tags và lấy text xung quanh
|
|
712
|
+
const textOnly = context.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
713
|
+
const words = textOnly.split(' ');
|
|
714
|
+
|
|
715
|
+
// Lấy từ có nghĩa
|
|
716
|
+
const meaningfulWords = words.filter(word =>
|
|
717
|
+
word.length > 2 && !/^\d+$/.test(word) && !/^[^\w]+$/.test(word)
|
|
718
|
+
).slice(0, 8);
|
|
719
|
+
|
|
720
|
+
return meaningfulWords.join(' ');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
findListContext(context) {
|
|
724
|
+
const listItemRegex = /<li[^>]*>([^<]+)<\/li>/gi;
|
|
725
|
+
const match = listItemRegex.exec(context);
|
|
726
|
+
return match ? match[1].trim() : null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
findTableContext(context) {
|
|
730
|
+
const tableCellRegex = /<t[hd][^>]*>([^<]+)<\/t[hd]>/gi;
|
|
731
|
+
const match = tableCellRegex.exec(context);
|
|
732
|
+
return match ? match[1].trim() : null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
extractLinkText(linkTag) {
|
|
736
|
+
const textMatch = linkTag.match(/>([^<]+)</);
|
|
737
|
+
return textMatch ? textMatch[1].trim() : null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
module.exports = EnhancedAltGenerator;
|