create-byan-agent 2.7.9 → 2.8.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/bin/create-byan-agent-v2.js +135 -184
- package/lib/stt/engine.js +277 -0
- package/lib/stt/parakeet-backend.js +262 -0
- package/lib/stt/whisper-backend.js +171 -0
- package/lib/utils/file-differ.js +110 -0
- package/lib/utils/manifest.js +118 -0
- package/lib/utils/version-compare.js +69 -0
- package/lib/yanstaller/backuper.js +101 -45
- package/lib/yanstaller/index.js +41 -4
- package/lib/yanstaller/updater.js +271 -0
- package/package.json +5 -2
- package/setup-parakeet.js +260 -0
- package/src/webui/api.js +293 -0
- package/src/webui/public/app.js +455 -0
- package/src/webui/public/index.html +192 -0
- package/src/webui/public/style.css +732 -0
- package/src/webui/server.js +215 -0
|
@@ -1695,230 +1695,181 @@ program
|
|
|
1695
1695
|
.version(BYAN_VERSION)
|
|
1696
1696
|
.action(install);
|
|
1697
1697
|
|
|
1698
|
-
// Update Command
|
|
1698
|
+
// Update Command (Yanstaller v3)
|
|
1699
1699
|
program
|
|
1700
1700
|
.command('update')
|
|
1701
|
-
.description('
|
|
1702
|
-
.option('--
|
|
1703
|
-
.option('--force', '
|
|
1701
|
+
.description('Update BYAN to the latest npm version')
|
|
1702
|
+
.option('--preview', 'Show what would change without applying')
|
|
1703
|
+
.option('--force', 'Force update even if already up to date')
|
|
1704
1704
|
.action(async (options) => {
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
const Analyzer = require('../../update-byan-agent/lib/analyzer');
|
|
1709
|
-
const Backup = require('../../update-byan-agent/lib/backup');
|
|
1710
|
-
const CustomizationDetector = require('../../update-byan-agent/lib/customization-detector');
|
|
1711
|
-
|
|
1705
|
+
const yanstaller = require('../lib/yanstaller');
|
|
1706
|
+
const projectRoot = process.cwd();
|
|
1707
|
+
|
|
1712
1708
|
try {
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
console.log(
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
// Step 2: Confirm update
|
|
1732
|
-
const { confirmUpdate } = await inquirer.prompt([{
|
|
1733
|
-
type: 'confirm',
|
|
1734
|
-
name: 'confirmUpdate',
|
|
1735
|
-
message: `Mettre a jour BYAN ${versionInfo.current} -> ${versionInfo.latest}?`,
|
|
1736
|
-
default: true
|
|
1737
|
-
}]);
|
|
1738
|
-
|
|
1739
|
-
if (!confirmUpdate) {
|
|
1740
|
-
console.log(chalk.yellow('Mise a jour annulee'));
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// Step 3: Detect customizations
|
|
1745
|
-
const detectorSpinner = ora('Detection des personnalisations...').start();
|
|
1746
|
-
const detector = new CustomizationDetector(installPath);
|
|
1747
|
-
const customizations = await detector.detectCustomizations();
|
|
1748
|
-
detectorSpinner.succeed(`${customizations.length} fichiers a preserver detectes`);
|
|
1749
|
-
|
|
1750
|
-
// Step 4: Create backup
|
|
1751
|
-
const backupSpinner = ora('Creation backup...').start();
|
|
1752
|
-
const backup = new Backup(installPath);
|
|
1753
|
-
const backupPath = await backup.create();
|
|
1754
|
-
backupSpinner.succeed(`Backup cree: ${path.basename(backupPath)}`);
|
|
1755
|
-
|
|
1756
|
-
// Step 5: Preserve customizations
|
|
1757
|
-
const preserveSpinner = ora('Sauvegarde des personnalisations...').start();
|
|
1758
|
-
const tempDir = path.join(installPath, '.byan-update-temp');
|
|
1759
|
-
if (fs.existsSync(tempDir)) {
|
|
1760
|
-
await fs.remove(tempDir);
|
|
1761
|
-
}
|
|
1762
|
-
await fs.ensureDir(tempDir);
|
|
1763
|
-
|
|
1764
|
-
for (const custom of customizations) {
|
|
1765
|
-
if (await fs.pathExists(custom.path)) {
|
|
1766
|
-
const relativePath = path.relative(installPath, custom.path);
|
|
1767
|
-
const tempPath = path.join(tempDir, relativePath);
|
|
1768
|
-
const tempParent = path.dirname(tempPath);
|
|
1769
|
-
|
|
1770
|
-
await fs.ensureDir(tempParent);
|
|
1771
|
-
await fs.copy(custom.path, tempPath);
|
|
1709
|
+
if (options.preview) {
|
|
1710
|
+
const spinner = ora('Analyzing update...').start();
|
|
1711
|
+
const { diff, userModified, installed, latest } = await yanstaller.updater.preview(projectRoot);
|
|
1712
|
+
spinner.succeed('Analysis complete');
|
|
1713
|
+
|
|
1714
|
+
console.log('');
|
|
1715
|
+
console.log(chalk.bold(`Version: ${installed} -> ${latest}`));
|
|
1716
|
+
console.log('');
|
|
1717
|
+
|
|
1718
|
+
if (diff.toUpdate.length) {
|
|
1719
|
+
console.log(chalk.yellow(` Updated files (${diff.toUpdate.length}):`));
|
|
1720
|
+
diff.toUpdate.forEach(f => {
|
|
1721
|
+
const tag = userModified.includes(f) ? chalk.red(' [user-modified, will skip]') : '';
|
|
1722
|
+
console.log(chalk.gray(` ~ ${f}${tag}`));
|
|
1723
|
+
});
|
|
1772
1724
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
// Step 6: Download and install latest version
|
|
1777
|
-
const updateSpinner = ora('Telechargement derniere version...').start();
|
|
1778
|
-
try {
|
|
1779
|
-
// Remove current _byan directory
|
|
1780
|
-
const byanDir = path.join(installPath, '_byan');
|
|
1781
|
-
if (await fs.pathExists(byanDir)) {
|
|
1782
|
-
await fs.remove(byanDir);
|
|
1725
|
+
if (diff.toAdd.length) {
|
|
1726
|
+
console.log(chalk.green(` New files (${diff.toAdd.length}):`));
|
|
1727
|
+
diff.toAdd.forEach(f => console.log(chalk.gray(` + ${f}`)));
|
|
1783
1728
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
cwd: installPath,
|
|
1788
|
-
stdio: 'pipe'
|
|
1789
|
-
});
|
|
1790
|
-
|
|
1791
|
-
// Copy _byan from node_modules to project root
|
|
1792
|
-
const nodeModulesByan = path.join(installPath, 'node_modules', 'create-byan-agent', '_byan');
|
|
1793
|
-
if (await fs.pathExists(nodeModulesByan)) {
|
|
1794
|
-
await fs.copy(nodeModulesByan, byanDir);
|
|
1795
|
-
} else {
|
|
1796
|
-
throw new Error('_byan directory not found in npm package');
|
|
1729
|
+
if (diff.toKeep.length) {
|
|
1730
|
+
console.log(chalk.blue(` User files kept (${diff.toKeep.length}):`));
|
|
1731
|
+
diff.toKeep.forEach(f => console.log(chalk.gray(` = ${f}`)));
|
|
1797
1732
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
} catch (error) {
|
|
1801
|
-
updateSpinner.fail('Erreur installation');
|
|
1802
|
-
|
|
1803
|
-
// Rollback
|
|
1804
|
-
const rollbackSpinner = ora('Restauration backup...').start();
|
|
1805
|
-
await backup.restore(backupPath);
|
|
1806
|
-
rollbackSpinner.succeed('Backup restaure');
|
|
1807
|
-
|
|
1808
|
-
throw error;
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
// Step 7: Restore customizations
|
|
1812
|
-
const restoreSpinner = ora('Restauration personnalisations...').start();
|
|
1813
|
-
for (const custom of customizations) {
|
|
1814
|
-
const relativePath = path.relative(installPath, custom.path);
|
|
1815
|
-
const tempPath = path.join(tempDir, relativePath);
|
|
1816
|
-
|
|
1817
|
-
if (await fs.pathExists(tempPath)) {
|
|
1818
|
-
const targetParent = path.dirname(custom.path);
|
|
1819
|
-
await fs.ensureDir(targetParent);
|
|
1820
|
-
await fs.copy(tempPath, custom.path);
|
|
1733
|
+
if (diff.toSkip.length) {
|
|
1734
|
+
console.log(chalk.gray(` Unchanged: ${diff.toSkip.length} files`));
|
|
1821
1735
|
}
|
|
1736
|
+
console.log('');
|
|
1737
|
+
return;
|
|
1822
1738
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
// Update version in config.yaml
|
|
1831
|
-
const configPath = path.join(installPath, '_byan', 'bmb', 'config.yaml');
|
|
1832
|
-
if (await fs.pathExists(configPath)) {
|
|
1833
|
-
const configContent = await fs.readFile(configPath, 'utf8');
|
|
1834
|
-
const config = yaml.load(configContent);
|
|
1835
|
-
config.byan_version = versionInfo.latest;
|
|
1836
|
-
await fs.writeFile(configPath, yaml.dump(config), 'utf8');
|
|
1739
|
+
|
|
1740
|
+
const checkSpinner = ora('Checking for updates...').start();
|
|
1741
|
+
const check = await yanstaller.updater.checkForUpdate(projectRoot);
|
|
1742
|
+
|
|
1743
|
+
if (!check.updateAvailable && !options.force) {
|
|
1744
|
+
checkSpinner.succeed(`Already up to date (${check.installed})`);
|
|
1745
|
+
return;
|
|
1837
1746
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1747
|
+
checkSpinner.succeed(`Update available: ${check.installed} -> ${check.latest}`);
|
|
1748
|
+
|
|
1749
|
+
const updateSpinner = ora('Updating BYAN...').start();
|
|
1750
|
+
const result = await yanstaller.update(projectRoot, { force: options.force });
|
|
1751
|
+
updateSpinner.succeed('Update complete');
|
|
1752
|
+
|
|
1842
1753
|
console.log('');
|
|
1843
|
-
console.log(chalk.
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1754
|
+
console.log(chalk.green.bold(`Updated: ${result.previousVersion} -> ${result.newVersion}`));
|
|
1755
|
+
console.log(chalk.gray(` Files updated: ${result.filesUpdated}`));
|
|
1756
|
+
console.log(chalk.gray(` Files added: ${result.filesAdded}`));
|
|
1757
|
+
if (result.filesSkipped > 0) {
|
|
1758
|
+
console.log(chalk.yellow(` Files skipped: ${result.filesSkipped} (user-modified)`));
|
|
1759
|
+
}
|
|
1760
|
+
if (result.backupPath) {
|
|
1761
|
+
console.log(chalk.gray(` Backup: ${path.basename(result.backupPath)}`));
|
|
1762
|
+
}
|
|
1847
1763
|
console.log('');
|
|
1848
|
-
|
|
1849
1764
|
} catch (error) {
|
|
1850
1765
|
console.error('');
|
|
1851
|
-
console.error(chalk.red.bold('
|
|
1766
|
+
console.error(chalk.red.bold('Update failed:'));
|
|
1852
1767
|
console.error(chalk.red(` ${error.message}`));
|
|
1853
1768
|
console.error('');
|
|
1854
|
-
console.error(chalk.yellow('
|
|
1855
|
-
console.error(chalk.gray('Restaurer avec: npx create-byan-agent restore'));
|
|
1769
|
+
console.error(chalk.yellow('Restore with: npx create-byan-agent rollback'));
|
|
1856
1770
|
process.exit(1);
|
|
1857
1771
|
}
|
|
1858
1772
|
});
|
|
1859
1773
|
|
|
1860
|
-
//
|
|
1774
|
+
// Rollback Command (Yanstaller v3)
|
|
1861
1775
|
program
|
|
1862
|
-
.command('
|
|
1863
|
-
.description('
|
|
1864
|
-
.
|
|
1865
|
-
|
|
1866
|
-
const
|
|
1867
|
-
const spinner = ora('
|
|
1868
|
-
|
|
1776
|
+
.command('rollback')
|
|
1777
|
+
.description('Restore BYAN from the most recent backup')
|
|
1778
|
+
.action(async () => {
|
|
1779
|
+
const yanstaller = require('../lib/yanstaller');
|
|
1780
|
+
const projectRoot = process.cwd();
|
|
1781
|
+
const spinner = ora('Restoring from backup...').start();
|
|
1782
|
+
|
|
1869
1783
|
try {
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
await backup.restore(options.path);
|
|
1874
|
-
|
|
1875
|
-
spinner.succeed('Backup restaure avec succes');
|
|
1876
|
-
console.log(chalk.green('\nBYAN restaure depuis backup'));
|
|
1877
|
-
|
|
1784
|
+
await yanstaller.rollback(projectRoot);
|
|
1785
|
+
spinner.succeed('Rollback complete');
|
|
1786
|
+
console.log(chalk.green('\nBYAN restored from latest backup'));
|
|
1878
1787
|
} catch (error) {
|
|
1879
|
-
spinner.fail('
|
|
1788
|
+
spinner.fail('Rollback failed');
|
|
1880
1789
|
console.error(chalk.red(` ${error.message}`));
|
|
1881
1790
|
process.exit(1);
|
|
1882
1791
|
}
|
|
1883
1792
|
});
|
|
1884
1793
|
|
|
1885
|
-
//
|
|
1794
|
+
// Backups Command (Yanstaller v3)
|
|
1795
|
+
program
|
|
1796
|
+
.command('backups')
|
|
1797
|
+
.description('List available BYAN backups')
|
|
1798
|
+
.action(async () => {
|
|
1799
|
+
const yanstaller = require('../lib/yanstaller');
|
|
1800
|
+
const projectRoot = process.cwd();
|
|
1801
|
+
|
|
1802
|
+
try {
|
|
1803
|
+
const backups = await yanstaller.listBackups(projectRoot);
|
|
1804
|
+
|
|
1805
|
+
if (backups.length === 0) {
|
|
1806
|
+
console.log(chalk.yellow('No backups found.'));
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
console.log(chalk.bold(`\nAvailable backups (${backups.length}):\n`));
|
|
1811
|
+
for (const backupPath of backups) {
|
|
1812
|
+
const name = path.basename(backupPath);
|
|
1813
|
+
const timestamp = parseInt(name.replace('_byan.backup-', ''), 10);
|
|
1814
|
+
const date = new Date(timestamp).toLocaleString();
|
|
1815
|
+
const size = await yanstaller.backuper.getBackupSize(backupPath);
|
|
1816
|
+
const sizeKB = (size / 1024).toFixed(1);
|
|
1817
|
+
console.log(chalk.gray(` ${name} ${date} (${sizeKB} KB)`));
|
|
1818
|
+
}
|
|
1819
|
+
console.log('');
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
console.error(chalk.red(` ${error.message}`));
|
|
1822
|
+
process.exit(1);
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
// Check Version Command (Yanstaller v3)
|
|
1886
1827
|
program
|
|
1887
1828
|
.command('check')
|
|
1888
|
-
.description('
|
|
1829
|
+
.description('Check installed version vs latest npm version')
|
|
1889
1830
|
.action(async () => {
|
|
1890
|
-
const
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1831
|
+
const yanstaller = require('../lib/yanstaller');
|
|
1832
|
+
const projectRoot = process.cwd();
|
|
1833
|
+
const spinner = ora('Checking BYAN version...').start();
|
|
1834
|
+
|
|
1893
1835
|
try {
|
|
1894
|
-
const
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
const versionInfo = await analyzer.checkVersion();
|
|
1898
|
-
|
|
1899
|
-
spinner.succeed('Verification terminee');
|
|
1900
|
-
|
|
1836
|
+
const check = await yanstaller.updater.checkForUpdate(projectRoot);
|
|
1837
|
+
spinner.succeed('Check complete');
|
|
1838
|
+
|
|
1901
1839
|
console.log('');
|
|
1902
|
-
console.log(chalk.bold('
|
|
1903
|
-
console.log(chalk.gray('
|
|
1904
|
-
console.log(chalk.gray('
|
|
1840
|
+
console.log(chalk.bold('Version info:'));
|
|
1841
|
+
console.log(chalk.gray(' Installed: ') + chalk.cyan(check.installed));
|
|
1842
|
+
console.log(chalk.gray(' Latest: ') + chalk.cyan(check.latest));
|
|
1905
1843
|
console.log('');
|
|
1906
|
-
|
|
1907
|
-
if (
|
|
1908
|
-
console.log(chalk.
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
console.log(chalk.
|
|
1912
|
-
} else if (versionInfo.ahead) {
|
|
1913
|
-
console.log(chalk.blue(' → Version dev en avance sur npm'));
|
|
1844
|
+
|
|
1845
|
+
if (check.updateAvailable) {
|
|
1846
|
+
console.log(chalk.yellow(` Update available (${check.changes.length} files changed)`));
|
|
1847
|
+
console.log(chalk.gray(' Run: npx create-byan-agent update'));
|
|
1848
|
+
} else {
|
|
1849
|
+
console.log(chalk.green(' Up to date'));
|
|
1914
1850
|
}
|
|
1915
1851
|
console.log('');
|
|
1916
|
-
|
|
1917
1852
|
} catch (error) {
|
|
1918
|
-
spinner.fail('
|
|
1853
|
+
spinner.fail('Version check failed');
|
|
1919
1854
|
console.error(chalk.red(` ${error.message}`));
|
|
1920
1855
|
process.exit(1);
|
|
1921
1856
|
}
|
|
1922
1857
|
});
|
|
1923
1858
|
|
|
1859
|
+
program
|
|
1860
|
+
.command('web')
|
|
1861
|
+
.description('Launch BYAN WebUI installer in the browser')
|
|
1862
|
+
.option('-p, --port <port>', 'Port number', '3000')
|
|
1863
|
+
.action(async (options) => {
|
|
1864
|
+
const ByanWebUI = require('../src/webui/server');
|
|
1865
|
+
const port = parseInt(options.port, 10);
|
|
1866
|
+
const projectRoot = process.cwd();
|
|
1867
|
+
|
|
1868
|
+
console.log(chalk.cyan.bold('\n BYAN WebUI\n'));
|
|
1869
|
+
const server = new ByanWebUI({ port, projectRoot });
|
|
1870
|
+
server.start();
|
|
1871
|
+
console.log(chalk.green(` Server running at http://localhost:${port}`));
|
|
1872
|
+
console.log(chalk.gray(' Press Ctrl+C to stop\n'));
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1924
1875
|
program.parse(process.argv);
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STT Engine Abstraction Layer
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects the best available speech-to-text engine.
|
|
5
|
+
* Priority: Parakeet TDT > Whisper > none.
|
|
6
|
+
*
|
|
7
|
+
* @module stt/engine
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
|
|
13
|
+
const PARAKEET_MIN_VRAM = 4000; // 4 GB
|
|
14
|
+
const WHISPER_MIN_VRAM = 1000; // 1 GB for GPU mode
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detect GPU presence and capabilities via nvidia-smi.
|
|
18
|
+
* Shared across all STT backends to avoid duplication.
|
|
19
|
+
*
|
|
20
|
+
* @returns {{ hasGPU: boolean, vram: number, gpuName: string }}
|
|
21
|
+
*/
|
|
22
|
+
function detectGPU() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = execSync(
|
|
25
|
+
'nvidia-smi --query-gpu=name,memory.total --format=csv,noheader',
|
|
26
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
27
|
+
).trim();
|
|
28
|
+
|
|
29
|
+
if (!raw || raw === 'NO_GPU') {
|
|
30
|
+
return { hasGPU: false, vram: 0, gpuName: '' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [gpuName, vramStr] = raw.split(',').map(s => s.trim());
|
|
34
|
+
const vram = parseInt(vramStr, 10) || 0;
|
|
35
|
+
|
|
36
|
+
return { hasGPU: true, vram, gpuName };
|
|
37
|
+
} catch {
|
|
38
|
+
return { hasGPU: false, vram: 0, gpuName: '' };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check whether a shell command is available on PATH.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} cmd
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
function commandExists(cmd) {
|
|
49
|
+
try {
|
|
50
|
+
execSync(`which ${cmd}`, { stdio: 'pipe' });
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check Python availability and optional package presence.
|
|
59
|
+
*
|
|
60
|
+
* @returns {{ available: boolean, version: string, hasNemo: boolean }}
|
|
61
|
+
*/
|
|
62
|
+
function checkPython() {
|
|
63
|
+
const available = commandExists('python3');
|
|
64
|
+
if (!available) {
|
|
65
|
+
return { available: false, version: '', hasNemo: false };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let version = '';
|
|
69
|
+
try {
|
|
70
|
+
version = execSync('python3 --version', {
|
|
71
|
+
encoding: 'utf-8',
|
|
72
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
73
|
+
}).trim().replace('Python ', '');
|
|
74
|
+
} catch { /* ignore */ }
|
|
75
|
+
|
|
76
|
+
let hasNemo = false;
|
|
77
|
+
try {
|
|
78
|
+
execSync('python3 -c "import nemo.collections.asr"', {
|
|
79
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
80
|
+
});
|
|
81
|
+
hasNemo = true;
|
|
82
|
+
} catch { /* ignore */ }
|
|
83
|
+
|
|
84
|
+
return { available, version, hasNemo };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Detect the best available STT engine based on hardware and software.
|
|
89
|
+
*
|
|
90
|
+
* Decision logic:
|
|
91
|
+
* 1. GPU with >= 4 GB VRAM + (Python+NeMo or Docker) -> parakeet
|
|
92
|
+
* 2. GPU with >= 1 GB VRAM or Docker available -> whisper
|
|
93
|
+
* 3. Otherwise -> none
|
|
94
|
+
*
|
|
95
|
+
* @returns {{ engine: 'parakeet'|'whisper'|'none', gpu: object, reason: string }}
|
|
96
|
+
*/
|
|
97
|
+
function detect() {
|
|
98
|
+
const gpu = detectGPU();
|
|
99
|
+
const python = checkPython();
|
|
100
|
+
const hasDocker = commandExists('docker');
|
|
101
|
+
|
|
102
|
+
// Parakeet needs a capable GPU
|
|
103
|
+
if (gpu.hasGPU && gpu.vram >= PARAKEET_MIN_VRAM) {
|
|
104
|
+
const canRunLocal = python.available && python.hasNemo;
|
|
105
|
+
if (canRunLocal || hasDocker) {
|
|
106
|
+
return {
|
|
107
|
+
engine: 'parakeet',
|
|
108
|
+
gpu,
|
|
109
|
+
reason: `GPU ${gpu.gpuName} with ${gpu.vram} MB VRAM meets Parakeet requirements`
|
|
110
|
+
+ (canRunLocal ? ' (local NeMo available)' : ' (Docker available)')
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Whisper is less demanding
|
|
116
|
+
if (gpu.hasGPU && gpu.vram >= WHISPER_MIN_VRAM) {
|
|
117
|
+
return {
|
|
118
|
+
engine: 'whisper',
|
|
119
|
+
gpu,
|
|
120
|
+
reason: `GPU ${gpu.gpuName} with ${gpu.vram} MB VRAM — Whisper GPU mode`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (hasDocker) {
|
|
125
|
+
return {
|
|
126
|
+
engine: 'whisper',
|
|
127
|
+
gpu,
|
|
128
|
+
reason: 'No sufficient GPU — Whisper CPU mode via Docker'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (python.available) {
|
|
133
|
+
return {
|
|
134
|
+
engine: 'whisper',
|
|
135
|
+
gpu,
|
|
136
|
+
reason: 'No GPU/Docker — Whisper CPU mode via local Python'
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
engine: 'none',
|
|
142
|
+
gpu,
|
|
143
|
+
reason: 'No GPU, Docker, or Python found — cannot run STT'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Return configuration defaults for a given engine.
|
|
149
|
+
*
|
|
150
|
+
* @param {'parakeet'|'whisper'} engine
|
|
151
|
+
* @returns {object}
|
|
152
|
+
*/
|
|
153
|
+
function getConfig(engine) {
|
|
154
|
+
if (engine === 'parakeet') {
|
|
155
|
+
return {
|
|
156
|
+
engine: 'parakeet',
|
|
157
|
+
port: 8001,
|
|
158
|
+
model: 'nvidia/parakeet-tdt-0.6b-v2',
|
|
159
|
+
languages: ['fr', 'en'],
|
|
160
|
+
dockerImage: 'nvcr.io/nvidia/nemo:24.07',
|
|
161
|
+
minVram: PARAKEET_MIN_VRAM,
|
|
162
|
+
healthEndpoint: 'http://localhost:8001/health'
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Default: whisper
|
|
167
|
+
const gpu = detectGPU();
|
|
168
|
+
return {
|
|
169
|
+
engine: 'whisper',
|
|
170
|
+
port: 8000,
|
|
171
|
+
model: getWhisperModel(gpu.vram),
|
|
172
|
+
dockerImage: `fedirz/faster-whisper-server:latest-${gpu.hasGPU ? 'cuda' : 'cpu'}`,
|
|
173
|
+
minVram: WHISPER_MIN_VRAM,
|
|
174
|
+
healthEndpoint: 'http://localhost:8000/health'
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Map available VRAM to the best Whisper model.
|
|
180
|
+
*
|
|
181
|
+
* @param {number} vram - VRAM in MB
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
function getWhisperModel(vram) {
|
|
185
|
+
if (vram >= 10000) return 'large-v3';
|
|
186
|
+
if (vram >= 6000) return 'large-v2';
|
|
187
|
+
if (vram >= 4000) return 'medium';
|
|
188
|
+
if (vram >= 2000) return 'small';
|
|
189
|
+
if (vram >= 1000) return 'tiny';
|
|
190
|
+
return 'base';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check runtime status of either STT engine.
|
|
195
|
+
*
|
|
196
|
+
* @returns {{ engine: string, mode: 'local'|'docker'|'unknown', running: boolean, model: string }}
|
|
197
|
+
*/
|
|
198
|
+
function getStatus() {
|
|
199
|
+
const parakeetRunning = isPortOpen(8001);
|
|
200
|
+
const whisperRunning = isPortOpen(8000);
|
|
201
|
+
|
|
202
|
+
if (parakeetRunning) {
|
|
203
|
+
return { engine: 'parakeet', mode: detectMode(8001), running: true, model: 'parakeet-tdt-0.6b-v2' };
|
|
204
|
+
}
|
|
205
|
+
if (whisperRunning) {
|
|
206
|
+
return { engine: 'whisper', mode: detectMode(8000), running: true, model: 'whisper' };
|
|
207
|
+
}
|
|
208
|
+
return { engine: 'none', mode: 'unknown', running: false, model: '' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {number} port
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
function isPortOpen(port) {
|
|
216
|
+
try {
|
|
217
|
+
execSync(`curl -sf http://localhost:${port}/health`, {
|
|
218
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
219
|
+
timeout: 3000
|
|
220
|
+
});
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Heuristic: if a Docker container is listening on the port, it's Docker mode.
|
|
229
|
+
*
|
|
230
|
+
* @param {number} port
|
|
231
|
+
* @returns {'local'|'docker'|'unknown'}
|
|
232
|
+
*/
|
|
233
|
+
function detectMode(port) {
|
|
234
|
+
try {
|
|
235
|
+
const out = execSync(`docker ps --format '{{.Ports}}'`, {
|
|
236
|
+
encoding: 'utf-8',
|
|
237
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
238
|
+
});
|
|
239
|
+
if (out.includes(`:${port}->`)) return 'docker';
|
|
240
|
+
return 'local';
|
|
241
|
+
} catch {
|
|
242
|
+
return 'unknown';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Print a summary of the detection result.
|
|
248
|
+
*
|
|
249
|
+
* @param {{ engine: string, gpu: object, reason: string }} result
|
|
250
|
+
*/
|
|
251
|
+
function printDetectionSummary(result) {
|
|
252
|
+
const { engine, gpu, reason } = result;
|
|
253
|
+
|
|
254
|
+
if (gpu.hasGPU) {
|
|
255
|
+
console.log(chalk.green(` GPU: ${gpu.gpuName} (${gpu.vram} MB VRAM)`));
|
|
256
|
+
} else {
|
|
257
|
+
console.log(chalk.yellow(' GPU: not detected'));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const color = engine === 'parakeet' ? chalk.cyan
|
|
261
|
+
: engine === 'whisper' ? chalk.blue
|
|
262
|
+
: chalk.red;
|
|
263
|
+
|
|
264
|
+
console.log(color(` Engine: ${engine}`));
|
|
265
|
+
console.log(chalk.gray(` Reason: ${reason}`));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
detect,
|
|
270
|
+
detectGPU,
|
|
271
|
+
commandExists,
|
|
272
|
+
checkPython,
|
|
273
|
+
getConfig,
|
|
274
|
+
getStatus,
|
|
275
|
+
getWhisperModel,
|
|
276
|
+
printDetectionSummary
|
|
277
|
+
};
|