llm-checker 3.1.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/LICENSE +21 -0
- package/README.md +418 -0
- package/analyzer/compatibility.js +584 -0
- package/analyzer/performance.js +505 -0
- package/bin/CLAUDE.md +12 -0
- package/bin/enhanced_cli.js +3118 -0
- package/bin/test-deterministic.js +41 -0
- package/package.json +96 -0
- package/src/CLAUDE.md +12 -0
- package/src/ai/intelligent-selector.js +615 -0
- package/src/ai/model-selector.js +312 -0
- package/src/ai/multi-objective-selector.js +820 -0
- package/src/commands/check.js +58 -0
- package/src/data/CLAUDE.md +11 -0
- package/src/data/model-database.js +637 -0
- package/src/data/sync-manager.js +279 -0
- package/src/hardware/CLAUDE.md +12 -0
- package/src/hardware/backends/CLAUDE.md +11 -0
- package/src/hardware/backends/apple-silicon.js +318 -0
- package/src/hardware/backends/cpu-detector.js +490 -0
- package/src/hardware/backends/cuda-detector.js +417 -0
- package/src/hardware/backends/intel-detector.js +436 -0
- package/src/hardware/backends/rocm-detector.js +440 -0
- package/src/hardware/detector.js +573 -0
- package/src/hardware/pc-optimizer.js +635 -0
- package/src/hardware/specs.js +286 -0
- package/src/hardware/unified-detector.js +442 -0
- package/src/index.js +2289 -0
- package/src/models/CLAUDE.md +17 -0
- package/src/models/ai-check-selector.js +806 -0
- package/src/models/catalog.json +426 -0
- package/src/models/deterministic-selector.js +1145 -0
- package/src/models/expanded_database.js +1142 -0
- package/src/models/intelligent-selector.js +532 -0
- package/src/models/requirements.js +310 -0
- package/src/models/scoring-config.js +57 -0
- package/src/models/scoring-engine.js +715 -0
- package/src/ollama/.cache/README.md +33 -0
- package/src/ollama/CLAUDE.md +24 -0
- package/src/ollama/client.js +438 -0
- package/src/ollama/enhanced-client.js +113 -0
- package/src/ollama/enhanced-scraper.js +634 -0
- package/src/ollama/manager.js +357 -0
- package/src/ollama/native-scraper.js +776 -0
- package/src/plugins/CLAUDE.md +11 -0
- package/src/plugins/examples/custom_model_plugin.js +87 -0
- package/src/plugins/index.js +295 -0
- package/src/utils/CLAUDE.md +11 -0
- package/src/utils/config.js +359 -0
- package/src/utils/formatter.js +315 -0
- package/src/utils/logger.js +272 -0
- package/src/utils/model-classifier.js +167 -0
- package/src/utils/verbose-progress.js +266 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
class Logger {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.level = options.level || process.env.LLM_CHECKER_LOG_LEVEL || 'info';
|
|
8
|
+
this.enableColors = options.colors !== false && !process.env.NO_COLOR;
|
|
9
|
+
this.logFile = options.logFile || null;
|
|
10
|
+
this.enableConsole = options.console !== false;
|
|
11
|
+
this.enableDebug = options.debug || process.env.DEBUG === '1';
|
|
12
|
+
|
|
13
|
+
this.levels = {
|
|
14
|
+
error: 0,
|
|
15
|
+
warn: 1,
|
|
16
|
+
info: 2,
|
|
17
|
+
debug: 3,
|
|
18
|
+
trace: 4
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.colors = {
|
|
22
|
+
error: '\x1b[31m', // Red
|
|
23
|
+
warn: '\x1b[33m', // Yellow
|
|
24
|
+
info: '\x1b[36m', // Cyan
|
|
25
|
+
debug: '\x1b[35m', // Magenta
|
|
26
|
+
trace: '\x1b[37m', // White
|
|
27
|
+
reset: '\x1b[0m'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
this.setupLogFile();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setupLogFile() {
|
|
34
|
+
if (this.logFile) {
|
|
35
|
+
const logDir = path.dirname(this.logFile);
|
|
36
|
+
if (!fs.existsSync(logDir)) {
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn(`Warning: Could not create log directory: ${error.message}`);
|
|
41
|
+
this.logFile = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
shouldLog(level) {
|
|
48
|
+
return this.levels[level] <= this.levels[this.level];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
formatMessage(level, message, meta = {}) {
|
|
52
|
+
const timestamp = new Date().toISOString();
|
|
53
|
+
const levelUpper = level.toUpperCase();
|
|
54
|
+
|
|
55
|
+
let formatted = `${timestamp} [${levelUpper}]`;
|
|
56
|
+
|
|
57
|
+
if (meta.component) {
|
|
58
|
+
formatted += ` [${meta.component}]`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
formatted += ` ${message}`;
|
|
62
|
+
|
|
63
|
+
if (meta.data && typeof meta.data === 'object') {
|
|
64
|
+
formatted += '\n' + JSON.stringify(meta.data, null, 2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (meta.error && meta.error instanceof Error) {
|
|
68
|
+
formatted += '\n' + meta.error.stack;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return formatted;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
colorize(level, message) {
|
|
75
|
+
if (!this.enableColors) return message;
|
|
76
|
+
return this.colors[level] + message + this.colors.reset;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writeToFile(message) {
|
|
80
|
+
if (!this.logFile) return;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
fs.appendFileSync(this.logFile, message + '\n', 'utf8');
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Fallback to console if file write fails
|
|
86
|
+
if (this.enableConsole) {
|
|
87
|
+
console.error('Failed to write to log file:', error.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log(level, message, meta = {}) {
|
|
93
|
+
if (!this.shouldLog(level)) return;
|
|
94
|
+
|
|
95
|
+
const formatted = this.formatMessage(level, message, meta);
|
|
96
|
+
|
|
97
|
+
// Write to console
|
|
98
|
+
if (this.enableConsole) {
|
|
99
|
+
const colored = this.colorize(level, formatted);
|
|
100
|
+
|
|
101
|
+
if (level === 'error') {
|
|
102
|
+
console.error(colored);
|
|
103
|
+
} else if (level === 'warn') {
|
|
104
|
+
console.warn(colored);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(colored);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Write to file
|
|
111
|
+
this.writeToFile(formatted);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
error(message, meta = {}) {
|
|
115
|
+
this.log('error', message, meta);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
warn(message, meta = {}) {
|
|
119
|
+
this.log('warn', message, meta);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
info(message, meta = {}) {
|
|
123
|
+
this.log('info', message, meta);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
debug(message, meta = {}) {
|
|
127
|
+
this.log('debug', message, meta);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
trace(message, meta = {}) {
|
|
131
|
+
this.log('trace', message, meta);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Specialized logging methods
|
|
135
|
+
logHardwareDetection(hardware) {
|
|
136
|
+
this.debug('Hardware detection completed', {
|
|
137
|
+
component: 'HardwareDetector',
|
|
138
|
+
data: {
|
|
139
|
+
cpu: hardware.cpu.brand,
|
|
140
|
+
ram: `${hardware.memory.total}GB`,
|
|
141
|
+
gpu: hardware.gpu.model,
|
|
142
|
+
architecture: hardware.cpu.architecture
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
logModelAnalysis(model, score) {
|
|
148
|
+
this.debug('Model compatibility analyzed', {
|
|
149
|
+
component: 'CompatibilityAnalyzer',
|
|
150
|
+
data: {
|
|
151
|
+
model: model.name,
|
|
152
|
+
score: score,
|
|
153
|
+
category: model.category
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
logOllamaOperation(operation, model, result) {
|
|
159
|
+
this.info(`Ollama ${operation} ${result ? 'succeeded' : 'failed'}`, {
|
|
160
|
+
component: 'OllamaClient',
|
|
161
|
+
data: {
|
|
162
|
+
operation,
|
|
163
|
+
model,
|
|
164
|
+
success: result
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
logPerformanceTest(model, results) {
|
|
170
|
+
this.info('Performance test completed', {
|
|
171
|
+
component: 'PerformanceAnalyzer',
|
|
172
|
+
data: {
|
|
173
|
+
model,
|
|
174
|
+
tokensPerSecond: results.tokensPerSecond,
|
|
175
|
+
responseTime: results.responseTime
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logError(error, context = {}) {
|
|
181
|
+
this.error(error.message, {
|
|
182
|
+
component: context.component || 'Unknown',
|
|
183
|
+
error: error,
|
|
184
|
+
data: context.data
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Utility methods
|
|
189
|
+
time(label) {
|
|
190
|
+
const start = process.hrtime.bigint();
|
|
191
|
+
return {
|
|
192
|
+
end: () => {
|
|
193
|
+
const end = process.hrtime.bigint();
|
|
194
|
+
const duration = Number(end - start) / 1000000; // Convert to milliseconds
|
|
195
|
+
this.debug(`Timer: ${label} took ${duration.toFixed(2)}ms`);
|
|
196
|
+
return duration;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
createChild(component) {
|
|
202
|
+
return {
|
|
203
|
+
error: (message, meta = {}) => this.error(message, { ...meta, component }),
|
|
204
|
+
warn: (message, meta = {}) => this.warn(message, { ...meta, component }),
|
|
205
|
+
info: (message, meta = {}) => this.info(message, { ...meta, component }),
|
|
206
|
+
debug: (message, meta = {}) => this.debug(message, { ...meta, component }),
|
|
207
|
+
trace: (message, meta = {}) => this.trace(message, { ...meta, component })
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getLogFile() {
|
|
212
|
+
return this.logFile;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setLogFile(filePath) {
|
|
216
|
+
this.logFile = filePath;
|
|
217
|
+
this.setupLogFile();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setLevel(level) {
|
|
221
|
+
if (this.levels.hasOwnProperty(level)) {
|
|
222
|
+
this.level = level;
|
|
223
|
+
} else {
|
|
224
|
+
this.warn(`Invalid log level: ${level}. Valid levels: ${Object.keys(this.levels).join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
clearLogFile() {
|
|
229
|
+
if (this.logFile && fs.existsSync(this.logFile)) {
|
|
230
|
+
try {
|
|
231
|
+
fs.writeFileSync(this.logFile, '', 'utf8');
|
|
232
|
+
this.info('Log file cleared');
|
|
233
|
+
} catch (error) {
|
|
234
|
+
this.error('Failed to clear log file', { error });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
rotateLogFile() {
|
|
240
|
+
if (!this.logFile || !fs.existsSync(this.logFile)) return;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
244
|
+
const rotatedFile = this.logFile + '.' + timestamp;
|
|
245
|
+
|
|
246
|
+
fs.renameSync(this.logFile, rotatedFile);
|
|
247
|
+
this.info(`Log file rotated to: ${rotatedFile}`);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.error('Failed to rotate log file', { error });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Global logger instance
|
|
255
|
+
let globalLogger = null;
|
|
256
|
+
|
|
257
|
+
function getLogger(options = {}) {
|
|
258
|
+
if (!globalLogger) {
|
|
259
|
+
globalLogger = new Logger(options);
|
|
260
|
+
}
|
|
261
|
+
return globalLogger;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function setLogger(logger) {
|
|
265
|
+
globalLogger = logger;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
Logger,
|
|
270
|
+
getLogger,
|
|
271
|
+
setLogger
|
|
272
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// model-classifier.js
|
|
3
|
+
// Classifier that adds `categories` (multi) and `primary_category` (single) to each model
|
|
4
|
+
//
|
|
5
|
+
// IMPORTANT: STATIC DATABASE WITH PRE-CLASSIFIED CATEGORIES
|
|
6
|
+
// This system uses a static model database (177 models) with pre-classified categories.
|
|
7
|
+
// If you need to update the model database in the future:
|
|
8
|
+
// 1. Run the database update process
|
|
9
|
+
// 2. RE-ADD CATEGORIES: Run this classification system again on all new models
|
|
10
|
+
// 3. Update the cache file with the new categories
|
|
11
|
+
// 4. Test all use-case filters (coding, creative, reasoning, multimodal, embeddings, etc.)
|
|
12
|
+
//
|
|
13
|
+
// The classification rules below must be applied to any new models added to the database.
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
const CANON = ['coding','reasoning','creative','chat','multimodal','embeddings','safety','general'];
|
|
18
|
+
|
|
19
|
+
const CATEGORY_SYNONYMS = {
|
|
20
|
+
// normalize incoming "category" values
|
|
21
|
+
talking: 'chat',
|
|
22
|
+
multimodal: 'multimodal',
|
|
23
|
+
embeddings: 'embeddings',
|
|
24
|
+
reasoning: 'reasoning',
|
|
25
|
+
coding: 'coding',
|
|
26
|
+
safety: 'safety',
|
|
27
|
+
general: 'general',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const RULES = [
|
|
31
|
+
// embeddings - very specific patterns
|
|
32
|
+
{cat:'embeddings', re: /(embedding|embed|e5|bge|gte|minilm|paraphrase|text-embedding|nomic[-\s]?embed|voyage|snowflake-embed|arctic-embed|mxbai-embed)/i},
|
|
33
|
+
|
|
34
|
+
// multimodal / vision - specific vision models
|
|
35
|
+
{cat:'multimodal', re: /(llava|vision|vl\b|moondream|paligemma|pixtral|idefics|granite.*vision|llama3\.2[-_ ]?vision|qwen.*vl)/i},
|
|
36
|
+
|
|
37
|
+
// safety/moderation
|
|
38
|
+
{cat:'safety', re: /(guard|shield|moderation|safety)/i},
|
|
39
|
+
|
|
40
|
+
// coding - be more specific, exclude general models
|
|
41
|
+
{cat:'coding', re: /^(code|coder|codellama|starcoder|magicoder|phind[-_ ]?codellama|codeqwen|granite[-_ ]?code|yi[-_ ]?coder|opencoder|stable[-_ ]?code|deepseek[-_ ]?coder|codegemma)/i},
|
|
42
|
+
|
|
43
|
+
// reasoning / math
|
|
44
|
+
{cat:'reasoning', re: /(deepseek[-_ ]?r1|reason|r1\b|math|mathstral|wizard[-_ ]?math|gsm8k|mmlu|logic|phi4[-_ ]?reasoning|o1\b)/i},
|
|
45
|
+
|
|
46
|
+
// creative / RP / storytelling - look for specific creative models
|
|
47
|
+
{cat:'creative', re: /(dolphin(?![-_ ]?coder)|wizard(?![-_ ]?math)|mytho|synthia|airoboros|uncensored|roleplay|storyteller|creative|fiction)/i},
|
|
48
|
+
|
|
49
|
+
// chat / assistants - general conversational models
|
|
50
|
+
{cat:'chat', re: /(llama(?!.*code)|mistral(?!.*code)|qwen(?!.*code|.*vl)|gemma(?!.*code)|chat|assistant|hermes|openhermes|command\b|mistrallite|reflection)/i},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const PRIMARY_ORDER = [
|
|
54
|
+
'embeddings','safety','coding','reasoning','creative','multimodal','chat','general'
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function toText(model) {
|
|
58
|
+
return [
|
|
59
|
+
model.model_identifier, model.model_name,
|
|
60
|
+
model.description, model.detailed_description,
|
|
61
|
+
model.category, model.use_cases, model.input_types,
|
|
62
|
+
model.tags, model.labels, model.url
|
|
63
|
+
]
|
|
64
|
+
.flat().filter(Boolean).map(String).join(' ').toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeFromCategoryField(cat) {
|
|
68
|
+
if (!cat) return [];
|
|
69
|
+
const c = CATEGORY_SYNONYMS[String(cat).toLowerCase()];
|
|
70
|
+
return c ? [c] : [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fromUseCases(use_cases=[]) {
|
|
74
|
+
const set = new Set();
|
|
75
|
+
for (const u of use_cases.map(x => String(x).toLowerCase())) {
|
|
76
|
+
if (/(coding|programming|development)/.test(u)) set.add('coding');
|
|
77
|
+
if (/(reasoning|mathematics|logic)/.test(u)) set.add('reasoning');
|
|
78
|
+
if (/(chat|conversation|assistant)/.test(u)) set.add('chat');
|
|
79
|
+
if (/(vision|image|multimodal)/.test(u)) set.add('multimodal');
|
|
80
|
+
if (/(embedding|embeddings|search|similarity)/.test(u)) set.add('embeddings');
|
|
81
|
+
if (/(safety|moderation)/.test(u)) set.add('safety');
|
|
82
|
+
if (/(creative|story|roleplay)/.test(u)) set.add('creative');
|
|
83
|
+
}
|
|
84
|
+
return [...set];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fromInputTypes(input_types=[]) {
|
|
88
|
+
const set = new Set();
|
|
89
|
+
const low = input_types.map(x => String(x).toLowerCase());
|
|
90
|
+
// Only use input_types as hints, not definitive classification
|
|
91
|
+
// Most models have ['text', 'image', 'code'] automatically, so ignore generic ones
|
|
92
|
+
if (low.includes('image') && low.length === 1) set.add('multimodal'); // only if pure image model
|
|
93
|
+
if (low.includes('audio') && low.length === 1) set.add('multimodal'); // only if pure audio model
|
|
94
|
+
// Don't auto-classify as coding based on input_types since all models have 'code'
|
|
95
|
+
return [...set];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function regexRules(haystack) {
|
|
99
|
+
const set = new Set();
|
|
100
|
+
for (const {cat, re} of RULES) if (re.test(haystack)) set.add(cat);
|
|
101
|
+
return [...set];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function choosePrimary(cats) {
|
|
105
|
+
for (const c of PRIMARY_ORDER) if (cats.has(c)) return c;
|
|
106
|
+
return 'general';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function classifyModel(m) {
|
|
110
|
+
const hay = toText(m);
|
|
111
|
+
const cats = new Set([
|
|
112
|
+
...normalizeFromCategoryField(m.category),
|
|
113
|
+
...fromUseCases(m.use_cases),
|
|
114
|
+
...fromInputTypes(m.input_types),
|
|
115
|
+
...regexRules(hay),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
if (cats.size === 0) cats.add('general');
|
|
119
|
+
|
|
120
|
+
// Keep only canonical labels
|
|
121
|
+
for (const c of [...cats]) if (!CANON.includes(c)) cats.delete(c);
|
|
122
|
+
|
|
123
|
+
const primary = choosePrimary(cats);
|
|
124
|
+
const categories = [...cats].sort();
|
|
125
|
+
|
|
126
|
+
// Also annotate variants (handy for tag-level filtering)
|
|
127
|
+
const variants = (m.variants || []).map(v => {
|
|
128
|
+
const vHay = [m.model_name, v.tag, v.size, v.quantization].filter(Boolean).join(' ').toLowerCase();
|
|
129
|
+
const vCats = new Set(categories);
|
|
130
|
+
regexRules(vHay).forEach(c => vCats.add(c));
|
|
131
|
+
return { ...v, categories: [...vCats].sort() };
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return { ...m, categories, primary_category: primary, variants };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function classifyAllModels(inputData) {
|
|
138
|
+
const models = inputData.models || [];
|
|
139
|
+
const classifiedModels = models.map(classifyModel);
|
|
140
|
+
return { ...inputData, models: classifiedModels };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function run(path) {
|
|
144
|
+
try {
|
|
145
|
+
const input = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
146
|
+
const output = classifyAllModels(input);
|
|
147
|
+
console.log(JSON.stringify(output, null, 2));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof SyntaxError) {
|
|
150
|
+
console.error(`Invalid JSON in file: ${path}`, error.message);
|
|
151
|
+
} else {
|
|
152
|
+
console.error(`Error reading file: ${path}`, error.message);
|
|
153
|
+
}
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (require.main === module) {
|
|
159
|
+
const path = process.argv[2];
|
|
160
|
+
if (!path) {
|
|
161
|
+
console.error('Usage: node model-classifier.js models.json > models_with_categories.json');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
run(path);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { classifyAllModels, classifyModel };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verbose Progress Reporter - Enhanced Visual Style
|
|
6
|
+
* Muestra operaciones paso a paso con barras de progreso y spinners
|
|
7
|
+
*/
|
|
8
|
+
class VerboseProgress {
|
|
9
|
+
constructor(enabled = true) {
|
|
10
|
+
this.enabled = enabled;
|
|
11
|
+
this.currentStep = 0;
|
|
12
|
+
this.totalSteps = 0;
|
|
13
|
+
this.operationTitle = '';
|
|
14
|
+
this.startTime = null;
|
|
15
|
+
this.stepTimes = [];
|
|
16
|
+
this.currentSpinner = null;
|
|
17
|
+
this.stepStartTime = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Inicia una nueva operación con múltiples pasos
|
|
22
|
+
*/
|
|
23
|
+
startOperation(title, totalSteps) {
|
|
24
|
+
if (!this.enabled) return;
|
|
25
|
+
|
|
26
|
+
this.operationTitle = title;
|
|
27
|
+
this.totalSteps = totalSteps;
|
|
28
|
+
this.currentStep = 0;
|
|
29
|
+
this.startTime = Date.now();
|
|
30
|
+
this.stepTimes = [];
|
|
31
|
+
|
|
32
|
+
console.log(''); // Espacio inicial
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Avanza al siguiente paso de la operación
|
|
37
|
+
*/
|
|
38
|
+
step(description, details = null) {
|
|
39
|
+
if (!this.enabled) return;
|
|
40
|
+
|
|
41
|
+
// Finalizar spinner anterior si existe (disabled)
|
|
42
|
+
if (this.currentSpinner) {
|
|
43
|
+
// this.currentSpinner.stop(); // Disabled to prevent UI issues
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.currentStep++;
|
|
47
|
+
this.stepStartTime = Date.now();
|
|
48
|
+
|
|
49
|
+
// Crear indicador de progreso visual
|
|
50
|
+
const progress = this.createProgressBar();
|
|
51
|
+
const stepIndicator = chalk.cyan(`[${this.currentStep}/${this.totalSteps}]`);
|
|
52
|
+
|
|
53
|
+
// Mostrar el paso actual
|
|
54
|
+
console.log(`\n${progress} ${stepIndicator} ${chalk.white.bold(description)}`);
|
|
55
|
+
|
|
56
|
+
if (details) {
|
|
57
|
+
console.log(` ${chalk.gray('└─ ' + details)}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Crear spinner para este paso (disabled to fix UI issues)
|
|
61
|
+
this.currentSpinner = null;
|
|
62
|
+
|
|
63
|
+
this.stepTimes.push(this.stepStartTime);
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Muestra progreso dentro de un paso (sub-operación)
|
|
70
|
+
*/
|
|
71
|
+
substep(description, isLast = false) {
|
|
72
|
+
if (!this.enabled) return;
|
|
73
|
+
|
|
74
|
+
const connector = isLast ? '└─' : '├─';
|
|
75
|
+
const elapsed = this.getStepElapsedTime();
|
|
76
|
+
console.log(` ${chalk.gray(connector)} ${description} ${chalk.dim(`(${elapsed})`)}`);
|
|
77
|
+
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Marca el paso actual como completado exitosamente
|
|
83
|
+
*/
|
|
84
|
+
stepComplete(result = null, timing = null) {
|
|
85
|
+
if (!this.enabled) return;
|
|
86
|
+
|
|
87
|
+
if (result) {
|
|
88
|
+
console.log(` ${chalk.green(result)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Mostrar timing
|
|
92
|
+
const elapsed = this.getStepElapsedTime();
|
|
93
|
+
console.log(` ${chalk.dim(`└─ ${elapsed}`)}`);
|
|
94
|
+
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Marca el paso actual como fallido
|
|
100
|
+
*/
|
|
101
|
+
stepFail(error = null) {
|
|
102
|
+
if (!this.enabled) return;
|
|
103
|
+
|
|
104
|
+
if (error) {
|
|
105
|
+
console.log(` ${chalk.red(error)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Muestra información adicional durante un paso
|
|
113
|
+
*/
|
|
114
|
+
info(message, indent = true) {
|
|
115
|
+
if (!this.enabled) return;
|
|
116
|
+
|
|
117
|
+
const prefix = indent ? ' ' : '';
|
|
118
|
+
console.log(`${prefix}${chalk.gray(message)}`);
|
|
119
|
+
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Muestra una advertencia
|
|
125
|
+
*/
|
|
126
|
+
warn(message, indent = true) {
|
|
127
|
+
if (!this.enabled) return;
|
|
128
|
+
|
|
129
|
+
const prefix = indent ? ' ' : '';
|
|
130
|
+
console.log(`${prefix}${chalk.yellow(message)}`);
|
|
131
|
+
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Muestra resultados o datos encontrados
|
|
137
|
+
*/
|
|
138
|
+
found(message, count = null, indent = true) {
|
|
139
|
+
if (!this.enabled) return;
|
|
140
|
+
|
|
141
|
+
const prefix = indent ? ' ' : '';
|
|
142
|
+
const countStr = count !== null ? chalk.cyan.bold(` (${count})`) : '';
|
|
143
|
+
console.log(`${prefix}${chalk.white(message)}${countStr}`);
|
|
144
|
+
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Finaliza la operación completa
|
|
150
|
+
*/
|
|
151
|
+
complete(summary = null) {
|
|
152
|
+
if (!this.enabled) return;
|
|
153
|
+
|
|
154
|
+
// Finalizar spinner si existe (disabled)
|
|
155
|
+
if (this.currentSpinner) {
|
|
156
|
+
// this.currentSpinner.stop(); // Disabled to prevent UI issues
|
|
157
|
+
this.currentSpinner = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const totalTime = this.getTotalElapsedTime();
|
|
161
|
+
|
|
162
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
163
|
+
|
|
164
|
+
if (summary) {
|
|
165
|
+
console.log(chalk.green.bold(`${this.operationTitle} complete!`));
|
|
166
|
+
console.log(chalk.gray(` ${summary}`));
|
|
167
|
+
} else {
|
|
168
|
+
console.log(chalk.green.bold(`Operation complete!`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(chalk.dim(` Total time: ${totalTime}`));
|
|
172
|
+
console.log('');
|
|
173
|
+
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Finaliza la operación con error
|
|
179
|
+
*/
|
|
180
|
+
fail(error = null) {
|
|
181
|
+
if (!this.enabled) return;
|
|
182
|
+
|
|
183
|
+
// Finalizar spinner si existe (disabled)
|
|
184
|
+
if (this.currentSpinner) {
|
|
185
|
+
// this.currentSpinner.fail(); // Disabled to prevent UI issues
|
|
186
|
+
this.currentSpinner = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
190
|
+
console.log(chalk.red.bold(`Operation failed!`));
|
|
191
|
+
|
|
192
|
+
if (error) {
|
|
193
|
+
console.log(chalk.red(` ${error}`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log('');
|
|
197
|
+
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Crea una barra de progreso visual
|
|
203
|
+
*/
|
|
204
|
+
createProgressBar() {
|
|
205
|
+
if (this.totalSteps === 0) return '';
|
|
206
|
+
|
|
207
|
+
const percentage = (this.currentStep / this.totalSteps);
|
|
208
|
+
const filledBars = Math.floor(percentage * 20);
|
|
209
|
+
const emptyBars = 20 - filledBars;
|
|
210
|
+
|
|
211
|
+
const filled = chalk.cyan('█'.repeat(filledBars));
|
|
212
|
+
const empty = chalk.gray('░'.repeat(emptyBars));
|
|
213
|
+
|
|
214
|
+
return `${filled}${empty} ${Math.round(percentage * 100)}%`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Obtiene el tiempo transcurrido desde el inicio de la operación
|
|
219
|
+
*/
|
|
220
|
+
getTotalElapsedTime() {
|
|
221
|
+
if (!this.startTime) return '0ms';
|
|
222
|
+
|
|
223
|
+
const elapsed = Date.now() - this.startTime;
|
|
224
|
+
|
|
225
|
+
if (elapsed < 1000) return `${elapsed}ms`;
|
|
226
|
+
if (elapsed < 60000) return `${(elapsed / 1000).toFixed(1)}s`;
|
|
227
|
+
|
|
228
|
+
const minutes = Math.floor(elapsed / 60000);
|
|
229
|
+
const seconds = Math.floor((elapsed % 60000) / 1000);
|
|
230
|
+
return `${minutes}m ${seconds}s`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Obtiene el tiempo transcurrido desde el último paso
|
|
235
|
+
*/
|
|
236
|
+
getElapsedTime() {
|
|
237
|
+
if (this.stepTimes.length === 0) return '0ms';
|
|
238
|
+
|
|
239
|
+
const lastStepTime = this.stepTimes[this.stepTimes.length - 1];
|
|
240
|
+
const elapsed = Date.now() - lastStepTime;
|
|
241
|
+
|
|
242
|
+
if (elapsed < 1000) return `${elapsed}ms`;
|
|
243
|
+
return `${(elapsed / 1000).toFixed(1)}s`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Obtiene el tiempo transcurrido desde el inicio del paso actual
|
|
248
|
+
*/
|
|
249
|
+
getStepElapsedTime() {
|
|
250
|
+
if (!this.stepStartTime) return '0ms';
|
|
251
|
+
|
|
252
|
+
const elapsed = Date.now() - this.stepStartTime;
|
|
253
|
+
|
|
254
|
+
if (elapsed < 1000) return `${elapsed}ms`;
|
|
255
|
+
return `${(elapsed / 1000).toFixed(1)}s`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Método estático para crear una instancia rápida
|
|
260
|
+
*/
|
|
261
|
+
static create(enabled = true) {
|
|
262
|
+
return new VerboseProgress(enabled);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = VerboseProgress;
|