create-byan-agent 2.7.9 → 2.8.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.
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Parakeet TDT STT Backend
3
+ *
4
+ * Handles installation, configuration and status checks for
5
+ * NVIDIA Parakeet TDT (local NeMo or Docker).
6
+ *
7
+ * @module stt/parakeet-backend
8
+ */
9
+
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const { execSync } = require('child_process');
13
+ const chalk = require('chalk');
14
+ const ora = require('ora');
15
+ const { detectGPU, commandExists, checkPython } = require('./engine');
16
+
17
+ const PARAKEET_PORT = 8001;
18
+ const DEFAULT_MODEL = 'nvidia/parakeet-tdt-0.6b-v2';
19
+ const MIN_PYTHON_VERSION = '3.8';
20
+ const MIN_VRAM = 4000;
21
+
22
+ /**
23
+ * Install and configure the Parakeet TDT backend.
24
+ *
25
+ * @param {object} options
26
+ * @param {'local'|'docker'} options.mode
27
+ * @param {string} [options.model]
28
+ * @param {string[]} [options.languages]
29
+ * @param {string} options.projectRoot
30
+ * @returns {Promise<{ success: boolean, model: string, mode: string }>}
31
+ */
32
+ async function setup(options) {
33
+ const { mode, projectRoot } = options;
34
+ const model = options.model || DEFAULT_MODEL;
35
+ const languages = options.languages || ['fr', 'en'];
36
+
37
+ console.log(chalk.blue('\n Setting up Parakeet TDT backend...'));
38
+ console.log(chalk.gray(` Mode: ${mode} | Model: ${model} | Languages: ${languages.join(', ')}`));
39
+
40
+ const gpu = detectGPU();
41
+ if (!gpu.hasGPU || gpu.vram < MIN_VRAM) {
42
+ throw new Error(
43
+ `Parakeet TDT requires a GPU with >= ${MIN_VRAM} MB VRAM. `
44
+ + `Detected: ${gpu.hasGPU ? gpu.vram + ' MB' : 'no GPU'}. Use Whisper instead.`
45
+ );
46
+ }
47
+
48
+ if (mode === 'docker') {
49
+ await setupDocker(projectRoot);
50
+ } else {
51
+ await setupLocal(model);
52
+ }
53
+
54
+ return { success: true, model, mode };
55
+ }
56
+
57
+ /**
58
+ * Set up Parakeet via Docker Compose.
59
+ *
60
+ * @param {string} projectRoot
61
+ */
62
+ async function setupDocker(projectRoot) {
63
+ const spinner = ora('Generating Parakeet Docker Compose...').start();
64
+
65
+ try {
66
+ if (!commandExists('docker')) {
67
+ throw new Error('Docker is required for Parakeet Docker mode');
68
+ }
69
+
70
+ const content = getDockerCompose();
71
+ const composePath = path.join(projectRoot, 'docker-compose.parakeet.yml');
72
+ await fs.writeFile(composePath, content, 'utf-8');
73
+ spinner.succeed(`Parakeet Docker Compose written to ${composePath}`);
74
+
75
+ console.log(chalk.gray(' Start with: docker compose -f docker-compose.parakeet.yml up -d'));
76
+ } catch (err) {
77
+ spinner.fail('Failed to generate Parakeet Docker Compose');
78
+ throw err;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Set up Parakeet locally via pip + NeMo.
84
+ *
85
+ * @param {string} model
86
+ */
87
+ async function setupLocal(model) {
88
+ const spinner = ora('Checking Python environment...').start();
89
+
90
+ try {
91
+ const python = checkPython();
92
+ if (!python.available) {
93
+ throw new Error('Python 3 is required for local Parakeet installation');
94
+ }
95
+
96
+ if (!meetsMinVersion(python.version, MIN_PYTHON_VERSION)) {
97
+ throw new Error(
98
+ `Python >= ${MIN_PYTHON_VERSION} required, found ${python.version}`
99
+ );
100
+ }
101
+
102
+ spinner.succeed(`Python ${python.version} detected`);
103
+
104
+ if (!python.hasNemo) {
105
+ const pipSpinner = ora('Installing NVIDIA NeMo ASR toolkit (this may take a while)...').start();
106
+ try {
107
+ execSync('pip3 install "nvidia-nemo[asr]"', {
108
+ stdio: ['pipe', 'pipe', 'pipe'],
109
+ timeout: 600000 // 10 min
110
+ });
111
+ pipSpinner.succeed('NVIDIA NeMo ASR installed');
112
+ } catch (err) {
113
+ pipSpinner.fail('NeMo installation failed');
114
+ console.log(chalk.yellow(' Try manually: pip3 install "nvidia-nemo[asr]"'));
115
+ throw err;
116
+ }
117
+ } else {
118
+ console.log(chalk.green(' NeMo ASR already installed'));
119
+ }
120
+
121
+ // Pre-download model so first inference is not slow
122
+ const dlSpinner = ora(`Downloading model ${model}...`).start();
123
+ try {
124
+ execSync(
125
+ `python3 -c "from nemo.collections.asr.models import EncDecRNNTBPEModel; EncDecRNNTBPEModel.from_pretrained('${model}')"`,
126
+ { stdio: ['pipe', 'pipe', 'pipe'], timeout: 600000 }
127
+ );
128
+ dlSpinner.succeed('Model downloaded');
129
+ } catch (err) {
130
+ dlSpinner.fail('Model download failed');
131
+ console.log(chalk.yellow(` Try manually: python3 -c "from nemo.collections.asr.models import EncDecRNNTBPEModel; EncDecRNNTBPEModel.from_pretrained('${model}')"`));
132
+ throw err;
133
+ }
134
+ } catch (err) {
135
+ spinner.isSpinning && spinner.fail();
136
+ throw err;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Generate Docker Compose YAML for the Parakeet NeMo container.
142
+ *
143
+ * @returns {string}
144
+ */
145
+ function getDockerCompose() {
146
+ return `version: '3.8'
147
+
148
+ services:
149
+ parakeet-server:
150
+ image: nvcr.io/nvidia/nemo:24.07
151
+ ports:
152
+ - "${PARAKEET_PORT}:${PARAKEET_PORT}"
153
+ environment:
154
+ - MODEL_NAME=${DEFAULT_MODEL}
155
+ - NVIDIA_VISIBLE_DEVICES=all
156
+ volumes:
157
+ - parakeet-models:/root/.cache/huggingface
158
+ deploy:
159
+ resources:
160
+ reservations:
161
+ devices:
162
+ - driver: nvidia
163
+ count: 1
164
+ capabilities: [gpu]
165
+ restart: unless-stopped
166
+ healthcheck:
167
+ test: ["CMD", "curl", "-f", "http://localhost:${PARAKEET_PORT}/health"]
168
+ interval: 30s
169
+ timeout: 10s
170
+ retries: 5
171
+ start_period: 120s
172
+ command: >
173
+ python -c "
174
+ from nemo.collections.asr.models import EncDecRNNTBPEModel;
175
+ import json, http.server, io, soundfile as sf, numpy as np;
176
+ model = EncDecRNNTBPEModel.from_pretrained('${DEFAULT_MODEL}');
177
+
178
+ class Handler(http.server.BaseHTTPRequestHandler):
179
+ def do_GET(self):
180
+ if self.path == '/health':
181
+ self.send_response(200);
182
+ self.end_headers();
183
+ self.wfile.write(b'ok');
184
+ else:
185
+ self.send_response(404);
186
+ self.end_headers();
187
+ def do_POST(self):
188
+ length = int(self.headers['Content-Length']);
189
+ audio_bytes = self.rfile.read(length);
190
+ audio, sr = sf.read(io.BytesIO(audio_bytes));
191
+ transcription = model.transcribe([audio]);
192
+ result = json.dumps({'text': transcription[0] if transcription else ''});
193
+ self.send_response(200);
194
+ self.send_header('Content-Type', 'application/json');
195
+ self.end_headers();
196
+ self.wfile.write(result.encode());
197
+ def log_message(self, format, *args):
198
+ pass;
199
+
200
+ httpd = http.server.HTTPServer(('', ${PARAKEET_PORT}), Handler);
201
+ print('Parakeet TDT server listening on port ${PARAKEET_PORT}');
202
+ httpd.serve_forever()
203
+ "
204
+
205
+ volumes:
206
+ parakeet-models:
207
+ `;
208
+ }
209
+
210
+ /**
211
+ * Describe system requirements for Parakeet TDT.
212
+ *
213
+ * @returns {{ python: string, vram: string, packages: string[] }}
214
+ */
215
+ function getRequirements() {
216
+ return {
217
+ python: `>=${MIN_PYTHON_VERSION}`,
218
+ vram: `>=${MIN_VRAM} MB`,
219
+ packages: ['nvidia-nemo[asr]']
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Check whether the Parakeet server is responding.
225
+ *
226
+ * @returns {boolean}
227
+ */
228
+ function isRunning() {
229
+ try {
230
+ execSync(`curl -sf http://localhost:${PARAKEET_PORT}/health`, {
231
+ stdio: ['pipe', 'pipe', 'ignore'],
232
+ timeout: 3000
233
+ });
234
+ return true;
235
+ } catch {
236
+ return false;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Semver-light comparison: is `actual` >= `minimum`?
242
+ * Only compares major.minor.
243
+ *
244
+ * @param {string} actual - e.g. "3.10.12"
245
+ * @param {string} minimum - e.g. "3.8"
246
+ * @returns {boolean}
247
+ */
248
+ function meetsMinVersion(actual, minimum) {
249
+ const parse = v => v.split('.').map(Number);
250
+ const [aMaj, aMin = 0] = parse(actual);
251
+ const [mMaj, mMin = 0] = parse(minimum);
252
+ return aMaj > mMaj || (aMaj === mMaj && aMin >= mMin);
253
+ }
254
+
255
+ module.exports = {
256
+ setup,
257
+ getDockerCompose,
258
+ getRequirements,
259
+ isRunning,
260
+ PARAKEET_PORT,
261
+ DEFAULT_MODEL
262
+ };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Whisper STT Backend
3
+ *
4
+ * Handles installation, configuration and status checks for
5
+ * faster-whisper-server (Docker or local).
6
+ *
7
+ * @module stt/whisper-backend
8
+ */
9
+
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const { execSync } = require('child_process');
13
+ const chalk = require('chalk');
14
+ const ora = require('ora');
15
+ const { detectGPU, commandExists, getWhisperModel } = require('./engine');
16
+
17
+ const WHISPER_PORT = 8000;
18
+
19
+ /**
20
+ * Install and configure the Whisper STT backend.
21
+ *
22
+ * @param {object} options
23
+ * @param {'local'|'docker'} options.mode
24
+ * @param {string} [options.model] - Override model selection
25
+ * @param {string} options.projectRoot - Absolute path to project root
26
+ * @returns {Promise<{ success: boolean, model: string, mode: string }>}
27
+ */
28
+ async function setup(options) {
29
+ const { mode, projectRoot } = options;
30
+ const gpu = detectGPU();
31
+ const model = options.model || getWhisperModel(gpu.vram);
32
+
33
+ console.log(chalk.blue('\n Setting up Whisper STT backend...'));
34
+ console.log(chalk.gray(` Mode: ${mode} | Model: ${model}`));
35
+
36
+ if (mode === 'docker') {
37
+ await setupDocker(projectRoot, gpu, model);
38
+ } else {
39
+ await setupLocal();
40
+ }
41
+
42
+ return { success: true, model, mode };
43
+ }
44
+
45
+ /**
46
+ * Set up Whisper via Docker Compose.
47
+ *
48
+ * @param {string} projectRoot
49
+ * @param {object} gpu
50
+ * @param {string} model
51
+ */
52
+ async function setupDocker(projectRoot, gpu, model) {
53
+ const spinner = ora('Generating Whisper Docker Compose...').start();
54
+
55
+ try {
56
+ const content = getDockerCompose({ gpu, model });
57
+ const composePath = path.join(projectRoot, 'docker-compose.turbo-whisper.yml');
58
+ await fs.writeFile(composePath, content, 'utf-8');
59
+ spinner.succeed(`Whisper Docker Compose written to ${composePath}`);
60
+ } catch (err) {
61
+ spinner.fail('Failed to generate Docker Compose');
62
+ throw err;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Set up Whisper locally (clone repo, create venv, install deps).
68
+ */
69
+ async function setupLocal() {
70
+ const spinner = ora('Installing Whisper locally...').start();
71
+
72
+ try {
73
+ const serverDir = path.join(process.env.HOME, 'faster-whisper-server');
74
+
75
+ if (await fs.pathExists(serverDir)) {
76
+ spinner.text = 'Updating faster-whisper-server...';
77
+ execSync('git pull', { cwd: serverDir, stdio: 'pipe' });
78
+ } else {
79
+ spinner.text = 'Cloning faster-whisper-server...';
80
+ execSync(
81
+ `git clone https://github.com/fedirz/faster-whisper-server.git ${serverDir}`,
82
+ { stdio: 'pipe' }
83
+ );
84
+ }
85
+
86
+ spinner.text = 'Installing Python dependencies...';
87
+ execSync('python3 -m venv .venv && .venv/bin/pip install -e .', {
88
+ cwd: serverDir,
89
+ stdio: 'pipe'
90
+ });
91
+
92
+ spinner.succeed('Whisper local installation complete');
93
+ } catch (err) {
94
+ spinner.fail('Whisper local installation failed');
95
+ throw err;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Generate Docker Compose YAML content for Whisper.
101
+ *
102
+ * @param {object} [opts]
103
+ * @param {object} [opts.gpu] - GPU detection result
104
+ * @param {string} [opts.model] - Whisper model name
105
+ * @returns {string}
106
+ */
107
+ function getDockerCompose(opts = {}) {
108
+ const gpu = opts.gpu || detectGPU();
109
+ const model = opts.model || getWhisperModel(gpu.vram);
110
+ const modelFullName = `Systran/faster-whisper-${model}`;
111
+ const useGPU = gpu.hasGPU;
112
+
113
+ const gpuBlock = useGPU
114
+ ? `
115
+ deploy:
116
+ resources:
117
+ reservations:
118
+ devices:
119
+ - driver: nvidia
120
+ count: 1
121
+ capabilities: [gpu]`
122
+ : '';
123
+
124
+ return `version: '3.8'
125
+
126
+ services:
127
+ whisper-server:
128
+ image: fedirz/faster-whisper-server:latest-${useGPU ? 'cuda' : 'cpu'}
129
+ ports:
130
+ - "${WHISPER_PORT}:${WHISPER_PORT}"
131
+ environment:
132
+ - WHISPER__MODEL=${modelFullName}${gpuBlock}
133
+ restart: unless-stopped
134
+ `;
135
+ }
136
+
137
+ /**
138
+ * Select the best Whisper model for the given VRAM.
139
+ * Delegates to the shared engine helper.
140
+ *
141
+ * @param {number} vram - VRAM in MB
142
+ * @returns {string}
143
+ */
144
+ function getRecommendedModel(vram) {
145
+ return getWhisperModel(vram);
146
+ }
147
+
148
+ /**
149
+ * Check whether the Whisper server is responding on its port.
150
+ *
151
+ * @returns {boolean}
152
+ */
153
+ function isRunning() {
154
+ try {
155
+ execSync(`curl -sf http://localhost:${WHISPER_PORT}/health`, {
156
+ stdio: ['pipe', 'pipe', 'ignore'],
157
+ timeout: 3000
158
+ });
159
+ return true;
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ module.exports = {
166
+ setup,
167
+ getDockerCompose,
168
+ getRecommendedModel,
169
+ isRunning,
170
+ WHISPER_PORT
171
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * File Differ Utility
3
+ *
4
+ * Compares installed files against template files using SHA-256 hashes.
5
+ *
6
+ * @module utils/file-differ
7
+ */
8
+
9
+ const crypto = require('crypto');
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * Compute SHA-256 hash of a file.
15
+ *
16
+ * @param {string} filePath - Absolute path to the file
17
+ * @returns {Promise<string>} Hex-encoded SHA-256 hash
18
+ */
19
+ async function hashFile(filePath) {
20
+ const content = await fs.readFile(filePath);
21
+ return crypto.createHash('sha256').update(content).digest('hex');
22
+ }
23
+
24
+ /**
25
+ * Recursively collect all file paths relative to a root directory.
26
+ *
27
+ * @param {string} dir - Root directory to scan
28
+ * @param {string} [base] - Base path for recursion (internal)
29
+ * @returns {Promise<string[]>} Array of relative POSIX paths
30
+ */
31
+ async function collectFiles(dir, base) {
32
+ base = base || dir;
33
+ const entries = await fs.readdir(dir, { withFileTypes: true });
34
+ let files = [];
35
+
36
+ for (const entry of entries) {
37
+ const fullPath = path.join(dir, entry.name);
38
+ if (entry.isDirectory()) {
39
+ const nested = await collectFiles(fullPath, base);
40
+ files = files.concat(nested);
41
+ } else if (entry.isFile()) {
42
+ const rel = path.relative(base, fullPath).split(path.sep).join('/');
43
+ files.push(rel);
44
+ }
45
+ }
46
+
47
+ return files;
48
+ }
49
+
50
+ /**
51
+ * @typedef {Object} DiffResult
52
+ * @property {string[]} toUpdate - Files that exist in both dirs but differ
53
+ * @property {string[]} toAdd - Files only in template (new files)
54
+ * @property {string[]} toSkip - Files identical in both dirs
55
+ * @property {string[]} toKeep - Files only in installed dir (user-created)
56
+ */
57
+
58
+ /**
59
+ * Compare an installed directory against a template directory.
60
+ *
61
+ * @param {string} installedDir - Path to the installed _byan/ directory
62
+ * @param {string} templateDir - Path to the template _byan/ directory
63
+ * @returns {Promise<DiffResult>}
64
+ */
65
+ async function diffFiles(installedDir, templateDir) {
66
+ const [installedFiles, templateFiles] = await Promise.all([
67
+ collectFiles(installedDir),
68
+ collectFiles(templateDir)
69
+ ]);
70
+
71
+ const installedSet = new Set(installedFiles);
72
+ const templateSet = new Set(templateFiles);
73
+
74
+ const toUpdate = [];
75
+ const toAdd = [];
76
+ const toSkip = [];
77
+ const toKeep = [];
78
+
79
+ for (const file of templateFiles) {
80
+ if (!installedSet.has(file)) {
81
+ toAdd.push(file);
82
+ continue;
83
+ }
84
+
85
+ const [installedHash, templateHash] = await Promise.all([
86
+ hashFile(path.join(installedDir, file)),
87
+ hashFile(path.join(templateDir, file))
88
+ ]);
89
+
90
+ if (installedHash === templateHash) {
91
+ toSkip.push(file);
92
+ } else {
93
+ toUpdate.push(file);
94
+ }
95
+ }
96
+
97
+ for (const file of installedFiles) {
98
+ if (!templateSet.has(file)) {
99
+ toKeep.push(file);
100
+ }
101
+ }
102
+
103
+ return { toUpdate, toAdd, toSkip, toKeep };
104
+ }
105
+
106
+ module.exports = {
107
+ hashFile,
108
+ collectFiles,
109
+ diffFiles
110
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Manifest Manager
3
+ *
4
+ * Tracks installed BYAN files and their hashes in _byan/.manifest.json.
5
+ * Used by the update system to detect user modifications.
6
+ *
7
+ * @module utils/manifest
8
+ */
9
+
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const { hashFile, collectFiles } = require('./file-differ');
13
+
14
+ const MANIFEST_FILENAME = '.manifest.json';
15
+
16
+ /**
17
+ * @typedef {Object} FileEntry
18
+ * @property {string} hash - SHA-256 hex hash at install time
19
+ * @property {boolean} userModified - Whether user changed the file after install
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} Manifest
24
+ * @property {string} version - BYAN version when manifest was created
25
+ * @property {string} createdAt - ISO timestamp
26
+ * @property {string} updatedAt - ISO timestamp
27
+ * @property {Object<string, FileEntry>} files - Map of relative path to file entry
28
+ */
29
+
30
+ /**
31
+ * Read the manifest file from a project.
32
+ *
33
+ * @param {string} projectRoot - Project root directory
34
+ * @returns {Promise<Manifest|null>} Manifest object or null if not found
35
+ */
36
+ async function readManifest(projectRoot) {
37
+ const manifestPath = path.join(projectRoot, '_byan', MANIFEST_FILENAME);
38
+
39
+ if (!await fs.pathExists(manifestPath)) {
40
+ return null;
41
+ }
42
+
43
+ return fs.readJSON(manifestPath);
44
+ }
45
+
46
+ /**
47
+ * Write manifest to _byan/.manifest.json.
48
+ *
49
+ * @param {string} projectRoot - Project root directory
50
+ * @param {Manifest} manifest - Manifest object to write
51
+ * @returns {Promise<void>}
52
+ */
53
+ async function writeManifest(projectRoot, manifest) {
54
+ const manifestPath = path.join(projectRoot, '_byan', MANIFEST_FILENAME);
55
+ manifest.updatedAt = new Date().toISOString();
56
+ await fs.writeJSON(manifestPath, manifest, { spaces: 2 });
57
+ }
58
+
59
+ /**
60
+ * Generate a fresh manifest by scanning the _byan/ directory.
61
+ *
62
+ * @param {string} byanDir - Absolute path to _byan/ directory
63
+ * @param {string} version - BYAN version string
64
+ * @returns {Promise<Manifest>}
65
+ */
66
+ async function generateManifest(byanDir, version) {
67
+ const files = await collectFiles(byanDir);
68
+ const fileEntries = {};
69
+ const now = new Date().toISOString();
70
+
71
+ for (const relPath of files) {
72
+ if (relPath === MANIFEST_FILENAME) continue;
73
+ const hash = await hashFile(path.join(byanDir, relPath));
74
+ fileEntries[relPath] = { hash, userModified: false };
75
+ }
76
+
77
+ return {
78
+ version,
79
+ createdAt: now,
80
+ updatedAt: now,
81
+ files: fileEntries
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Detect which files the user has modified since installation.
87
+ * Compares current file hashes against those recorded in the manifest.
88
+ *
89
+ * @param {string} projectRoot - Project root directory
90
+ * @returns {Promise<string[]>} Array of relative paths that were modified by the user
91
+ */
92
+ async function detectUserModifications(projectRoot) {
93
+ const manifest = await readManifest(projectRoot);
94
+ if (!manifest) return [];
95
+
96
+ const byanDir = path.join(projectRoot, '_byan');
97
+ const modified = [];
98
+
99
+ for (const [relPath, entry] of Object.entries(manifest.files)) {
100
+ const fullPath = path.join(byanDir, relPath);
101
+ if (!await fs.pathExists(fullPath)) continue;
102
+
103
+ const currentHash = await hashFile(fullPath);
104
+ if (currentHash !== entry.hash) {
105
+ modified.push(relPath);
106
+ }
107
+ }
108
+
109
+ return modified;
110
+ }
111
+
112
+ module.exports = {
113
+ readManifest,
114
+ writeManifest,
115
+ generateManifest,
116
+ detectUserModifications,
117
+ MANIFEST_FILENAME
118
+ };