tjbot-ce 3.0.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/LICENSE +202 -0
- package/README.md +382 -0
- package/dist/camera/camera.d.ts +62 -0
- package/dist/camera/camera.d.ts.map +1 -0
- package/dist/camera/camera.js +155 -0
- package/dist/camera/camera.js.map +1 -0
- package/dist/camera/index.d.ts +18 -0
- package/dist/camera/index.d.ts.map +1 -0
- package/dist/camera/index.js +18 -0
- package/dist/camera/index.js.map +1 -0
- package/dist/config/config-types.d.ts +75 -0
- package/dist/config/config-types.d.ts.map +1 -0
- package/dist/config/config-types.generated.d.ts +495 -0
- package/dist/config/config-types.generated.d.ts.map +1 -0
- package/dist/config/config-types.generated.js +2 -0
- package/dist/config/config-types.generated.js.map +1 -0
- package/dist/config/config-types.js +175 -0
- package/dist/config/config-types.js.map +1 -0
- package/dist/config/index.d.ts +20 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +19 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/tjbot-config.d.ts +98 -0
- package/dist/config/tjbot-config.d.ts.map +1 -0
- package/dist/config/tjbot-config.js +309 -0
- package/dist/config/tjbot-config.js.map +1 -0
- package/dist/config/vendor/colors.yaml +61 -0
- package/dist/config/vendor/model-registry.yaml +275 -0
- package/dist/config/vendor/tjbot-config.schema.yaml +792 -0
- package/dist/config/vendor/tjbot.default.toml +452 -0
- package/dist/led/index.d.ts +20 -0
- package/dist/led/index.d.ts.map +1 -0
- package/dist/led/index.js +20 -0
- package/dist/led/index.js.map +1 -0
- package/dist/led/led-common-anode.d.ts +38 -0
- package/dist/led/led-common-anode.d.ts.map +1 -0
- package/dist/led/led-common-anode.js +79 -0
- package/dist/led/led-common-anode.js.map +1 -0
- package/dist/led/led-neopixel-spi.d.ts +60 -0
- package/dist/led/led-neopixel-spi.d.ts.map +1 -0
- package/dist/led/led-neopixel-spi.js +216 -0
- package/dist/led/led-neopixel-spi.js.map +1 -0
- package/dist/led/led-neopixel-ws281x.js +186 -0
- package/dist/led/led-neopixel.d.ts +57 -0
- package/dist/led/led-neopixel.d.ts.map +1 -0
- package/dist/led/led-neopixel.js +235 -0
- package/dist/led/led-neopixel.js.map +1 -0
- package/dist/microphone/index.d.ts +18 -0
- package/dist/microphone/index.d.ts.map +1 -0
- package/dist/microphone/index.js +18 -0
- package/dist/microphone/index.js.map +1 -0
- package/dist/microphone/microphone.d.ts +65 -0
- package/dist/microphone/microphone.d.ts.map +1 -0
- package/dist/microphone/microphone.js +179 -0
- package/dist/microphone/microphone.js.map +1 -0
- package/dist/rpi-drivers/index.d.ts +22 -0
- package/dist/rpi-drivers/index.d.ts.map +1 -0
- package/dist/rpi-drivers/index.js +22 -0
- package/dist/rpi-drivers/index.js.map +1 -0
- package/dist/rpi-drivers/rpi-detect.d.ts +24 -0
- package/dist/rpi-drivers/rpi-detect.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi-detect.js +49 -0
- package/dist/rpi-drivers/rpi-detect.js.map +1 -0
- package/dist/rpi-drivers/rpi-driver.d.ts +116 -0
- package/dist/rpi-drivers/rpi-driver.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi-driver.js +261 -0
- package/dist/rpi-drivers/rpi-driver.js.map +1 -0
- package/dist/rpi-drivers/rpi3-driver.d.ts +47 -0
- package/dist/rpi-drivers/rpi3-driver.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi3-driver.js +145 -0
- package/dist/rpi-drivers/rpi3-driver.js.map +1 -0
- package/dist/rpi-drivers/rpi4-driver.d.ts +35 -0
- package/dist/rpi-drivers/rpi4-driver.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi4-driver.js +101 -0
- package/dist/rpi-drivers/rpi4-driver.js.map +1 -0
- package/dist/rpi-drivers/rpi5-driver.d.ts +33 -0
- package/dist/rpi-drivers/rpi5-driver.d.ts.map +1 -0
- package/dist/rpi-drivers/rpi5-driver.js +78 -0
- package/dist/rpi-drivers/rpi5-driver.js.map +1 -0
- package/dist/servo/index.d.ts +19 -0
- package/dist/servo/index.d.ts.map +1 -0
- package/dist/servo/index.js +19 -0
- package/dist/servo/index.js.map +1 -0
- package/dist/servo/servo-constants.d.ts +33 -0
- package/dist/servo/servo-constants.d.ts.map +1 -0
- package/dist/servo/servo-constants.js +34 -0
- package/dist/servo/servo-constants.js.map +1 -0
- package/dist/servo/servo-lgpio.d.ts +82 -0
- package/dist/servo/servo-lgpio.d.ts.map +1 -0
- package/dist/servo/servo-lgpio.js +178 -0
- package/dist/servo/servo-lgpio.js.map +1 -0
- package/dist/speaker/audio-player.d.ts +30 -0
- package/dist/speaker/audio-player.d.ts.map +1 -0
- package/dist/speaker/audio-player.js +68 -0
- package/dist/speaker/audio-player.js.map +1 -0
- package/dist/speaker/index.d.ts +18 -0
- package/dist/speaker/index.d.ts.map +1 -0
- package/dist/speaker/index.js +18 -0
- package/dist/speaker/index.js.map +1 -0
- package/dist/speaker/speaker.d.ts +53 -0
- package/dist/speaker/speaker.d.ts.map +1 -0
- package/dist/speaker/speaker.js +125 -0
- package/dist/speaker/speaker.js.map +1 -0
- package/dist/stt/backends/azure-stt.d.ts +32 -0
- package/dist/stt/backends/azure-stt.d.ts.map +1 -0
- package/dist/stt/backends/azure-stt.js +227 -0
- package/dist/stt/backends/azure-stt.js.map +1 -0
- package/dist/stt/backends/google-cloud-stt.d.ts +31 -0
- package/dist/stt/backends/google-cloud-stt.d.ts.map +1 -0
- package/dist/stt/backends/google-cloud-stt.js +371 -0
- package/dist/stt/backends/google-cloud-stt.js.map +1 -0
- package/dist/stt/backends/ibm-watson-stt.d.ts +32 -0
- package/dist/stt/backends/ibm-watson-stt.d.ts.map +1 -0
- package/dist/stt/backends/ibm-watson-stt.js +190 -0
- package/dist/stt/backends/ibm-watson-stt.js.map +1 -0
- package/dist/stt/backends/sherpa-onnx-stt.d.ts +117 -0
- package/dist/stt/backends/sherpa-onnx-stt.d.ts.map +1 -0
- package/dist/stt/backends/sherpa-onnx-stt.js +694 -0
- package/dist/stt/backends/sherpa-onnx-stt.js.map +1 -0
- package/dist/stt/index.d.ts +20 -0
- package/dist/stt/index.d.ts.map +1 -0
- package/dist/stt/index.js +21 -0
- package/dist/stt/index.js.map +1 -0
- package/dist/stt/stt-engine.d.ts +68 -0
- package/dist/stt/stt-engine.d.ts.map +1 -0
- package/dist/stt/stt-engine.js +99 -0
- package/dist/stt/stt-engine.js.map +1 -0
- package/dist/stt/stt-utils.d.ts +36 -0
- package/dist/stt/stt-utils.d.ts.map +1 -0
- package/dist/stt/stt-utils.js +112 -0
- package/dist/stt/stt-utils.js.map +1 -0
- package/dist/stt/stt.d.ts +52 -0
- package/dist/stt/stt.d.ts.map +1 -0
- package/dist/stt/stt.js +100 -0
- package/dist/stt/stt.js.map +1 -0
- package/dist/tjbot.d.ts +317 -0
- package/dist/tjbot.d.ts.map +1 -0
- package/dist/tjbot.js +736 -0
- package/dist/tjbot.js.map +1 -0
- package/dist/tts/backends/azure-tts.d.ts +30 -0
- package/dist/tts/backends/azure-tts.d.ts.map +1 -0
- package/dist/tts/backends/azure-tts.js +92 -0
- package/dist/tts/backends/azure-tts.js.map +1 -0
- package/dist/tts/backends/google-cloud-tts.d.ts +38 -0
- package/dist/tts/backends/google-cloud-tts.d.ts.map +1 -0
- package/dist/tts/backends/google-cloud-tts.js +116 -0
- package/dist/tts/backends/google-cloud-tts.js.map +1 -0
- package/dist/tts/backends/ibm-watson-tts.d.ts +42 -0
- package/dist/tts/backends/ibm-watson-tts.d.ts.map +1 -0
- package/dist/tts/backends/ibm-watson-tts.js +99 -0
- package/dist/tts/backends/ibm-watson-tts.js.map +1 -0
- package/dist/tts/backends/sherpa-onnx-tts.d.ts +80 -0
- package/dist/tts/backends/sherpa-onnx-tts.d.ts.map +1 -0
- package/dist/tts/backends/sherpa-onnx-tts.js +237 -0
- package/dist/tts/backends/sherpa-onnx-tts.js.map +1 -0
- package/dist/tts/index.d.ts +19 -0
- package/dist/tts/index.d.ts.map +1 -0
- package/dist/tts/index.js +20 -0
- package/dist/tts/index.js.map +1 -0
- package/dist/tts/tts-engine.d.ts +67 -0
- package/dist/tts/tts-engine.d.ts.map +1 -0
- package/dist/tts/tts-engine.js +109 -0
- package/dist/tts/tts-engine.js.map +1 -0
- package/dist/tts/tts.d.ts +47 -0
- package/dist/tts/tts.d.ts.map +1 -0
- package/dist/tts/tts.js +101 -0
- package/dist/tts/tts.js.map +1 -0
- package/dist/utils/colors.d.ts +39 -0
- package/dist/utils/colors.d.ts.map +1 -0
- package/dist/utils/colors.js +155 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/constants.d.ts +41 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +43 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/credentials.d.ts +43 -0
- package/dist/utils/credentials.d.ts.map +1 -0
- package/dist/utils/credentials.js +121 -0
- package/dist/utils/credentials.js.map +1 -0
- package/dist/utils/errors.d.ts +26 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +32 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +25 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +23 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logging.d.ts +44 -0
- package/dist/utils/logging.d.ts.map +1 -0
- package/dist/utils/logging.js +113 -0
- package/dist/utils/logging.js.map +1 -0
- package/dist/utils/model-registry.d.ts +142 -0
- package/dist/utils/model-registry.d.ts.map +1 -0
- package/dist/utils/model-registry.js +391 -0
- package/dist/utils/model-registry.js.map +1 -0
- package/dist/utils/utils.d.ts +33 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +50 -0
- package/dist/utils/utils.js.map +1 -0
- package/dist/vision/backends/azure-vision.d.ts +33 -0
- package/dist/vision/backends/azure-vision.d.ts.map +1 -0
- package/dist/vision/backends/azure-vision.js +151 -0
- package/dist/vision/backends/azure-vision.js.map +1 -0
- package/dist/vision/backends/google-cloud-vision.d.ts +32 -0
- package/dist/vision/backends/google-cloud-vision.d.ts.map +1 -0
- package/dist/vision/backends/google-cloud-vision.js +193 -0
- package/dist/vision/backends/google-cloud-vision.js.map +1 -0
- package/dist/vision/backends/onnx.d.ts +116 -0
- package/dist/vision/backends/onnx.d.ts.map +1 -0
- package/dist/vision/backends/onnx.js +781 -0
- package/dist/vision/backends/onnx.js.map +1 -0
- package/dist/vision/index.d.ts +19 -0
- package/dist/vision/index.d.ts.map +1 -0
- package/dist/vision/index.js +20 -0
- package/dist/vision/index.js.map +1 -0
- package/dist/vision/vision-engine.d.ts +131 -0
- package/dist/vision/vision-engine.d.ts.map +1 -0
- package/dist/vision/vision-engine.js +97 -0
- package/dist/vision/vision-engine.js.map +1 -0
- package/dist/vision/vision.d.ts +48 -0
- package/dist/vision/vision.d.ts.map +1 -0
- package/dist/vision/vision.js +83 -0
- package/dist/vision/vision.js.map +1 -0
- package/package.json +124 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026-present TJBot Contributors. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import winston from 'winston';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
export var LogEmoji;
|
|
19
|
+
(function (LogEmoji) {
|
|
20
|
+
LogEmoji["CAMERA"] = "\uD83D\uDCF7";
|
|
21
|
+
LogEmoji["COLOR"] = "\uD83C\uDFA8";
|
|
22
|
+
LogEmoji["CONFIG"] = "\u2699\uFE0F";
|
|
23
|
+
LogEmoji["GENERAL"] = "\uD83E\uDD16";
|
|
24
|
+
LogEmoji["HARDWARE"] = "\uD83D\uDD27";
|
|
25
|
+
LogEmoji["LED"] = "\uD83D\uDCA1";
|
|
26
|
+
LogEmoji["MIC"] = "\uD83C\uDFA4";
|
|
27
|
+
LogEmoji["MODEL"] = "\uD83D\uDCE6";
|
|
28
|
+
LogEmoji["RPI"] = "\uD83C\uDF53";
|
|
29
|
+
LogEmoji["SERVO"] = "\uD83E\uDDBE";
|
|
30
|
+
LogEmoji["SPEAKER"] = "\uD83D\uDD08";
|
|
31
|
+
LogEmoji["STT"] = "\uD83E\uDDBB";
|
|
32
|
+
LogEmoji["TTS"] = "\uD83D\uDCAC";
|
|
33
|
+
LogEmoji["VISION"] = "\uD83D\uDC41\uFE0F";
|
|
34
|
+
})(LogEmoji || (LogEmoji = {}));
|
|
35
|
+
let winstonInitialized = false;
|
|
36
|
+
const LOGGER_NAME_EMOJI_RULES = [
|
|
37
|
+
['/camera/', LogEmoji.CAMERA],
|
|
38
|
+
['/config/', LogEmoji.CONFIG],
|
|
39
|
+
['/led/', LogEmoji.LED],
|
|
40
|
+
['/microphone/', LogEmoji.MIC],
|
|
41
|
+
['/rpi-drivers/', LogEmoji.RPI],
|
|
42
|
+
['/servo/', LogEmoji.SERVO],
|
|
43
|
+
['/speaker/', LogEmoji.SPEAKER],
|
|
44
|
+
['/stt/', LogEmoji.STT],
|
|
45
|
+
['/tts/', LogEmoji.TTS],
|
|
46
|
+
['/vision/', LogEmoji.VISION],
|
|
47
|
+
['model-registry', LogEmoji.MODEL],
|
|
48
|
+
['colors', LogEmoji.COLOR],
|
|
49
|
+
];
|
|
50
|
+
function normalizeModuleName(moduleName) {
|
|
51
|
+
if (!moduleName) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
const lower = moduleName.toLowerCase();
|
|
55
|
+
if (!lower.startsWith('file://')) {
|
|
56
|
+
return lower;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return fileURLToPath(moduleName).toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return lower;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function emojiForModuleName(moduleName) {
|
|
66
|
+
const normalized = normalizeModuleName(moduleName);
|
|
67
|
+
for (const [pattern, emoji] of LOGGER_NAME_EMOJI_RULES) {
|
|
68
|
+
if (normalized.includes(pattern)) {
|
|
69
|
+
return emoji;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return LogEmoji.GENERAL;
|
|
73
|
+
}
|
|
74
|
+
const prettyErrorFormat = winston.format.printf(((info) => {
|
|
75
|
+
const emoji = emojiForModuleName(typeof info.moduleName === 'string' ? info.moduleName : undefined);
|
|
76
|
+
let message = `${info.level}: ${emoji} ${info.message}`;
|
|
77
|
+
const metadata = { ...info };
|
|
78
|
+
delete metadata.level;
|
|
79
|
+
delete metadata.message;
|
|
80
|
+
delete metadata.moduleName;
|
|
81
|
+
delete metadata[Symbol.for('level')];
|
|
82
|
+
delete metadata[Symbol.for('message')];
|
|
83
|
+
delete metadata[Symbol.for('splat')];
|
|
84
|
+
if (Object.keys(metadata).length > 0) {
|
|
85
|
+
const jsonString = JSON.stringify(metadata, null, 2);
|
|
86
|
+
message += ' \x1b[36m' + jsonString + '\x1b[0m';
|
|
87
|
+
}
|
|
88
|
+
return message;
|
|
89
|
+
}));
|
|
90
|
+
/**
|
|
91
|
+
* Initialize Winston with TJBot's default formatter.
|
|
92
|
+
* Safe to call repeatedly; only the first call configures transports/format.
|
|
93
|
+
* Later calls only update log level.
|
|
94
|
+
*/
|
|
95
|
+
export function initWinston(level = 'info') {
|
|
96
|
+
if (!winstonInitialized) {
|
|
97
|
+
winston.configure({
|
|
98
|
+
level,
|
|
99
|
+
format: winston.format.combine(winston.format.colorize(), prettyErrorFormat),
|
|
100
|
+
transports: [new winston.transports.Console()],
|
|
101
|
+
});
|
|
102
|
+
winstonInitialized = true;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
winston.level = level;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get a module-scoped logger that lets the formatter infer a category emoji.
|
|
109
|
+
*/
|
|
110
|
+
export function getLogger(moduleName) {
|
|
111
|
+
return winston.child({ moduleName });
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=logging.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging.js","sourceRoot":"","sources":["../../src/utils/logging.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026-present TJBot Contributors. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Types of models managed by ModelManager
|
|
18
|
+
* Includes base types (stt, tts, vad) and vision subtypes
|
|
19
|
+
*/
|
|
20
|
+
export type ModelType = 'stt' | 'tts' | 'vad' | 'vision.object-recognition' | 'vision.classification' | 'vision.face-detection' | 'vision.image-description';
|
|
21
|
+
/**
|
|
22
|
+
* Base model metadata
|
|
23
|
+
*/
|
|
24
|
+
export interface BaseModelMetadata {
|
|
25
|
+
type: ModelType;
|
|
26
|
+
key: string;
|
|
27
|
+
label: string;
|
|
28
|
+
url: string;
|
|
29
|
+
folder: string;
|
|
30
|
+
required: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* STT model metadata
|
|
34
|
+
*/
|
|
35
|
+
export interface STTModelMetadata extends BaseModelMetadata {
|
|
36
|
+
kind: 'offline' | 'offline-whisper' | 'streaming-zipformer' | 'streaming';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* TTS model metadata
|
|
40
|
+
*/
|
|
41
|
+
export interface TTSModelMetadata extends BaseModelMetadata {
|
|
42
|
+
kind: 'vits-piper' | 'tacotron' | 'fastpitch' | 'streaming';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* VAD model metadata
|
|
46
|
+
*/
|
|
47
|
+
export type VADModelMetadata = BaseModelMetadata;
|
|
48
|
+
/**
|
|
49
|
+
* Vision model metadata
|
|
50
|
+
*/
|
|
51
|
+
export interface VisionModelMetadata extends BaseModelMetadata {
|
|
52
|
+
kind: 'detection' | 'classification' | 'face-detection' | 'image-description';
|
|
53
|
+
labelUrl?: string;
|
|
54
|
+
inputShape?: number[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Unified singleton registry for all TJBot models (STT, TTS, VAD, Vision)
|
|
58
|
+
* Handles model metadata, registration, downloading, extraction, and caching
|
|
59
|
+
*/
|
|
60
|
+
export declare class ModelRegistry {
|
|
61
|
+
private static instance?;
|
|
62
|
+
private registeredModels;
|
|
63
|
+
private metadataLoaded;
|
|
64
|
+
private constructor();
|
|
65
|
+
/**
|
|
66
|
+
* Get singleton instance
|
|
67
|
+
*/
|
|
68
|
+
static getInstance(): ModelRegistry;
|
|
69
|
+
/**
|
|
70
|
+
* Load model metadata from unified YAML file
|
|
71
|
+
* If no path provided, uses default model-registry.yaml in config directory
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
private loadMetadata;
|
|
75
|
+
/**
|
|
76
|
+
* Get model cache directory
|
|
77
|
+
*/
|
|
78
|
+
getModelCacheDir(): string;
|
|
79
|
+
/**
|
|
80
|
+
* Get model cache directory for a specific type
|
|
81
|
+
* @param modelType The model type (stt, tts, vad, vision.*)
|
|
82
|
+
* @returns The cache directory path for the specified model type
|
|
83
|
+
*/
|
|
84
|
+
getModelCacheDirForType(modelType: ModelType | 'vision'): string;
|
|
85
|
+
/**
|
|
86
|
+
* Register a model in the registry
|
|
87
|
+
* @param model The model metadata to register
|
|
88
|
+
*/
|
|
89
|
+
registerModel(model: BaseModelMetadata): void;
|
|
90
|
+
/**
|
|
91
|
+
* Lookup model metadata by key
|
|
92
|
+
* @param modelKey The model key
|
|
93
|
+
* @returns The model metadata
|
|
94
|
+
* @throws Error if model not found
|
|
95
|
+
*/
|
|
96
|
+
lookupModel<T extends BaseModelMetadata = BaseModelMetadata>(modelKey: string): T;
|
|
97
|
+
/**
|
|
98
|
+
* Lookup model metadata by type & installation status
|
|
99
|
+
* @param modelType The model type
|
|
100
|
+
* @param installedOnly If true, only return models that are downloaded
|
|
101
|
+
* @returns List of model metadata of the specified type
|
|
102
|
+
*/
|
|
103
|
+
lookupModels<T extends BaseModelMetadata = BaseModelMetadata>(modelType?: ModelType, installedOnly?: boolean): T[];
|
|
104
|
+
/**
|
|
105
|
+
* Ensure a model is downloaded and return its path
|
|
106
|
+
* @param modelKey The model key
|
|
107
|
+
* @returns The model metadata
|
|
108
|
+
* @throws TJBotError if model not found or download fails
|
|
109
|
+
*/
|
|
110
|
+
loadModel<T extends BaseModelMetadata = BaseModelMetadata>(modelKey: string): Promise<T>;
|
|
111
|
+
/**
|
|
112
|
+
* Check if a model is downloaded in the specified cache directory
|
|
113
|
+
* @param modelKey The model key
|
|
114
|
+
* @returns True if the model is downloaded, false otherwise
|
|
115
|
+
*/
|
|
116
|
+
isModelDownloaded(modelKey: string): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Copy a file from local filesystem
|
|
119
|
+
* Supports file:// URLs
|
|
120
|
+
*/
|
|
121
|
+
private copyFile;
|
|
122
|
+
/**
|
|
123
|
+
* Download a file from URL with progress bar and exponential backoff retry
|
|
124
|
+
* Retries up to 3 times with delays: 1s, 2s, 4s
|
|
125
|
+
* Supports both http/https URLs and file:// URLs
|
|
126
|
+
*/
|
|
127
|
+
private downloadFile;
|
|
128
|
+
/**
|
|
129
|
+
* Extract tar.bz2 archive using system tar command
|
|
130
|
+
*/
|
|
131
|
+
private extractTarBz2;
|
|
132
|
+
/**
|
|
133
|
+
* Download and extract a model
|
|
134
|
+
* @param modelKey The model key
|
|
135
|
+
* @throws Error if download or extraction fails
|
|
136
|
+
*/
|
|
137
|
+
downloadModel(modelKey: string): Promise<{
|
|
138
|
+
primaryPath: string;
|
|
139
|
+
cachePath: string;
|
|
140
|
+
} | undefined>;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=model-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../../src/utils/model-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAkBH;;;GAGG;AACH,MAAM,MAAM,SAAS,GACf,KAAK,GACL,KAAK,GACL,KAAK,GACL,2BAA2B,GAC3B,uBAAuB,GACvB,uBAAuB,GACvB,0BAA0B,CAAC;AAEjC;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACvD,IAAI,EAAE,SAAS,GAAG,iBAAiB,GAAG,qBAAqB,GAAG,WAAW,CAAC;CAC7E;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACvD,IAAI,EAAE,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;CAC/D;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC1D,IAAI,EAAE,WAAW,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,mBAAmB,CAAC;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AASD;;;GAGG;AACH,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAgB;IACxC,OAAO,CAAC,gBAAgB,CAA6C;IACrE,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO;IAIP;;OAEG;IACH,MAAM,CAAC,WAAW,IAAI,aAAa;IAOnC;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAiDpB;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;;;OAIG;IACH,uBAAuB,CAAC,SAAS,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM;IAUhE;;;OAGG;IACH,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAK7C;;;;;OAKG;IACH,WAAW,CAAC,CAAC,SAAS,iBAAiB,GAAG,iBAAiB,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC;IAQjF;;;;;OAKG;IACH,YAAY,CAAC,CAAC,SAAS,iBAAiB,GAAG,iBAAiB,EACxD,SAAS,CAAC,EAAE,SAAS,EACrB,aAAa,GAAE,OAAe,GAC/B,CAAC,EAAE;IAcN;;;;;OAKG;IACG,SAAS,CAAC,CAAC,SAAS,iBAAiB,GAAG,iBAAiB,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAiB9F;;;;OAIG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAa5C;;;OAGG;YACW,QAAQ;IAoEtB;;;;OAIG;YACW,YAAY;IAyF1B;;OAEG;YACW,aAAa;IAM3B;;;;OAIG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM;;;;CAoDvC"}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026-present TJBot Contributors. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { execFile } from 'child_process';
|
|
17
|
+
import cliProgress from 'cli-progress';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import yaml from 'js-yaml';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { Readable } from 'stream';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { promisify } from 'util';
|
|
25
|
+
import { TJBotError } from './errors.js';
|
|
26
|
+
import { getLogger } from './logging.js';
|
|
27
|
+
const logger = getLogger(import.meta.url);
|
|
28
|
+
const execFileAsync = promisify(execFile);
|
|
29
|
+
/**
|
|
30
|
+
* Unified singleton registry for all TJBot models (STT, TTS, VAD, Vision)
|
|
31
|
+
* Handles model metadata, registration, downloading, extraction, and caching
|
|
32
|
+
*/
|
|
33
|
+
export class ModelRegistry {
|
|
34
|
+
static instance;
|
|
35
|
+
registeredModels = new Map();
|
|
36
|
+
metadataLoaded = false;
|
|
37
|
+
constructor() {
|
|
38
|
+
this.loadMetadata();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get singleton instance
|
|
42
|
+
*/
|
|
43
|
+
static getInstance() {
|
|
44
|
+
if (!this.instance) {
|
|
45
|
+
this.instance = new ModelRegistry();
|
|
46
|
+
}
|
|
47
|
+
return this.instance;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Load model metadata from unified YAML file
|
|
51
|
+
* If no path provided, uses default model-registry.yaml in config directory
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
loadMetadata(yamlPath) {
|
|
55
|
+
if (this.metadataLoaded) {
|
|
56
|
+
logger.debug('loadMetadata() called but model metadata already loaded');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
// Default to model-registry.yaml in config directory
|
|
61
|
+
if (!yamlPath) {
|
|
62
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
63
|
+
const __dirname = path.dirname(__filename);
|
|
64
|
+
yamlPath = path.join(__dirname, '..', 'config', 'vendor', 'model-registry.yaml');
|
|
65
|
+
}
|
|
66
|
+
logger.verbose(`Loading model metadata from: ${yamlPath}`);
|
|
67
|
+
const fileContents = fs.readFileSync(yamlPath, 'utf8');
|
|
68
|
+
const data = yaml.load(fileContents);
|
|
69
|
+
const models = (data.models || []).map((m) => {
|
|
70
|
+
const key = m.key;
|
|
71
|
+
if (!key) {
|
|
72
|
+
throw new TJBotError('Model entry missing key');
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
...m,
|
|
76
|
+
key,
|
|
77
|
+
folder: m.folder ?? key,
|
|
78
|
+
required: m.required ?? [],
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
// Register all loaded models
|
|
82
|
+
for (const model of models) {
|
|
83
|
+
this.registerModel(model);
|
|
84
|
+
}
|
|
85
|
+
this.metadataLoaded = true;
|
|
86
|
+
logger.info(`Loaded metadata for ${this.registeredModels.size} ML models`);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
logger.error('Failed to load ML model metadata:', error);
|
|
90
|
+
throw new TJBotError('Failed to load ML model metadata', { cause: error });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Cache Directory Paths
|
|
95
|
+
// ============================================================================
|
|
96
|
+
/**
|
|
97
|
+
* Get model cache directory
|
|
98
|
+
*/
|
|
99
|
+
getModelCacheDir() {
|
|
100
|
+
return path.join(os.homedir(), '.tjbot', 'models');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get model cache directory for a specific type
|
|
104
|
+
* @param modelType The model type (stt, tts, vad, vision.*)
|
|
105
|
+
* @returns The cache directory path for the specified model type
|
|
106
|
+
*/
|
|
107
|
+
getModelCacheDirForType(modelType) {
|
|
108
|
+
// For vision subtypes, use 'vision' as the base directory
|
|
109
|
+
const cacheSubdir = modelType.startsWith('vision.') ? 'vision' : modelType;
|
|
110
|
+
return path.join(this.getModelCacheDir(), cacheSubdir);
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Query Methods
|
|
114
|
+
// ============================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Register a model in the registry
|
|
117
|
+
* @param model The model metadata to register
|
|
118
|
+
*/
|
|
119
|
+
registerModel(model) {
|
|
120
|
+
this.registeredModels.set(model.key, model);
|
|
121
|
+
logger.debug(`registered model: ${model.key} (type: ${model.type})`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Lookup model metadata by key
|
|
125
|
+
* @param modelKey The model key
|
|
126
|
+
* @returns The model metadata
|
|
127
|
+
* @throws Error if model not found
|
|
128
|
+
*/
|
|
129
|
+
lookupModel(modelKey) {
|
|
130
|
+
const model = this.registeredModels.get(modelKey);
|
|
131
|
+
if (!model) {
|
|
132
|
+
throw new TJBotError(`Model with key "${modelKey}" not found in registry`);
|
|
133
|
+
}
|
|
134
|
+
return model;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Lookup model metadata by type & installation status
|
|
138
|
+
* @param modelType The model type
|
|
139
|
+
* @param installedOnly If true, only return models that are downloaded
|
|
140
|
+
* @returns List of model metadata of the specified type
|
|
141
|
+
*/
|
|
142
|
+
lookupModels(modelType, installedOnly = false) {
|
|
143
|
+
let models;
|
|
144
|
+
if (installedOnly) {
|
|
145
|
+
models = Array.from(this.registeredModels.values()).filter((m) => (modelType === undefined || m.type === modelType) && this.isModelDownloaded(m.key));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
models = Array.from(this.registeredModels.values()).filter((m) => modelType === undefined || m.type === modelType);
|
|
149
|
+
}
|
|
150
|
+
return models;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Ensure a model is downloaded and return its path
|
|
154
|
+
* @param modelKey The model key
|
|
155
|
+
* @returns The model metadata
|
|
156
|
+
* @throws TJBotError if model not found or download fails
|
|
157
|
+
*/
|
|
158
|
+
async loadModel(modelKey) {
|
|
159
|
+
// Verify model exists in metadata
|
|
160
|
+
const model = this.lookupModel(modelKey);
|
|
161
|
+
// Download model if not already present
|
|
162
|
+
if (!this.isModelDownloaded(modelKey)) {
|
|
163
|
+
await this.downloadModel(modelKey);
|
|
164
|
+
}
|
|
165
|
+
// Return the model metadata
|
|
166
|
+
return model;
|
|
167
|
+
}
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Download/Extract Models
|
|
170
|
+
// ============================================================================
|
|
171
|
+
/**
|
|
172
|
+
* Check if a model is downloaded in the specified cache directory
|
|
173
|
+
* @param modelKey The model key
|
|
174
|
+
* @returns True if the model is downloaded, false otherwise
|
|
175
|
+
*/
|
|
176
|
+
isModelDownloaded(modelKey) {
|
|
177
|
+
const model = this.lookupModel(modelKey);
|
|
178
|
+
const cacheDir = this.getModelCacheDirForType(model.type);
|
|
179
|
+
const modelPath = path.join(cacheDir, model.folder);
|
|
180
|
+
if (!fs.existsSync(modelPath)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
// Check all required files exist
|
|
184
|
+
return model.required.every((file) => fs.existsSync(path.join(modelPath, file)));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Copy a file from local filesystem
|
|
188
|
+
* Supports file:// URLs
|
|
189
|
+
*/
|
|
190
|
+
async copyFile(sourceUrl, destination) {
|
|
191
|
+
try {
|
|
192
|
+
// Convert file:// URL to path
|
|
193
|
+
const sourcePath = sourceUrl.startsWith('file://') ? new URL(sourceUrl).pathname : sourceUrl;
|
|
194
|
+
logger.debug(`copying file from ${sourcePath} to ${destination}`);
|
|
195
|
+
// Get file size for progress bar
|
|
196
|
+
const stats = await fs.promises.stat(sourcePath);
|
|
197
|
+
const totalSize = stats.size;
|
|
198
|
+
let copiedSize = 0;
|
|
199
|
+
// Create progress bar
|
|
200
|
+
const progressBar = new cliProgress.SingleBar({
|
|
201
|
+
format: 'Copying [{bar}] {percentage}% | {value}/{total} MB',
|
|
202
|
+
barCompleteChar: '\u2588',
|
|
203
|
+
barIncompleteChar: '\u2591',
|
|
204
|
+
hideCursor: true,
|
|
205
|
+
});
|
|
206
|
+
if (totalSize > 0) {
|
|
207
|
+
progressBar.start(Math.round(totalSize / 1024 / 1024), 0);
|
|
208
|
+
}
|
|
209
|
+
// Create destination directory
|
|
210
|
+
await fs.promises.mkdir(path.dirname(destination), { recursive: true });
|
|
211
|
+
// Copy file with progress tracking
|
|
212
|
+
const reader = fs.createReadStream(sourcePath);
|
|
213
|
+
const writer = fs.createWriteStream(destination);
|
|
214
|
+
await new Promise((resolve, reject) => {
|
|
215
|
+
reader.on('data', (chunk) => {
|
|
216
|
+
copiedSize += chunk.length;
|
|
217
|
+
if (totalSize > 0) {
|
|
218
|
+
progressBar.update(Math.round(copiedSize / 1024 / 1024));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
reader.pipe(writer);
|
|
222
|
+
writer.on('finish', () => {
|
|
223
|
+
if (totalSize > 0) {
|
|
224
|
+
progressBar.stop();
|
|
225
|
+
}
|
|
226
|
+
logger.debug('file copy complete');
|
|
227
|
+
resolve();
|
|
228
|
+
});
|
|
229
|
+
writer.on('error', (err) => {
|
|
230
|
+
if (totalSize > 0) {
|
|
231
|
+
progressBar.stop();
|
|
232
|
+
}
|
|
233
|
+
reject(err);
|
|
234
|
+
});
|
|
235
|
+
reader.on('error', (err) => {
|
|
236
|
+
if (totalSize > 0) {
|
|
237
|
+
progressBar.stop();
|
|
238
|
+
}
|
|
239
|
+
reject(err);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
logger.error('File copy failed:', err);
|
|
245
|
+
throw new TJBotError(`Failed to copy file from ${sourceUrl}`, {
|
|
246
|
+
cause: err,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Download a file from URL with progress bar and exponential backoff retry
|
|
252
|
+
* Retries up to 3 times with delays: 1s, 2s, 4s
|
|
253
|
+
* Supports both http/https URLs and file:// URLs
|
|
254
|
+
*/
|
|
255
|
+
async downloadFile(url, destination, maxRetries = 3) {
|
|
256
|
+
// Handle file:// URLs (local files)
|
|
257
|
+
if (url.startsWith('file://')) {
|
|
258
|
+
return this.copyFile(url, destination);
|
|
259
|
+
}
|
|
260
|
+
let attempt = 0;
|
|
261
|
+
let lastError = null;
|
|
262
|
+
while (attempt < maxRetries) {
|
|
263
|
+
try {
|
|
264
|
+
if (attempt > 0) {
|
|
265
|
+
const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s
|
|
266
|
+
logger.info(`Retrying download in ${delay / 1000}s... (attempt ${attempt + 1}/${maxRetries})`);
|
|
267
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
268
|
+
}
|
|
269
|
+
logger.info(`Downloading from ${url} (attempt ${attempt + 1}/${maxRetries})`);
|
|
270
|
+
const response = await fetch(url);
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
273
|
+
}
|
|
274
|
+
const totalSize = parseInt(response.headers.get('content-length') ?? '0', 10);
|
|
275
|
+
let downloadedSize = 0;
|
|
276
|
+
// Create progress bar
|
|
277
|
+
const progressBar = new cliProgress.SingleBar({
|
|
278
|
+
format: 'Downloading [{bar}] {percentage}% | {value}/{total} MB',
|
|
279
|
+
barCompleteChar: '\u2588',
|
|
280
|
+
barIncompleteChar: '\u2591',
|
|
281
|
+
hideCursor: true,
|
|
282
|
+
});
|
|
283
|
+
if (totalSize > 0) {
|
|
284
|
+
progressBar.start(Math.round(totalSize / 1024 / 1024), 0);
|
|
285
|
+
}
|
|
286
|
+
await fs.promises.mkdir(path.dirname(destination), { recursive: true });
|
|
287
|
+
const writer = fs.createWriteStream(destination);
|
|
288
|
+
const nodeStream = Readable.from(response.body);
|
|
289
|
+
await new Promise((resolve, reject) => {
|
|
290
|
+
nodeStream.on('data', (chunk) => {
|
|
291
|
+
downloadedSize += chunk.length;
|
|
292
|
+
if (totalSize > 0) {
|
|
293
|
+
progressBar.update(Math.round(downloadedSize / 1024 / 1024));
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
nodeStream.pipe(writer);
|
|
297
|
+
writer.on('finish', () => {
|
|
298
|
+
if (totalSize > 0) {
|
|
299
|
+
progressBar.stop();
|
|
300
|
+
}
|
|
301
|
+
logger.info('Download complete');
|
|
302
|
+
resolve();
|
|
303
|
+
});
|
|
304
|
+
writer.on('error', (err) => {
|
|
305
|
+
if (totalSize > 0) {
|
|
306
|
+
progressBar.stop();
|
|
307
|
+
}
|
|
308
|
+
reject(err);
|
|
309
|
+
});
|
|
310
|
+
nodeStream.on('error', (err) => {
|
|
311
|
+
if (totalSize > 0) {
|
|
312
|
+
progressBar.stop();
|
|
313
|
+
}
|
|
314
|
+
reject(err);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
// Success!
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
lastError = err;
|
|
322
|
+
logger.warn(`Download failed (attempt ${attempt + 1}/${maxRetries}):`, err);
|
|
323
|
+
attempt++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// All retries exhausted
|
|
327
|
+
logger.error(`Download failed after ${maxRetries} attempts`);
|
|
328
|
+
throw new TJBotError(`Failed to download file after ${maxRetries} attempts`, {
|
|
329
|
+
cause: lastError || new Error('Unknown error'),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Extract tar.bz2 archive using system tar command
|
|
334
|
+
*/
|
|
335
|
+
async extractTarBz2(archivePath, destinationDir) {
|
|
336
|
+
logger.info('Extracting archive...');
|
|
337
|
+
await execFileAsync('tar', ['-xjf', archivePath, '-C', destinationDir]);
|
|
338
|
+
logger.info('Extraction complete');
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Download and extract a model
|
|
342
|
+
* @param modelKey The model key
|
|
343
|
+
* @throws Error if download or extraction fails
|
|
344
|
+
*/
|
|
345
|
+
async downloadModel(modelKey) {
|
|
346
|
+
const model = this.lookupModel(modelKey);
|
|
347
|
+
const cacheDir = this.getModelCacheDirForType(model.type);
|
|
348
|
+
const modelPath = path.join(cacheDir, model.folder);
|
|
349
|
+
if (this.isModelDownloaded(modelKey)) {
|
|
350
|
+
return { primaryPath: path.join(modelPath, model.required[0]), cachePath: modelPath };
|
|
351
|
+
}
|
|
352
|
+
// Ensure cache directory exists
|
|
353
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
354
|
+
// Download to temporary file
|
|
355
|
+
const tempArchivePath = path.join(os.tmpdir(), `${model.key}-download`);
|
|
356
|
+
await this.downloadFile(model.url, tempArchivePath);
|
|
357
|
+
// Check if we need to extract based on file extension
|
|
358
|
+
const isTarBz2 = model.url.endsWith('.tar.bz2');
|
|
359
|
+
if (isTarBz2) {
|
|
360
|
+
// Extract tar.bz2 archive
|
|
361
|
+
await this.extractTarBz2(tempArchivePath, cacheDir);
|
|
362
|
+
// Remove temporary archive file
|
|
363
|
+
fs.unlinkSync(tempArchivePath);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// Single file model - move directly to model path
|
|
367
|
+
await fs.promises.mkdir(modelPath, { recursive: true });
|
|
368
|
+
const fileName = path.basename(model.url).split('?')[0]; // Remove query params
|
|
369
|
+
const targetPath = path.join(modelPath, fileName);
|
|
370
|
+
// Use copy + unlink instead of rename to handle cross-filesystem moves (EXDEV error)
|
|
371
|
+
await fs.promises.copyFile(tempArchivePath, targetPath);
|
|
372
|
+
await fs.promises.unlink(tempArchivePath);
|
|
373
|
+
}
|
|
374
|
+
// Download labels file for vision models
|
|
375
|
+
if (typeof model.type === 'string' && model.type.startsWith('vision.')) {
|
|
376
|
+
const visionModel = model;
|
|
377
|
+
if (visionModel.labelUrl) {
|
|
378
|
+
const labelsFileName = path.basename(visionModel.labelUrl).split('?')[0]; // Extract filename, remove query params
|
|
379
|
+
const labelsFilePath = path.join(modelPath, labelsFileName);
|
|
380
|
+
await this.downloadFile(visionModel.labelUrl, labelsFilePath);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Verify all required files exist
|
|
384
|
+
if (!this.isModelDownloaded(modelKey)) {
|
|
385
|
+
throw new TJBotError(`Model "${modelKey}" download incomplete: required files missing`);
|
|
386
|
+
}
|
|
387
|
+
logger.info(`Model "${modelKey}" downloaded and extracted to ${modelPath}`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=model-registry.js.map
|