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.
Files changed (224) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +382 -0
  3. package/dist/camera/camera.d.ts +62 -0
  4. package/dist/camera/camera.d.ts.map +1 -0
  5. package/dist/camera/camera.js +155 -0
  6. package/dist/camera/camera.js.map +1 -0
  7. package/dist/camera/index.d.ts +18 -0
  8. package/dist/camera/index.d.ts.map +1 -0
  9. package/dist/camera/index.js +18 -0
  10. package/dist/camera/index.js.map +1 -0
  11. package/dist/config/config-types.d.ts +75 -0
  12. package/dist/config/config-types.d.ts.map +1 -0
  13. package/dist/config/config-types.generated.d.ts +495 -0
  14. package/dist/config/config-types.generated.d.ts.map +1 -0
  15. package/dist/config/config-types.generated.js +2 -0
  16. package/dist/config/config-types.generated.js.map +1 -0
  17. package/dist/config/config-types.js +175 -0
  18. package/dist/config/config-types.js.map +1 -0
  19. package/dist/config/index.d.ts +20 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +19 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/config/tjbot-config.d.ts +98 -0
  24. package/dist/config/tjbot-config.d.ts.map +1 -0
  25. package/dist/config/tjbot-config.js +309 -0
  26. package/dist/config/tjbot-config.js.map +1 -0
  27. package/dist/config/vendor/colors.yaml +61 -0
  28. package/dist/config/vendor/model-registry.yaml +275 -0
  29. package/dist/config/vendor/tjbot-config.schema.yaml +792 -0
  30. package/dist/config/vendor/tjbot.default.toml +452 -0
  31. package/dist/led/index.d.ts +20 -0
  32. package/dist/led/index.d.ts.map +1 -0
  33. package/dist/led/index.js +20 -0
  34. package/dist/led/index.js.map +1 -0
  35. package/dist/led/led-common-anode.d.ts +38 -0
  36. package/dist/led/led-common-anode.d.ts.map +1 -0
  37. package/dist/led/led-common-anode.js +79 -0
  38. package/dist/led/led-common-anode.js.map +1 -0
  39. package/dist/led/led-neopixel-spi.d.ts +60 -0
  40. package/dist/led/led-neopixel-spi.d.ts.map +1 -0
  41. package/dist/led/led-neopixel-spi.js +216 -0
  42. package/dist/led/led-neopixel-spi.js.map +1 -0
  43. package/dist/led/led-neopixel-ws281x.js +186 -0
  44. package/dist/led/led-neopixel.d.ts +57 -0
  45. package/dist/led/led-neopixel.d.ts.map +1 -0
  46. package/dist/led/led-neopixel.js +235 -0
  47. package/dist/led/led-neopixel.js.map +1 -0
  48. package/dist/microphone/index.d.ts +18 -0
  49. package/dist/microphone/index.d.ts.map +1 -0
  50. package/dist/microphone/index.js +18 -0
  51. package/dist/microphone/index.js.map +1 -0
  52. package/dist/microphone/microphone.d.ts +65 -0
  53. package/dist/microphone/microphone.d.ts.map +1 -0
  54. package/dist/microphone/microphone.js +179 -0
  55. package/dist/microphone/microphone.js.map +1 -0
  56. package/dist/rpi-drivers/index.d.ts +22 -0
  57. package/dist/rpi-drivers/index.d.ts.map +1 -0
  58. package/dist/rpi-drivers/index.js +22 -0
  59. package/dist/rpi-drivers/index.js.map +1 -0
  60. package/dist/rpi-drivers/rpi-detect.d.ts +24 -0
  61. package/dist/rpi-drivers/rpi-detect.d.ts.map +1 -0
  62. package/dist/rpi-drivers/rpi-detect.js +49 -0
  63. package/dist/rpi-drivers/rpi-detect.js.map +1 -0
  64. package/dist/rpi-drivers/rpi-driver.d.ts +116 -0
  65. package/dist/rpi-drivers/rpi-driver.d.ts.map +1 -0
  66. package/dist/rpi-drivers/rpi-driver.js +261 -0
  67. package/dist/rpi-drivers/rpi-driver.js.map +1 -0
  68. package/dist/rpi-drivers/rpi3-driver.d.ts +47 -0
  69. package/dist/rpi-drivers/rpi3-driver.d.ts.map +1 -0
  70. package/dist/rpi-drivers/rpi3-driver.js +145 -0
  71. package/dist/rpi-drivers/rpi3-driver.js.map +1 -0
  72. package/dist/rpi-drivers/rpi4-driver.d.ts +35 -0
  73. package/dist/rpi-drivers/rpi4-driver.d.ts.map +1 -0
  74. package/dist/rpi-drivers/rpi4-driver.js +101 -0
  75. package/dist/rpi-drivers/rpi4-driver.js.map +1 -0
  76. package/dist/rpi-drivers/rpi5-driver.d.ts +33 -0
  77. package/dist/rpi-drivers/rpi5-driver.d.ts.map +1 -0
  78. package/dist/rpi-drivers/rpi5-driver.js +78 -0
  79. package/dist/rpi-drivers/rpi5-driver.js.map +1 -0
  80. package/dist/servo/index.d.ts +19 -0
  81. package/dist/servo/index.d.ts.map +1 -0
  82. package/dist/servo/index.js +19 -0
  83. package/dist/servo/index.js.map +1 -0
  84. package/dist/servo/servo-constants.d.ts +33 -0
  85. package/dist/servo/servo-constants.d.ts.map +1 -0
  86. package/dist/servo/servo-constants.js +34 -0
  87. package/dist/servo/servo-constants.js.map +1 -0
  88. package/dist/servo/servo-lgpio.d.ts +82 -0
  89. package/dist/servo/servo-lgpio.d.ts.map +1 -0
  90. package/dist/servo/servo-lgpio.js +178 -0
  91. package/dist/servo/servo-lgpio.js.map +1 -0
  92. package/dist/speaker/audio-player.d.ts +30 -0
  93. package/dist/speaker/audio-player.d.ts.map +1 -0
  94. package/dist/speaker/audio-player.js +68 -0
  95. package/dist/speaker/audio-player.js.map +1 -0
  96. package/dist/speaker/index.d.ts +18 -0
  97. package/dist/speaker/index.d.ts.map +1 -0
  98. package/dist/speaker/index.js +18 -0
  99. package/dist/speaker/index.js.map +1 -0
  100. package/dist/speaker/speaker.d.ts +53 -0
  101. package/dist/speaker/speaker.d.ts.map +1 -0
  102. package/dist/speaker/speaker.js +125 -0
  103. package/dist/speaker/speaker.js.map +1 -0
  104. package/dist/stt/backends/azure-stt.d.ts +32 -0
  105. package/dist/stt/backends/azure-stt.d.ts.map +1 -0
  106. package/dist/stt/backends/azure-stt.js +227 -0
  107. package/dist/stt/backends/azure-stt.js.map +1 -0
  108. package/dist/stt/backends/google-cloud-stt.d.ts +31 -0
  109. package/dist/stt/backends/google-cloud-stt.d.ts.map +1 -0
  110. package/dist/stt/backends/google-cloud-stt.js +371 -0
  111. package/dist/stt/backends/google-cloud-stt.js.map +1 -0
  112. package/dist/stt/backends/ibm-watson-stt.d.ts +32 -0
  113. package/dist/stt/backends/ibm-watson-stt.d.ts.map +1 -0
  114. package/dist/stt/backends/ibm-watson-stt.js +190 -0
  115. package/dist/stt/backends/ibm-watson-stt.js.map +1 -0
  116. package/dist/stt/backends/sherpa-onnx-stt.d.ts +117 -0
  117. package/dist/stt/backends/sherpa-onnx-stt.d.ts.map +1 -0
  118. package/dist/stt/backends/sherpa-onnx-stt.js +694 -0
  119. package/dist/stt/backends/sherpa-onnx-stt.js.map +1 -0
  120. package/dist/stt/index.d.ts +20 -0
  121. package/dist/stt/index.d.ts.map +1 -0
  122. package/dist/stt/index.js +21 -0
  123. package/dist/stt/index.js.map +1 -0
  124. package/dist/stt/stt-engine.d.ts +68 -0
  125. package/dist/stt/stt-engine.d.ts.map +1 -0
  126. package/dist/stt/stt-engine.js +99 -0
  127. package/dist/stt/stt-engine.js.map +1 -0
  128. package/dist/stt/stt-utils.d.ts +36 -0
  129. package/dist/stt/stt-utils.d.ts.map +1 -0
  130. package/dist/stt/stt-utils.js +112 -0
  131. package/dist/stt/stt-utils.js.map +1 -0
  132. package/dist/stt/stt.d.ts +52 -0
  133. package/dist/stt/stt.d.ts.map +1 -0
  134. package/dist/stt/stt.js +100 -0
  135. package/dist/stt/stt.js.map +1 -0
  136. package/dist/tjbot.d.ts +317 -0
  137. package/dist/tjbot.d.ts.map +1 -0
  138. package/dist/tjbot.js +736 -0
  139. package/dist/tjbot.js.map +1 -0
  140. package/dist/tts/backends/azure-tts.d.ts +30 -0
  141. package/dist/tts/backends/azure-tts.d.ts.map +1 -0
  142. package/dist/tts/backends/azure-tts.js +92 -0
  143. package/dist/tts/backends/azure-tts.js.map +1 -0
  144. package/dist/tts/backends/google-cloud-tts.d.ts +38 -0
  145. package/dist/tts/backends/google-cloud-tts.d.ts.map +1 -0
  146. package/dist/tts/backends/google-cloud-tts.js +116 -0
  147. package/dist/tts/backends/google-cloud-tts.js.map +1 -0
  148. package/dist/tts/backends/ibm-watson-tts.d.ts +42 -0
  149. package/dist/tts/backends/ibm-watson-tts.d.ts.map +1 -0
  150. package/dist/tts/backends/ibm-watson-tts.js +99 -0
  151. package/dist/tts/backends/ibm-watson-tts.js.map +1 -0
  152. package/dist/tts/backends/sherpa-onnx-tts.d.ts +80 -0
  153. package/dist/tts/backends/sherpa-onnx-tts.d.ts.map +1 -0
  154. package/dist/tts/backends/sherpa-onnx-tts.js +237 -0
  155. package/dist/tts/backends/sherpa-onnx-tts.js.map +1 -0
  156. package/dist/tts/index.d.ts +19 -0
  157. package/dist/tts/index.d.ts.map +1 -0
  158. package/dist/tts/index.js +20 -0
  159. package/dist/tts/index.js.map +1 -0
  160. package/dist/tts/tts-engine.d.ts +67 -0
  161. package/dist/tts/tts-engine.d.ts.map +1 -0
  162. package/dist/tts/tts-engine.js +109 -0
  163. package/dist/tts/tts-engine.js.map +1 -0
  164. package/dist/tts/tts.d.ts +47 -0
  165. package/dist/tts/tts.d.ts.map +1 -0
  166. package/dist/tts/tts.js +101 -0
  167. package/dist/tts/tts.js.map +1 -0
  168. package/dist/utils/colors.d.ts +39 -0
  169. package/dist/utils/colors.d.ts.map +1 -0
  170. package/dist/utils/colors.js +155 -0
  171. package/dist/utils/colors.js.map +1 -0
  172. package/dist/utils/constants.d.ts +41 -0
  173. package/dist/utils/constants.d.ts.map +1 -0
  174. package/dist/utils/constants.js +43 -0
  175. package/dist/utils/constants.js.map +1 -0
  176. package/dist/utils/credentials.d.ts +43 -0
  177. package/dist/utils/credentials.d.ts.map +1 -0
  178. package/dist/utils/credentials.js +121 -0
  179. package/dist/utils/credentials.js.map +1 -0
  180. package/dist/utils/errors.d.ts +26 -0
  181. package/dist/utils/errors.d.ts.map +1 -0
  182. package/dist/utils/errors.js +32 -0
  183. package/dist/utils/errors.js.map +1 -0
  184. package/dist/utils/index.d.ts +25 -0
  185. package/dist/utils/index.d.ts.map +1 -0
  186. package/dist/utils/index.js +23 -0
  187. package/dist/utils/index.js.map +1 -0
  188. package/dist/utils/logging.d.ts +44 -0
  189. package/dist/utils/logging.d.ts.map +1 -0
  190. package/dist/utils/logging.js +113 -0
  191. package/dist/utils/logging.js.map +1 -0
  192. package/dist/utils/model-registry.d.ts +142 -0
  193. package/dist/utils/model-registry.d.ts.map +1 -0
  194. package/dist/utils/model-registry.js +391 -0
  195. package/dist/utils/model-registry.js.map +1 -0
  196. package/dist/utils/utils.d.ts +33 -0
  197. package/dist/utils/utils.d.ts.map +1 -0
  198. package/dist/utils/utils.js +50 -0
  199. package/dist/utils/utils.js.map +1 -0
  200. package/dist/vision/backends/azure-vision.d.ts +33 -0
  201. package/dist/vision/backends/azure-vision.d.ts.map +1 -0
  202. package/dist/vision/backends/azure-vision.js +151 -0
  203. package/dist/vision/backends/azure-vision.js.map +1 -0
  204. package/dist/vision/backends/google-cloud-vision.d.ts +32 -0
  205. package/dist/vision/backends/google-cloud-vision.d.ts.map +1 -0
  206. package/dist/vision/backends/google-cloud-vision.js +193 -0
  207. package/dist/vision/backends/google-cloud-vision.js.map +1 -0
  208. package/dist/vision/backends/onnx.d.ts +116 -0
  209. package/dist/vision/backends/onnx.d.ts.map +1 -0
  210. package/dist/vision/backends/onnx.js +781 -0
  211. package/dist/vision/backends/onnx.js.map +1 -0
  212. package/dist/vision/index.d.ts +19 -0
  213. package/dist/vision/index.d.ts.map +1 -0
  214. package/dist/vision/index.js +20 -0
  215. package/dist/vision/index.js.map +1 -0
  216. package/dist/vision/vision-engine.d.ts +131 -0
  217. package/dist/vision/vision-engine.d.ts.map +1 -0
  218. package/dist/vision/vision-engine.js +97 -0
  219. package/dist/vision/vision-engine.js.map +1 -0
  220. package/dist/vision/vision.d.ts +48 -0
  221. package/dist/vision/vision.d.ts.map +1 -0
  222. package/dist/vision/vision.js +83 -0
  223. package/dist/vision/vision.js.map +1 -0
  224. 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