loukai-app 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +558 -0
- package/bin/loukai.js +32 -0
- package/package.json +243 -0
- package/src/main/appState.js +250 -0
- package/src/main/audioEngine.js +478 -0
- package/src/main/creator/conversionService.js +503 -0
- package/src/main/creator/downloadManager.js +1128 -0
- package/src/main/creator/ffmpegService.js +487 -0
- package/src/main/creator/installLogger.js +51 -0
- package/src/main/creator/keyDetection.js +212 -0
- package/src/main/creator/llmService.js +370 -0
- package/src/main/creator/lrclibService.js +340 -0
- package/src/main/creator/python/crepe_runner.py +189 -0
- package/src/main/creator/python/demucs_runner.py +158 -0
- package/src/main/creator/python/whisper_runner.py +172 -0
- package/src/main/creator/pythonRunner.js +268 -0
- package/src/main/creator/stemBuilder.js +491 -0
- package/src/main/creator/systemChecker.js +474 -0
- package/src/main/handlers/appHandlers.js +45 -0
- package/src/main/handlers/audioHandlers.js +33 -0
- package/src/main/handlers/autotuneHandlers.js +28 -0
- package/src/main/handlers/canvasHandlers.js +84 -0
- package/src/main/handlers/creatorHandlers.js +159 -0
- package/src/main/handlers/editorHandlers.js +98 -0
- package/src/main/handlers/effectsHandlers.js +100 -0
- package/src/main/handlers/fileHandlers.js +45 -0
- package/src/main/handlers/index.js +78 -0
- package/src/main/handlers/libraryHandlers.js +96 -0
- package/src/main/handlers/mixerHandlers.js +64 -0
- package/src/main/handlers/playerHandlers.js +39 -0
- package/src/main/handlers/preferencesHandlers.js +46 -0
- package/src/main/handlers/queueHandlers.js +81 -0
- package/src/main/handlers/rendererHandlers.js +63 -0
- package/src/main/handlers/settingsHandlers.js +42 -0
- package/src/main/handlers/webServerHandlers.js +105 -0
- package/src/main/main.js +2351 -0
- package/src/main/preload.js +252 -0
- package/src/main/settingsManager.js +139 -0
- package/src/main/statePersistence.js +193 -0
- package/src/main/utils/pathValidator.js +112 -0
- package/src/main/webServer.js +2535 -0
- package/src/native/autotune.js +417 -0
- package/src/renderer/adapters/ElectronBridge.js +677 -0
- package/src/renderer/canvas.html +80 -0
- package/src/renderer/components/App.jsx +303 -0
- package/src/renderer/components/AppRoot.jsx +37 -0
- package/src/renderer/components/AudioDeviceSettings.jsx +145 -0
- package/src/renderer/components/EffectsPanelWrapper.jsx +267 -0
- package/src/renderer/components/MixerTab.jsx +233 -0
- package/src/renderer/components/MixerTabWrapper.jsx +31 -0
- package/src/renderer/components/PortalSelect.jsx +239 -0
- package/src/renderer/components/QueueTab.jsx +116 -0
- package/src/renderer/components/RequestsListWrapper.jsx +78 -0
- package/src/renderer/components/ServerTab.jsx +472 -0
- package/src/renderer/components/SongInfoBarWrapper.jsx +77 -0
- package/src/renderer/components/StatusBar.jsx +92 -0
- package/src/renderer/components/TabNavigation.jsx +77 -0
- package/src/renderer/components/TransportControlsWrapper.jsx +69 -0
- package/src/renderer/components/creator/CreateTab.jsx +1236 -0
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js +2 -0
- package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +1 -0
- package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js +2 -0
- package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js.map +1 -0
- package/src/renderer/dist/assets/player-DVrqp7N5.js +3 -0
- package/src/renderer/dist/assets/player-DVrqp7N5.js.map +1 -0
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +2 -0
- package/src/renderer/dist/assets/songLoaders-BaTgGib4.js.map +1 -0
- package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js +2 -0
- package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js.map +1 -0
- package/src/renderer/dist/js/autoTuneWorklet.js +224 -0
- package/src/renderer/dist/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/dist/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/dist/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/dist/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/dist/renderer.css +1 -0
- package/src/renderer/dist/renderer.js +62 -0
- package/src/renderer/dist/renderer.js.map +1 -0
- package/src/renderer/dist/renderer.woff2 +0 -0
- package/src/renderer/hooks/useKeyboardShortcuts.js +154 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/index.html.backup +372 -0
- package/src/renderer/js/PlayerInterface.js +267 -0
- package/src/renderer/js/autoTuneWorklet.js +224 -0
- package/src/renderer/js/butterchurnVerify.js +46 -0
- package/src/renderer/js/canvas-app.js +114 -0
- package/src/renderer/js/cdgPlayer.js +685 -0
- package/src/renderer/js/kaiPlayer.js +1200 -0
- package/src/renderer/js/karaokeRenderer.js +3392 -0
- package/src/renderer/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/js/microphoneEngine.js +656 -0
- package/src/renderer/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/js/player.js +232 -0
- package/src/renderer/js/referencePitchTracker.js +130 -0
- package/src/renderer/js/songLoaders.js +334 -0
- package/src/renderer/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/js/webrtcManager.js +511 -0
- package/src/renderer/lib/butterchurn.min.js +6739 -0
- package/src/renderer/lib/butterchurnPresets.min.js +1 -0
- package/src/renderer/lib/cdgraphics-wrapper.js +16 -0
- package/src/renderer/lib/cdgraphics.js +299 -0
- package/src/renderer/public/js/autoTuneWorklet.js +224 -0
- package/src/renderer/public/js/micPitchDetectorWorklet.js +137 -0
- package/src/renderer/public/js/musicAnalysisWorklet.js +216 -0
- package/src/renderer/public/js/phaseVocoderWorklet.js +341 -0
- package/src/renderer/public/js/soundtouch-worklet.js +1395 -0
- package/src/renderer/react-entry.jsx +44 -0
- package/src/renderer/styles/tailwind.css +106 -0
- package/src/renderer/utils/qrCodeGenerator.js +98 -0
- package/src/renderer/vite.config.js +31 -0
- package/src/shared/adapters/BridgeInterface.js +195 -0
- package/src/shared/components/EffectsPanel.jsx +177 -0
- package/src/shared/components/LibraryPanel.jsx +701 -0
- package/src/shared/components/LineDetailCanvas.jsx +167 -0
- package/src/shared/components/LyricLine.jsx +505 -0
- package/src/shared/components/LyricRejection.jsx +84 -0
- package/src/shared/components/LyricSuggestion.jsx +80 -0
- package/src/shared/components/LyricsEditorCanvas.jsx +271 -0
- package/src/shared/components/MixerPanel.jsx +94 -0
- package/src/shared/components/PlayerControls.jsx +206 -0
- package/src/shared/components/PortalSelect.jsx +239 -0
- package/src/shared/components/QueueList.jsx +365 -0
- package/src/shared/components/QuickSearch.jsx +126 -0
- package/src/shared/components/RequestsList.jsx +121 -0
- package/src/shared/components/SongEditor.jsx +1362 -0
- package/src/shared/components/SongInfoBar.jsx +81 -0
- package/src/shared/components/ThemeToggle.jsx +106 -0
- package/src/shared/components/Toast.jsx +30 -0
- package/src/shared/components/VisualizationSettings.jsx +243 -0
- package/src/shared/constants.js +95 -0
- package/src/shared/context/BridgeContext.jsx +32 -0
- package/src/shared/contexts/AudioContext.jsx +37 -0
- package/src/shared/contexts/PlayerContext.jsx +66 -0
- package/src/shared/contexts/SettingsContext.jsx +50 -0
- package/src/shared/defaults.js +158 -0
- package/src/shared/formatUtils.js +59 -0
- package/src/shared/formatUtils.test.js +207 -0
- package/src/shared/hooks/useAppState.js +97 -0
- package/src/shared/hooks/useAudioEngine.js +264 -0
- package/src/shared/hooks/usePlayer.js +89 -0
- package/src/shared/hooks/useSettingsPersistence.js +74 -0
- package/src/shared/hooks/useWebRTC.js +118 -0
- package/src/shared/ipcContracts.js +299 -0
- package/src/shared/package.json +3 -0
- package/src/shared/services/creatorService.js +373 -0
- package/src/shared/services/creatorService.test.js +413 -0
- package/src/shared/services/editorService.js +213 -0
- package/src/shared/services/editorService.test.js +219 -0
- package/src/shared/services/effectsService.js +271 -0
- package/src/shared/services/effectsService.test.js +418 -0
- package/src/shared/services/libraryService.js +438 -0
- package/src/shared/services/libraryService.test.js +474 -0
- package/src/shared/services/mixerService.js +172 -0
- package/src/shared/services/mixerService.test.js +399 -0
- package/src/shared/services/playerService.js +221 -0
- package/src/shared/services/playerService.test.js +357 -0
- package/src/shared/services/preferencesService.js +219 -0
- package/src/shared/services/queueService.js +226 -0
- package/src/shared/services/queueService.test.js +430 -0
- package/src/shared/services/requestsService.js +155 -0
- package/src/shared/services/requestsService.test.js +362 -0
- package/src/shared/services/serverSettingsService.js +151 -0
- package/src/shared/services/settingsService.js +257 -0
- package/src/shared/services/settingsService.test.js +295 -0
- package/src/shared/state/StateManager.js +263 -0
- package/src/shared/utils/audio.js +42 -0
- package/src/shared/utils/format.js +32 -0
- package/src/shared/utils/lyricsUtils.js +162 -0
- package/src/test/setup.js +40 -0
- package/src/utils/cdgLoader.js +180 -0
- package/src/utils/m4aLoader.js +333 -0
- package/src/web/App.jsx +578 -0
- package/src/web/adapters/WebBridge.js +428 -0
- package/src/web/components/PlayerSettingsPanel.jsx +231 -0
- package/src/web/components/SongSearch.jsx +180 -0
- package/src/web/dist/assets/index-0H-RnRrV.js +51 -0
- package/src/web/dist/assets/index-0H-RnRrV.js.map +1 -0
- package/src/web/dist/assets/index-DYW2zB0u.css +1 -0
- package/src/web/dist/index.html +15 -0
- package/src/web/index.html +14 -0
- package/src/web/main.jsx +10 -0
- package/src/web/package-lock.json +1765 -0
- package/src/web/pages/SongRequestPage.jsx +619 -0
- package/src/web/styles/tailwind.css +68 -0
- package/src/web/vite.config.js +27 -0
- package/static/fonts/material-icons.woff2 +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Potion of Spirits.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Songflower _Moss Posy_.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi - Storm of the Eye _Thunder_ - mash0000 - quasi pseudo meta concentrics.png +0 -0
- package/static/images/butterchurn-screenshots/Aderrasi _ Geiss - Airhandler _Kali Mix_ - Canvas Mix.png +0 -0
- package/static/images/butterchurn-screenshots/An AdamFX n Martin Infusion 2 flexi - Why The Sky Looks Diffrent Today - AdamFx n Martin Infusion - Tack Tile Disfunction B.png +0 -0
- package/static/images/butterchurn-screenshots/Cope - The Neverending Explosion of Red Liquid Fire.png +0 -0
- proton lights __Krash_s beat code_ _Phat_remix02b.png +0 -0
- package/static/images/butterchurn-screenshots/Eo_S_ _ Phat - cubetrace - v2.png +0 -0
- package/static/images/butterchurn-screenshots/Eo_S_ _ Zylot - skylight _Stained Glass Majesty mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - alien fish pond.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - area 51.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - infused with the spiral.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - mindblob _shiny mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - mindblob mix.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - predator-prey-spirals.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - smashing fractals _acid etching mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi - truly soft piece of software - this is generic texturing _Jelly_ .png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ Martin - astral projection.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ Martin - cascading decay swing.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ amandio c - piercing 05 - Kopie _2_ - Kopie.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi _ stahlregen - jelly showoff parade.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi_ fishbrain_ Geiss _ Martin - tokamak witchery.png +0 -0
- package/static/images/butterchurn-screenshots/Flexi_ martin _ geiss - dedicated to the sherwin maxawow.png +0 -0
- package/static/images/butterchurn-screenshots/Fumbling_Foo _ Flexi_ Martin_ Orb_ Unchained - Star Nova v7b.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Cauldron - painterly 2 _saturation remix_.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Reaction Diffusion 2.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Spiral Artifact.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss - Thumb Drum.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss _ Flexi _ Martin - disconnected.png +0 -0
- package/static/images/butterchurn-screenshots/Geiss_ Flexi _ Stahlregen - Thumbdrum Tokamak _crossfiring aftermath jelly mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/Goody - The Wild Vort.png +0 -0
- package/static/images/butterchurn-screenshots/Idiot - Star Of Annon.png +0 -0
- package/static/images/butterchurn-screenshots/Krash _ Illusion - Spiral Movement.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - QBikal - Surface Turbulence IIb.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - acid wiring.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - charisma.png +0 -0
- package/static/images/butterchurn-screenshots/Martin - liquid arrows.png +0 -0
- package/static/images/butterchurn-screenshots/Milk Artist At our Best - FED - SlowFast Ft AdamFX n Martin - HD CosmoFX.png +0 -0
- package/static/images/butterchurn-screenshots/ORB - Waaa.png +0 -0
- package/static/images/butterchurn-screenshots/Phat_fiShbRaiN_Eo_S_Mandala_Chasers_remix.png +0 -0
- package/static/images/butterchurn-screenshots/Rovastar - Oozing Resistance.png +0 -0
- package/static/images/butterchurn-screenshots/Rovastar _ Loadus _ Geiss - FractalDrop _Triple Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/TonyMilkdrop - Leonardo Da Vinci_s Balloon _Flexi - merry-go-round _ techstyle_.png +0 -0
- package/static/images/butterchurn-screenshots/TonyMilkdrop - Magellan_s Nebula _Flexi - you enter first _ multiverse_.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained - Rewop.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained - Unified Drag 2.png +0 -0
- package/static/images/butterchurn-screenshots/Unchained _ Rovastar - Wormhole Pillars _Hall of Shadows mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - Paint Spill _Music Reactive Paint Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - Star Ornament.png +0 -0
- package/static/images/butterchurn-screenshots/Zylot - True Visionary _Final Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz _Geiss color mix_.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - Artifact 01.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - Desert Rose 2.png +0 -0
- package/static/images/butterchurn-screenshots/_Geiss - untitled.png +0 -0
- package/static/images/butterchurn-screenshots/_Mig_049.png +0 -0
- package/static/images/butterchurn-screenshots/_Mig_085.png +0 -0
- package/static/images/butterchurn-screenshots/_Rovastar _ Geiss - Hurricane Nightmare _Posterize Mix_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _197_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _220_.png +0 -0
- package/static/images/butterchurn-screenshots/___ Royal - Mashup _431_.png +0 -0
- package/static/images/butterchurn-screenshots/cope _ martin - mother-of-pearl.png +0 -0
- package/static/images/butterchurn-screenshots/fiShbRaiN _ Flexi - witchcraft 2_0.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - bouncing balls _double mindblob neon mix_.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - mom_ why the sky looks different today.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - patternton_ district of media_ capitol of the united abstractions of fractopia.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - swing out on the spiral.png +0 -0
- package/static/images/butterchurn-screenshots/flexi - what is the matrix.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ amandio c - organic _random mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ amandio c - organic12-3d-2_milk.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ fishbrain - neon mindblob grafitti.png +0 -0
- package/static/images/butterchurn-screenshots/flexi _ geiss - pogo cubes vs_ tokamak vs_ game of life _stahls jelly 4_5 finish_.png +0 -0
- package/static/images/butterchurn-screenshots/high-altitude basket unraveling - singh grooves nitrogen argon nz_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - The Bridge of Khazad-Dum.png +0 -0
- package/static/images/butterchurn-screenshots/martin - angel flight.png +0 -0
- package/static/images/butterchurn-screenshots/martin - another kind of groove.png +0 -0
- package/static/images/butterchurn-screenshots/martin - bombyx mori.png +0 -0
- package/static/images/butterchurn-screenshots/martin - castle in the air.png +0 -0
- package/static/images/butterchurn-screenshots/martin - chain breaker.png +0 -0
- package/static/images/butterchurn-screenshots/martin - disco mix 4.png +0 -0
- package/static/images/butterchurn-screenshots/martin - extreme heat.png +0 -0
- package/static/images/butterchurn-screenshots/martin - frosty caves 2.png +0 -0
- package/static/images/butterchurn-screenshots/martin - fruit machine.png +0 -0
- package/static/images/butterchurn-screenshots/martin - ghost city.png +0 -0
- package/static/images/butterchurn-screenshots/martin - glass corridor.png +0 -0
- package/static/images/butterchurn-screenshots/martin - infinity _2010 update_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - mandelbox explorer - high speed demo version.png +0 -0
- package/static/images/butterchurn-screenshots/martin - mucus cervix.png +0 -0
- package/static/images/butterchurn-screenshots/martin - reflections on black tiles.png +0 -0
- package/static/images/butterchurn-screenshots/martin - stormy sea _2010 update_.png +0 -0
- package/static/images/butterchurn-screenshots/martin - witchcraft reloaded.png +0 -0
- package/static/images/butterchurn-screenshots/martin _ flexi - diamond cutter _prismaticvortex_com_ - camille - i wish i wish i wish i was constrained.png +0 -0
- package/static/images/butterchurn-screenshots/martin _shadow harlequins shape code_ - fata morgana.png +0 -0
- package/static/images/butterchurn-screenshots/martin_ flexi_ fishbrain _ sto - enterstate _random mashup_.png +0 -0
- package/static/images/butterchurn-screenshots/sawtooth grin roam.png +0 -0
- package/static/images/butterchurn-screenshots/shifter - dark tides bdrv mix 2.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - Rovastar - Sunflower Passion _Enlightment Mix__Phat_edit _ flexi und martin shaders - circumflex in character classes in regular expression.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - heretical crosscut playpen.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - uninitialized variabowl _hydroponic chronic_.png +0 -0
- package/static/images/butterchurn-screenshots/suksma - vector exp 1 - couldn_t not.png +0 -0
- package/static/images/butterchurn-screenshots/yin - 191 - Temporal singularities.png +0 -0
- package/static/images/logo-512.png +0 -0
- package/static/images/logo.png +0 -0
- package/static/loukai-logo.png +0 -0
- package/static/screenshot-generator.html +610 -0
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download Manager - Handles downloading and installing AI components for Creator
|
|
3
|
+
*
|
|
4
|
+
* Components:
|
|
5
|
+
* - Python (standalone build from python-build-standalone)
|
|
6
|
+
* - PyTorch (with MPS/CUDA/CPU support)
|
|
7
|
+
* - Demucs (stem separation)
|
|
8
|
+
* - Whisper (transcription)
|
|
9
|
+
* - torchcrepe (pitch detection)
|
|
10
|
+
* - FFmpeg (audio processing)
|
|
11
|
+
* - Models (Whisper large-v3-turbo, Demucs htdemucs_ft)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import https from 'https';
|
|
15
|
+
import http from 'http';
|
|
16
|
+
import { createWriteStream, existsSync, mkdirSync, rmSync, chmodSync } from 'fs';
|
|
17
|
+
import { join, dirname } from 'path';
|
|
18
|
+
import { execSync, spawn } from 'child_process';
|
|
19
|
+
import { getCacheDir, getPythonPath, getPythonEnv } from './systemChecker.js';
|
|
20
|
+
import yauzl from 'yauzl';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract a zip file using yauzl (cross-platform)
|
|
24
|
+
*/
|
|
25
|
+
function extractZip(zipPath, destDir) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
|
28
|
+
if (err) {
|
|
29
|
+
reject(err);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
zipfile.readEntry();
|
|
34
|
+
|
|
35
|
+
zipfile.on('entry', (entry) => {
|
|
36
|
+
const fullPath = join(destDir, entry.fileName);
|
|
37
|
+
|
|
38
|
+
if (/\/$/.test(entry.fileName)) {
|
|
39
|
+
// Directory entry
|
|
40
|
+
mkdirSync(fullPath, { recursive: true });
|
|
41
|
+
zipfile.readEntry();
|
|
42
|
+
} else {
|
|
43
|
+
// File entry
|
|
44
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
45
|
+
zipfile.openReadStream(entry, (err, readStream) => {
|
|
46
|
+
if (err) {
|
|
47
|
+
reject(err);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const writeStream = createWriteStream(fullPath);
|
|
51
|
+
readStream.pipe(writeStream);
|
|
52
|
+
writeStream.on('close', () => {
|
|
53
|
+
zipfile.readEntry();
|
|
54
|
+
});
|
|
55
|
+
writeStream.on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
zipfile.on('end', resolve);
|
|
61
|
+
zipfile.on('error', reject);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Python standalone builds from indygreg/python-build-standalone
|
|
67
|
+
const PYTHON_BUILDS = {
|
|
68
|
+
darwin: {
|
|
69
|
+
x64: 'https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7+20241016-x86_64-apple-darwin-install_only.tar.gz',
|
|
70
|
+
arm64:
|
|
71
|
+
'https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7+20241016-aarch64-apple-darwin-install_only.tar.gz',
|
|
72
|
+
},
|
|
73
|
+
win32: {
|
|
74
|
+
x64: 'https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz',
|
|
75
|
+
},
|
|
76
|
+
linux: {
|
|
77
|
+
x64: 'https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz',
|
|
78
|
+
arm64:
|
|
79
|
+
'https://github.com/indygreg/python-build-standalone/releases/download/20241016/cpython-3.12.7+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get Python build URL for current platform
|
|
85
|
+
*/
|
|
86
|
+
function getPythonBuildUrl() {
|
|
87
|
+
const platform = process.platform;
|
|
88
|
+
const arch = process.arch;
|
|
89
|
+
|
|
90
|
+
const builds = PYTHON_BUILDS[platform];
|
|
91
|
+
if (!builds) {
|
|
92
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const url = builds[arch] || builds.x64;
|
|
96
|
+
if (!url) {
|
|
97
|
+
throw new Error(`Unsupported architecture: ${arch} on ${platform}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return url;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Download a file with progress tracking
|
|
105
|
+
*/
|
|
106
|
+
// SECURITY FIX (#29): Add max redirects parameter to prevent infinite loops
|
|
107
|
+
function downloadFile(url, destPath, onProgress = null, maxRedirects = 5) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
console.log(`š„ Downloading: ${url}`);
|
|
110
|
+
console.log(` Destination: ${destPath}`);
|
|
111
|
+
|
|
112
|
+
let protocol;
|
|
113
|
+
try {
|
|
114
|
+
protocol = url.startsWith('https') ? https : http;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('ā Invalid URL:', url);
|
|
117
|
+
console.error('Error:', error);
|
|
118
|
+
console.error('Stack:', error.stack);
|
|
119
|
+
reject(new Error(`Invalid URL: ${url} - ${error.message}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Ensure directory exists
|
|
124
|
+
const dir = dirname(destPath);
|
|
125
|
+
if (!existsSync(dir)) {
|
|
126
|
+
try {
|
|
127
|
+
mkdirSync(dir, { recursive: true });
|
|
128
|
+
console.log(`ā
Created directory: ${dir}`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`ā Failed to create directory: ${dir}`);
|
|
131
|
+
console.error('Error:', error);
|
|
132
|
+
console.error('Stack:', error.stack);
|
|
133
|
+
reject(new Error(`Failed to create directory ${dir}: ${error.message}`));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const request = protocol.get(url, (response) => {
|
|
139
|
+
console.log(`š” Response status: ${response.statusCode} ${response.statusMessage}`);
|
|
140
|
+
console.log(` Headers:`, JSON.stringify(response.headers, null, 2));
|
|
141
|
+
|
|
142
|
+
// Handle redirects
|
|
143
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
144
|
+
const location = response.headers.location;
|
|
145
|
+
console.log(`š Redirect to: ${location}`);
|
|
146
|
+
|
|
147
|
+
// SECURITY FIX (#29): Check redirect limit to prevent infinite loops/SSRF
|
|
148
|
+
if (maxRedirects <= 0) {
|
|
149
|
+
reject(new Error(`Too many redirects (max 5) when downloading ${url}`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Handle relative redirects by resolving against original URL
|
|
155
|
+
const redirectUrl = location.startsWith('http') ? location : new URL(location, url).href;
|
|
156
|
+
console.log(`š Resolved redirect URL: ${redirectUrl} (${maxRedirects - 1} redirects remaining)`);
|
|
157
|
+
downloadFile(redirectUrl, destPath, onProgress, maxRedirects - 1).then(resolve).catch(reject);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(`ā Failed to resolve redirect URL`);
|
|
160
|
+
console.error('Original URL:', url);
|
|
161
|
+
console.error('Location header:', location);
|
|
162
|
+
console.error('Error:', error);
|
|
163
|
+
console.error('Stack:', error.stack);
|
|
164
|
+
reject(
|
|
165
|
+
new Error(`Failed to resolve redirect from ${url} to ${location}: ${error.message}`)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (response.statusCode !== 200) {
|
|
172
|
+
const errorMsg = `HTTP ${response.statusCode}: ${response.statusMessage} for ${url}`;
|
|
173
|
+
console.error(`ā ${errorMsg}`);
|
|
174
|
+
reject(new Error(errorMsg));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const totalBytes = parseInt(response.headers['content-length'] || '0', 10);
|
|
179
|
+
console.log(`š¦ Content length: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
180
|
+
let downloadedBytes = 0;
|
|
181
|
+
let lastLoggedPercent = -1;
|
|
182
|
+
|
|
183
|
+
const fileStream = createWriteStream(destPath);
|
|
184
|
+
|
|
185
|
+
let lastCallbackPercent = -1;
|
|
186
|
+
|
|
187
|
+
response.on('data', (chunk) => {
|
|
188
|
+
downloadedBytes += chunk.length;
|
|
189
|
+
if (onProgress && totalBytes > 0) {
|
|
190
|
+
const percent = Math.floor((downloadedBytes / totalBytes) * 100);
|
|
191
|
+
|
|
192
|
+
// Log progress every 10%
|
|
193
|
+
if (percent >= lastLoggedPercent + 10 || percent === 100) {
|
|
194
|
+
console.log(
|
|
195
|
+
` Progress: ${percent}% (${(downloadedBytes / 1024 / 1024).toFixed(2)} / ${(totalBytes / 1024 / 1024).toFixed(2)} MB)`
|
|
196
|
+
);
|
|
197
|
+
lastLoggedPercent = percent;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Only call onProgress when percent actually changes (avoid flooding IPC)
|
|
201
|
+
if (percent !== lastCallbackPercent) {
|
|
202
|
+
lastCallbackPercent = percent;
|
|
203
|
+
onProgress(percent, downloadedBytes, totalBytes);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
response.pipe(fileStream);
|
|
209
|
+
|
|
210
|
+
fileStream.on('finish', () => {
|
|
211
|
+
fileStream.close();
|
|
212
|
+
console.log(`ā
Download complete: ${destPath}`);
|
|
213
|
+
console.log(` Size: ${(downloadedBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
214
|
+
resolve();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
fileStream.on('error', (error) => {
|
|
218
|
+
fileStream.close();
|
|
219
|
+
console.error(`ā File stream error for ${destPath}`);
|
|
220
|
+
console.error('Error:', error);
|
|
221
|
+
console.error('Stack:', error.stack);
|
|
222
|
+
reject(new Error(`File write failed for ${destPath}: ${error.message}`));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
request.on('error', (error) => {
|
|
227
|
+
console.error(`ā Request error for ${url}`);
|
|
228
|
+
console.error('Error:', error);
|
|
229
|
+
console.error('Stack:', error.stack);
|
|
230
|
+
reject(new Error(`Download failed for ${url}: ${error.message}`));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
request.end();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run pip install command with progress tracking
|
|
239
|
+
*/
|
|
240
|
+
function pipInstall(packages, onProgress = null) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const pythonPath = getPythonPath();
|
|
243
|
+
|
|
244
|
+
if (!existsSync(pythonPath)) {
|
|
245
|
+
reject(new Error('Python not installed'));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Split packages string into args
|
|
250
|
+
const packageArgs = packages.split(/\s+/).filter((p) => p);
|
|
251
|
+
// Use --progress-bar on to ensure we get progress output
|
|
252
|
+
const args = ['-m', 'pip', 'install', ...packageArgs, '--no-cache-dir', '--progress-bar', 'on'];
|
|
253
|
+
|
|
254
|
+
const proc = spawn(pythonPath, args, {
|
|
255
|
+
env: {
|
|
256
|
+
...getPythonEnv(),
|
|
257
|
+
// Force color output which includes progress bars
|
|
258
|
+
FORCE_COLOR: '1',
|
|
259
|
+
PIP_PROGRESS_BAR: 'on',
|
|
260
|
+
},
|
|
261
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
let stdout = '';
|
|
265
|
+
let stderr = '';
|
|
266
|
+
let currentPackage = '';
|
|
267
|
+
let lastProgressUpdate = 0;
|
|
268
|
+
|
|
269
|
+
proc.stdout.on('data', (data) => {
|
|
270
|
+
stdout += data.toString();
|
|
271
|
+
const text = data.toString();
|
|
272
|
+
|
|
273
|
+
if (onProgress) {
|
|
274
|
+
// Parse pip output for progress info
|
|
275
|
+
// Look for "Collecting package" or "Downloading package"
|
|
276
|
+
const collectMatch = text.match(/Collecting\s+(\S+)/);
|
|
277
|
+
if (collectMatch) {
|
|
278
|
+
currentPackage = collectMatch[1].split('[')[0].split('>')[0].split('<')[0].split('=')[0];
|
|
279
|
+
onProgress('collecting', `Collecting ${currentPackage}...`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (text.includes('Successfully installed')) {
|
|
283
|
+
onProgress('complete', 'Installation complete');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
proc.stderr.on('data', (data) => {
|
|
289
|
+
stderr += data.toString();
|
|
290
|
+
const text = data.toString();
|
|
291
|
+
|
|
292
|
+
if (onProgress) {
|
|
293
|
+
// pip 23+ shows download progress in stderr with format like:
|
|
294
|
+
// "Downloading torch-2.0.0.whl (619.9 MB)"
|
|
295
|
+
// " āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā 100.5/619.9 MB 15.2 MB/s eta 0:00:34"
|
|
296
|
+
|
|
297
|
+
// Match "Downloading package (size)"
|
|
298
|
+
const downloadMatch = text.match(/Downloading\s+(\S+)\s+\(([^)]+)\)/);
|
|
299
|
+
if (downloadMatch) {
|
|
300
|
+
currentPackage = downloadMatch[1].split('-')[0];
|
|
301
|
+
const totalSize = downloadMatch[2];
|
|
302
|
+
onProgress('downloading', `Downloading ${currentPackage} (${totalSize})...`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Match progress line with downloaded/total and speed
|
|
306
|
+
// Format: " āāāāāāāā 100.5/619.9 MB 15.2 MB/s eta 0:00:34"
|
|
307
|
+
const progressMatch = text.match(
|
|
308
|
+
/(\d+\.?\d*)\s*\/\s*(\d+\.?\d*)\s*(MB|GB|KB)\s+(\d+\.?\d*)\s*(MB|GB|KB)\/s/
|
|
309
|
+
);
|
|
310
|
+
if (progressMatch) {
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
// Throttle updates to every 200ms to avoid flooding
|
|
313
|
+
if (now - lastProgressUpdate > 200) {
|
|
314
|
+
lastProgressUpdate = now;
|
|
315
|
+
const downloaded = parseFloat(progressMatch[1]);
|
|
316
|
+
const total = parseFloat(progressMatch[2]);
|
|
317
|
+
const unit = progressMatch[3];
|
|
318
|
+
const speed = progressMatch[4];
|
|
319
|
+
const speedUnit = progressMatch[5];
|
|
320
|
+
|
|
321
|
+
if (total > 0) {
|
|
322
|
+
const percent = Math.floor((downloaded / total) * 100);
|
|
323
|
+
const packageName = currentPackage || 'package';
|
|
324
|
+
onProgress(
|
|
325
|
+
'downloading',
|
|
326
|
+
`Downloading ${packageName}: ${downloaded}/${total} ${unit} (${speed} ${speedUnit}/s) - ${percent}%`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Also check for simpler progress format
|
|
333
|
+
// " āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā 619.9/619.9 MB"
|
|
334
|
+
const simpleProgressMatch = text.match(/(\d+\.?\d*)\s*\/\s*(\d+\.?\d*)\s*(MB|GB|KB)\s*$/);
|
|
335
|
+
if (simpleProgressMatch && !progressMatch) {
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
if (now - lastProgressUpdate > 200) {
|
|
338
|
+
lastProgressUpdate = now;
|
|
339
|
+
const downloaded = parseFloat(simpleProgressMatch[1]);
|
|
340
|
+
const total = parseFloat(simpleProgressMatch[2]);
|
|
341
|
+
const unit = simpleProgressMatch[3];
|
|
342
|
+
|
|
343
|
+
if (total > 0) {
|
|
344
|
+
const percent = Math.floor((downloaded / total) * 100);
|
|
345
|
+
const packageName = currentPackage || 'package';
|
|
346
|
+
onProgress(
|
|
347
|
+
'downloading',
|
|
348
|
+
`Downloading ${packageName}: ${downloaded}/${total} ${unit} - ${percent}%`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
proc.on('close', (code) => {
|
|
357
|
+
if (code === 0) {
|
|
358
|
+
resolve({ success: true, stdout });
|
|
359
|
+
} else {
|
|
360
|
+
reject(new Error(`pip install failed (code ${code}): ${stderr.slice(-500)}`));
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
proc.on('error', (err) => {
|
|
365
|
+
reject(new Error(`Failed to run pip: ${err.message}`));
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Detect GPU type for PyTorch variant selection
|
|
372
|
+
*/
|
|
373
|
+
function detectGPU() {
|
|
374
|
+
const platform = process.platform;
|
|
375
|
+
|
|
376
|
+
// macOS: Check for Apple Silicon (MPS)
|
|
377
|
+
if (platform === 'darwin') {
|
|
378
|
+
return process.arch === 'arm64' ? 'mps' : 'cpu';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Linux/Windows: Check for NVIDIA GPU
|
|
382
|
+
try {
|
|
383
|
+
execSync('nvidia-smi', { stdio: 'ignore' });
|
|
384
|
+
return 'cuda';
|
|
385
|
+
} catch {
|
|
386
|
+
return 'cpu';
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Download and install Python
|
|
392
|
+
*/
|
|
393
|
+
export async function downloadPython(onProgress = null) {
|
|
394
|
+
console.log('š Starting Python installation...');
|
|
395
|
+
const cacheDir = getCacheDir();
|
|
396
|
+
const pythonDir = join(cacheDir, 'python');
|
|
397
|
+
console.log(` Cache dir: ${cacheDir}`);
|
|
398
|
+
console.log(` Python dir: ${pythonDir}`);
|
|
399
|
+
|
|
400
|
+
// Check if already installed
|
|
401
|
+
const pythonPath = getPythonPath();
|
|
402
|
+
console.log(` Checking for existing Python: ${pythonPath}`);
|
|
403
|
+
if (existsSync(pythonPath)) {
|
|
404
|
+
console.log('ā
Python already installed');
|
|
405
|
+
if (onProgress) onProgress('complete', 'Python already installed');
|
|
406
|
+
return { success: true, path: pythonPath };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const url = getPythonBuildUrl();
|
|
411
|
+
console.log(`š Python download URL: ${url}`);
|
|
412
|
+
const tarPath = join(cacheDir, 'python.tar.gz');
|
|
413
|
+
|
|
414
|
+
// Download
|
|
415
|
+
console.log('š„ Starting Python download...');
|
|
416
|
+
if (onProgress) onProgress('downloading', 'Downloading Python...');
|
|
417
|
+
await downloadFile(url, tarPath, (percent) => {
|
|
418
|
+
if (onProgress) onProgress('downloading', `Downloading Python... ${percent}%`);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Extract
|
|
422
|
+
console.log('š¦ Extracting Python...');
|
|
423
|
+
if (onProgress) onProgress('extracting', 'Extracting Python...');
|
|
424
|
+
|
|
425
|
+
// Create python directory
|
|
426
|
+
if (!existsSync(pythonDir)) {
|
|
427
|
+
console.log(` Creating Python directory: ${pythonDir}`);
|
|
428
|
+
mkdirSync(pythonDir, { recursive: true });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Use tar to extract (available on all platforms)
|
|
432
|
+
console.log(' Loading tar module...');
|
|
433
|
+
const tar = await import('tar');
|
|
434
|
+
console.log(' Extracting tarball...');
|
|
435
|
+
await tar.extract({
|
|
436
|
+
file: tarPath,
|
|
437
|
+
cwd: pythonDir,
|
|
438
|
+
strip: 1,
|
|
439
|
+
});
|
|
440
|
+
console.log('ā
Extraction complete');
|
|
441
|
+
|
|
442
|
+
// Remove quarantine on macOS
|
|
443
|
+
if (process.platform === 'darwin') {
|
|
444
|
+
console.log('š Removing macOS quarantine attributes...');
|
|
445
|
+
try {
|
|
446
|
+
execSync(`xattr -cr "${pythonDir}"`, { stdio: 'ignore' });
|
|
447
|
+
console.log('ā
Quarantine removed');
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.warn('ā ļø Failed to remove quarantine (non-fatal):', error.message);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Clean up tarball
|
|
454
|
+
console.log('š§¹ Cleaning up tarball...');
|
|
455
|
+
rmSync(tarPath, { force: true });
|
|
456
|
+
|
|
457
|
+
// Upgrade pip and setuptools, fix common conflicts
|
|
458
|
+
if (onProgress) onProgress('configuring', 'Upgrading pip and setuptools...');
|
|
459
|
+
await pipInstall('--upgrade pip setuptools wheel');
|
|
460
|
+
|
|
461
|
+
// Fix coverage module conflict that can break installs
|
|
462
|
+
try {
|
|
463
|
+
await pipInstall('--upgrade coverage');
|
|
464
|
+
} catch {
|
|
465
|
+
// Non-fatal - coverage may not be installed
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
console.log('ā
Python installation complete');
|
|
469
|
+
if (onProgress) onProgress('complete', 'Python installed successfully');
|
|
470
|
+
return { success: true, path: pythonPath };
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error('ā Python installation failed');
|
|
473
|
+
console.error('Error:', error);
|
|
474
|
+
console.error('Stack:', error.stack);
|
|
475
|
+
return { success: false, error: error.message };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Download and install PyTorch
|
|
481
|
+
*/
|
|
482
|
+
export async function downloadPyTorch(variant = 'auto', onProgress = null) {
|
|
483
|
+
try {
|
|
484
|
+
// Detect variant if auto
|
|
485
|
+
if (variant === 'auto') {
|
|
486
|
+
const gpu = detectGPU();
|
|
487
|
+
variant = gpu === 'cuda' ? 'cuda' : gpu === 'mps' ? 'default' : 'cpu';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let packageSpec;
|
|
491
|
+
if (variant === 'cuda') {
|
|
492
|
+
packageSpec =
|
|
493
|
+
'torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118';
|
|
494
|
+
} else if (variant === 'default' || process.platform === 'darwin') {
|
|
495
|
+
packageSpec = 'torch torchvision torchaudio';
|
|
496
|
+
} else {
|
|
497
|
+
packageSpec = 'torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (onProgress) onProgress('installing', 'Installing PyTorch...');
|
|
501
|
+
await pipInstall(packageSpec, (stage, msg) => {
|
|
502
|
+
if (onProgress) onProgress(stage, msg);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
if (onProgress) onProgress('complete', 'PyTorch installed');
|
|
506
|
+
return { success: true, variant };
|
|
507
|
+
} catch (error) {
|
|
508
|
+
return { success: false, error: error.message };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Download and install SoundFile (audio backend for torchaudio)
|
|
514
|
+
*/
|
|
515
|
+
export async function downloadSoundFile(onProgress = null) {
|
|
516
|
+
try {
|
|
517
|
+
if (onProgress) onProgress('installing', 'Installing SoundFile...');
|
|
518
|
+
await pipInstall('soundfile', (stage, msg) => {
|
|
519
|
+
if (onProgress) onProgress(stage, msg);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
if (onProgress) onProgress('complete', 'SoundFile installed');
|
|
523
|
+
return { success: true };
|
|
524
|
+
} catch (error) {
|
|
525
|
+
return { success: false, error: error.message };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Download and install Demucs
|
|
531
|
+
*/
|
|
532
|
+
export async function downloadDemucs(onProgress = null) {
|
|
533
|
+
try {
|
|
534
|
+
if (onProgress) onProgress('installing', 'Installing Demucs...');
|
|
535
|
+
await pipInstall('demucs', (stage, msg) => {
|
|
536
|
+
if (onProgress) onProgress(stage, msg);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
if (onProgress) onProgress('complete', 'Demucs installed');
|
|
540
|
+
return { success: true };
|
|
541
|
+
} catch (error) {
|
|
542
|
+
return { success: false, error: error.message };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Download and install Whisper
|
|
548
|
+
*/
|
|
549
|
+
export async function downloadWhisper(onProgress = null) {
|
|
550
|
+
try {
|
|
551
|
+
if (onProgress) onProgress('installing', 'Installing Whisper...');
|
|
552
|
+
await pipInstall('openai-whisper', (stage, msg) => {
|
|
553
|
+
if (onProgress) onProgress(stage, msg);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
if (onProgress) onProgress('complete', 'Whisper installed');
|
|
557
|
+
return { success: true };
|
|
558
|
+
} catch (error) {
|
|
559
|
+
return { success: false, error: error.message };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Download and install torchcrepe (CREPE pitch detection)
|
|
565
|
+
*/
|
|
566
|
+
export async function downloadCrepe(onProgress = null) {
|
|
567
|
+
try {
|
|
568
|
+
if (onProgress) onProgress('installing', 'Installing torchcrepe...');
|
|
569
|
+
await pipInstall('torchcrepe>=0.0.12', (stage, msg) => {
|
|
570
|
+
if (onProgress) onProgress(stage, msg);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (onProgress) onProgress('complete', 'torchcrepe installed');
|
|
574
|
+
return { success: true };
|
|
575
|
+
} catch (error) {
|
|
576
|
+
return { success: false, error: error.message };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Whisper model URLs from https://github.com/openai/whisper/blob/main/whisper/__init__.py
|
|
581
|
+
const WHISPER_MODELS = {
|
|
582
|
+
tiny: 'https://openaipublic.azureedge.net/main/whisper/models/65147644a518d12f04e32d6f3b26facc3f8dd46e5390956a9424a650c0ce22b9/tiny.pt',
|
|
583
|
+
base: 'https://openaipublic.azureedge.net/main/whisper/models/ed3a0b6b1c0edf879ad9b11b1af5a0e6ab5db9205f891f668f8b0e6c6326e34e/base.pt',
|
|
584
|
+
small:
|
|
585
|
+
'https://openaipublic.azureedge.net/main/whisper/models/9ecf779972d90ba49c06d968637d720dd632c55bbf19d441fb42bf17a411e794/small.pt',
|
|
586
|
+
medium:
|
|
587
|
+
'https://openaipublic.azureedge.net/main/whisper/models/345ae4da62f9b3d59415adc60127b97c714f32e89e936602e85993674d08dcb1/medium.pt',
|
|
588
|
+
'large-v1':
|
|
589
|
+
'https://openaipublic.azureedge.net/main/whisper/models/e4b87e7e0bf463eb8e6956e646f1e277e901512310def2c24bf0e11bd3c28e9a/large-v1.pt',
|
|
590
|
+
'large-v2':
|
|
591
|
+
'https://openaipublic.azureedge.net/main/whisper/models/81f7c96c852ee8fc832187b0132e569d6c3065a3252ed18e56effd0b6a73e524/large-v2.pt',
|
|
592
|
+
'large-v3':
|
|
593
|
+
'https://openaipublic.azureedge.net/main/whisper/models/e5b1a55b89c1367dacf97e3e19bfd829a01529dbfdeefa8caeb59b3f1b81dadb/large-v3.pt',
|
|
594
|
+
'large-v3-turbo':
|
|
595
|
+
'https://openaipublic.azureedge.net/main/whisper/models/aff26ae408abcba5fbf8813c21e62b0941638c5f6eebfb145be0c9839262a19a/large-v3-turbo.pt',
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// Model sizes for progress display
|
|
599
|
+
const WHISPER_MODEL_SIZES = {
|
|
600
|
+
tiny: '~75 MB',
|
|
601
|
+
base: '~145 MB',
|
|
602
|
+
small: '~465 MB',
|
|
603
|
+
medium: '~1.5 GB',
|
|
604
|
+
'large-v1': '~3 GB',
|
|
605
|
+
'large-v2': '~3 GB',
|
|
606
|
+
'large-v3': '~3 GB',
|
|
607
|
+
'large-v3-turbo': '~1.6 GB',
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Download Whisper model directly with progress, then verify
|
|
612
|
+
*/
|
|
613
|
+
export async function downloadWhisperModel(modelName = 'large-v3-turbo', onProgress = null) {
|
|
614
|
+
const pythonPath = getPythonPath();
|
|
615
|
+
|
|
616
|
+
if (!existsSync(pythonPath)) {
|
|
617
|
+
return { success: false, error: 'Python not installed' };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const modelUrl = WHISPER_MODELS[modelName];
|
|
621
|
+
if (!modelUrl) {
|
|
622
|
+
return { success: false, error: `Unknown model: ${modelName}` };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const modelSize = WHISPER_MODEL_SIZES[modelName] || 'unknown size';
|
|
626
|
+
|
|
627
|
+
// Whisper stores models in ~/.cache/whisper/ (we use XDG_CACHE_HOME from getPythonEnv)
|
|
628
|
+
const cacheDir = getCacheDir();
|
|
629
|
+
const whisperCacheDir = join(cacheDir, 'whisper');
|
|
630
|
+
const modelPath = join(whisperCacheDir, `${modelName}.pt`);
|
|
631
|
+
|
|
632
|
+
// Check if model already exists
|
|
633
|
+
if (existsSync(modelPath)) {
|
|
634
|
+
if (onProgress) onProgress('complete', `${modelName} model already downloaded`);
|
|
635
|
+
return { success: true, model: modelName, cached: true };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Ensure whisper cache directory exists
|
|
639
|
+
if (!existsSync(whisperCacheDir)) {
|
|
640
|
+
mkdirSync(whisperCacheDir, { recursive: true });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
// Download with progress
|
|
645
|
+
if (onProgress)
|
|
646
|
+
onProgress('downloading', `Downloading ${modelName} model (${modelSize})... 0%`);
|
|
647
|
+
|
|
648
|
+
await downloadFile(modelUrl, modelPath, (percent) => {
|
|
649
|
+
if (onProgress) {
|
|
650
|
+
onProgress('downloading', `Downloading ${modelName} model (${modelSize})... ${percent}%`);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
if (onProgress) onProgress('downloading', `Verifying ${modelName} model...`);
|
|
655
|
+
|
|
656
|
+
// Verify the model loads correctly
|
|
657
|
+
const verifyResult = await verifyWhisperModel(modelName);
|
|
658
|
+
if (!verifyResult.success) {
|
|
659
|
+
// Delete corrupted download
|
|
660
|
+
try {
|
|
661
|
+
rmSync(modelPath);
|
|
662
|
+
} catch {
|
|
663
|
+
// Ignore cleanup errors
|
|
664
|
+
}
|
|
665
|
+
return { success: false, error: verifyResult.error };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (onProgress) onProgress('complete', `${modelName} model ready`);
|
|
669
|
+
return { success: true, model: modelName };
|
|
670
|
+
} catch (error) {
|
|
671
|
+
// Clean up partial download
|
|
672
|
+
try {
|
|
673
|
+
if (existsSync(modelPath)) {
|
|
674
|
+
rmSync(modelPath);
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
// Ignore cleanup errors
|
|
678
|
+
}
|
|
679
|
+
return { success: false, error: error.message };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Verify a Whisper model loads correctly
|
|
685
|
+
*/
|
|
686
|
+
function verifyWhisperModel(modelName) {
|
|
687
|
+
const pythonPath = getPythonPath();
|
|
688
|
+
|
|
689
|
+
return new Promise((resolve) => {
|
|
690
|
+
const script = `
|
|
691
|
+
import sys
|
|
692
|
+
import json
|
|
693
|
+
try:
|
|
694
|
+
import whisper
|
|
695
|
+
model = whisper.load_model("${modelName}")
|
|
696
|
+
print(json.dumps({"success": True}))
|
|
697
|
+
except Exception as e:
|
|
698
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
699
|
+
`;
|
|
700
|
+
|
|
701
|
+
const proc = spawn(pythonPath, ['-c', script], {
|
|
702
|
+
env: getPythonEnv(),
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
let stdout = '';
|
|
706
|
+
|
|
707
|
+
proc.stdout.on('data', (data) => {
|
|
708
|
+
stdout += data.toString();
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
proc.on('close', () => {
|
|
712
|
+
try {
|
|
713
|
+
const result = JSON.parse(stdout.trim());
|
|
714
|
+
resolve(result);
|
|
715
|
+
} catch {
|
|
716
|
+
resolve({ success: false, error: 'Failed to verify model' });
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
proc.on('error', (err) => {
|
|
721
|
+
resolve({ success: false, error: err.message });
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Download Demucs model by running a test load
|
|
728
|
+
*/
|
|
729
|
+
export function downloadDemucsModel(modelName = 'htdemucs_ft', onProgress = null) {
|
|
730
|
+
const pythonPath = getPythonPath();
|
|
731
|
+
|
|
732
|
+
if (!existsSync(pythonPath)) {
|
|
733
|
+
return Promise.resolve({ success: false, error: 'Python not installed' });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return new Promise((resolve) => {
|
|
737
|
+
if (onProgress) onProgress('downloading', `Downloading Demucs ${modelName} model (~300 MB)...`);
|
|
738
|
+
|
|
739
|
+
const script = `
|
|
740
|
+
import sys
|
|
741
|
+
import json
|
|
742
|
+
try:
|
|
743
|
+
old_stdout = sys.stdout
|
|
744
|
+
sys.stdout = sys.stderr
|
|
745
|
+
print("STATUS:Downloading model...", file=sys.stderr)
|
|
746
|
+
from demucs.pretrained import get_model
|
|
747
|
+
model = get_model("${modelName}")
|
|
748
|
+
print("STATUS:Model loaded successfully", file=sys.stderr)
|
|
749
|
+
sys.stdout = old_stdout
|
|
750
|
+
print(json.dumps({"success": True}))
|
|
751
|
+
except Exception as e:
|
|
752
|
+
sys.stdout = old_stdout if 'old_stdout' in locals() else sys.stdout
|
|
753
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
754
|
+
`;
|
|
755
|
+
|
|
756
|
+
const proc = spawn(pythonPath, ['-c', script], {
|
|
757
|
+
env: getPythonEnv(),
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
let stdout = '';
|
|
761
|
+
|
|
762
|
+
proc.stdout.on('data', (data) => {
|
|
763
|
+
stdout += data.toString();
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
proc.stderr.on('data', (data) => {
|
|
767
|
+
const line = data.toString().trim();
|
|
768
|
+
if (!line) return;
|
|
769
|
+
|
|
770
|
+
if (onProgress) {
|
|
771
|
+
// Check for our custom status messages
|
|
772
|
+
if (line.startsWith('STATUS:')) {
|
|
773
|
+
onProgress('downloading', line.replace('STATUS:', ''));
|
|
774
|
+
} else if (line.includes('%|')) {
|
|
775
|
+
// tqdm progress bar - extract percentage
|
|
776
|
+
const match = line.match(/(\d+)%\|/);
|
|
777
|
+
if (match) {
|
|
778
|
+
onProgress('downloading', `Downloading model... ${match[1]}%`);
|
|
779
|
+
}
|
|
780
|
+
} else if (line.includes('Downloading') || line.includes('downloading')) {
|
|
781
|
+
onProgress('downloading', line.slice(0, 80));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
proc.on('close', () => {
|
|
787
|
+
try {
|
|
788
|
+
const result = JSON.parse(stdout.trim());
|
|
789
|
+
if (result.success) {
|
|
790
|
+
if (onProgress) onProgress('complete', `${modelName} model ready`);
|
|
791
|
+
resolve({ success: true, model: modelName });
|
|
792
|
+
} else {
|
|
793
|
+
resolve({ success: false, error: result.error });
|
|
794
|
+
}
|
|
795
|
+
} catch {
|
|
796
|
+
resolve({ success: false, error: 'Failed to parse output' });
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
proc.on('error', (err) => {
|
|
801
|
+
resolve({ success: false, error: err.message });
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Download FFmpeg binary
|
|
808
|
+
*/
|
|
809
|
+
export async function downloadFFmpeg(onProgress = null) {
|
|
810
|
+
console.log('š¬ Starting FFmpeg installation...');
|
|
811
|
+
const cacheDir = getCacheDir();
|
|
812
|
+
const binDir = join(cacheDir, 'bin');
|
|
813
|
+
console.log(` Binary dir: ${binDir}`);
|
|
814
|
+
|
|
815
|
+
if (!existsSync(binDir)) {
|
|
816
|
+
mkdirSync(binDir, { recursive: true });
|
|
817
|
+
console.log(` Created binary directory`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const plat = process.platform;
|
|
821
|
+
const ffmpegName = plat === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
|
822
|
+
const ffprobeName = plat === 'win32' ? 'ffprobe.exe' : 'ffprobe';
|
|
823
|
+
const ffmpegPath = join(binDir, ffmpegName);
|
|
824
|
+
const ffprobePath = join(binDir, ffprobeName);
|
|
825
|
+
console.log(` Platform: ${plat}`);
|
|
826
|
+
console.log(` FFmpeg path: ${ffmpegPath}`);
|
|
827
|
+
console.log(` FFprobe path: ${ffprobePath}`);
|
|
828
|
+
|
|
829
|
+
// Check if both already exist
|
|
830
|
+
if (existsSync(ffmpegPath) && existsSync(ffprobePath)) {
|
|
831
|
+
console.log('ā
FFmpeg and FFprobe already installed');
|
|
832
|
+
if (onProgress) onProgress('complete', 'FFmpeg already downloaded');
|
|
833
|
+
return { success: true, ffmpegPath, ffprobePath };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
// URLs for ffmpeg builds that include both ffmpeg and ffprobe
|
|
838
|
+
let url;
|
|
839
|
+
let ffprobeUrl = null; // macOS needs separate download for ffprobe
|
|
840
|
+
if (plat === 'darwin') {
|
|
841
|
+
// evermeet.cx provides separate downloads for ffmpeg and ffprobe on macOS
|
|
842
|
+
url = 'https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip';
|
|
843
|
+
ffprobeUrl = 'https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip';
|
|
844
|
+
} else if (plat === 'win32') {
|
|
845
|
+
// BtbN builds include both ffmpeg and ffprobe
|
|
846
|
+
url =
|
|
847
|
+
'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip';
|
|
848
|
+
} else {
|
|
849
|
+
// John Van Sickle builds include both ffmpeg and ffprobe
|
|
850
|
+
url = 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz';
|
|
851
|
+
}
|
|
852
|
+
console.log(`š FFmpeg download URL: ${url}`);
|
|
853
|
+
if (ffprobeUrl) {
|
|
854
|
+
console.log(`š FFprobe download URL: ${ffprobeUrl}`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const archivePath = join(binDir, plat === 'linux' ? 'ffmpeg.tar.xz' : 'ffmpeg.zip');
|
|
858
|
+
console.log(` Archive path: ${archivePath}`);
|
|
859
|
+
|
|
860
|
+
// Download ffmpeg
|
|
861
|
+
console.log('š„ Starting FFmpeg download...');
|
|
862
|
+
if (onProgress) onProgress('downloading', 'Downloading FFmpeg...');
|
|
863
|
+
await downloadFile(url, archivePath, (percent) => {
|
|
864
|
+
if (onProgress) onProgress('downloading', `Downloading FFmpeg... ${percent}%`);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// Download ffprobe separately if needed (macOS)
|
|
868
|
+
let ffprobeArchivePath = null;
|
|
869
|
+
if (ffprobeUrl) {
|
|
870
|
+
ffprobeArchivePath = join(binDir, 'ffprobe.zip');
|
|
871
|
+
console.log('š„ Starting FFprobe download...');
|
|
872
|
+
if (onProgress) onProgress('downloading', 'Downloading FFprobe...');
|
|
873
|
+
await downloadFile(ffprobeUrl, ffprobeArchivePath, (percent) => {
|
|
874
|
+
if (onProgress) onProgress('downloading', `Downloading FFprobe... ${percent}%`);
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Extract
|
|
879
|
+
console.log('š¦ Extracting FFmpeg...');
|
|
880
|
+
if (onProgress) onProgress('extracting', 'Extracting FFmpeg...');
|
|
881
|
+
|
|
882
|
+
// Extract and find ffmpeg binary
|
|
883
|
+
const { mkdtempSync, readdirSync, statSync, copyFileSync } = await import('fs');
|
|
884
|
+
const { tmpdir } = await import('os');
|
|
885
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'ffmpeg-'));
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
console.log(` Extracting to temp dir: ${tempDir}`);
|
|
889
|
+
if (plat === 'linux') {
|
|
890
|
+
console.log(' Using tar to extract...');
|
|
891
|
+
execSync(`tar -xf "${archivePath}" -C "${tempDir}"`);
|
|
892
|
+
} else {
|
|
893
|
+
console.log(' Using yauzl to extract...');
|
|
894
|
+
await extractZip(archivePath, tempDir);
|
|
895
|
+
}
|
|
896
|
+
console.log(' Extraction complete, searching for binary...');
|
|
897
|
+
|
|
898
|
+
// Find binary recursively by name
|
|
899
|
+
const findBinary = (dir, name) => {
|
|
900
|
+
const files = readdirSync(dir);
|
|
901
|
+
for (const file of files) {
|
|
902
|
+
const fullPath = join(dir, file);
|
|
903
|
+
try {
|
|
904
|
+
if (statSync(fullPath).isDirectory()) {
|
|
905
|
+
const found = findBinary(fullPath, name);
|
|
906
|
+
if (found) return found;
|
|
907
|
+
} else if (file.toLowerCase() === name.toLowerCase()) {
|
|
908
|
+
return fullPath;
|
|
909
|
+
}
|
|
910
|
+
} catch {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return null;
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// Extract both ffmpeg and ffprobe
|
|
918
|
+
const ffmpegName = plat === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
|
919
|
+
const ffprobeName = plat === 'win32' ? 'ffprobe.exe' : 'ffprobe';
|
|
920
|
+
|
|
921
|
+
const ffmpegFound = findBinary(tempDir, ffmpegName);
|
|
922
|
+
const ffprobeFound = findBinary(tempDir, ffprobeName);
|
|
923
|
+
|
|
924
|
+
if (ffmpegFound) {
|
|
925
|
+
const ffmpegDest = join(binDir, ffmpegName);
|
|
926
|
+
console.log(` Found ffmpeg: ${ffmpegFound}`);
|
|
927
|
+
console.log(` Copying to: ${ffmpegDest}`);
|
|
928
|
+
copyFileSync(ffmpegFound, ffmpegDest);
|
|
929
|
+
if (plat !== 'win32') {
|
|
930
|
+
chmodSync(ffmpegDest, 0o755);
|
|
931
|
+
}
|
|
932
|
+
console.log('ā
ffmpeg binary installed');
|
|
933
|
+
} else {
|
|
934
|
+
console.error('ā ffmpeg binary not found in archive');
|
|
935
|
+
console.error(` Searched in: ${tempDir}`);
|
|
936
|
+
throw new Error('ffmpeg binary not found in archive');
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (ffprobeFound) {
|
|
940
|
+
console.log(` Found ffprobe: ${ffprobeFound}`);
|
|
941
|
+
console.log(` Copying to: ${ffprobePath}`);
|
|
942
|
+
copyFileSync(ffprobeFound, ffprobePath);
|
|
943
|
+
if (plat !== 'win32') {
|
|
944
|
+
chmodSync(ffprobePath, 0o755);
|
|
945
|
+
}
|
|
946
|
+
console.log('ā
ffprobe binary installed');
|
|
947
|
+
} else if (ffprobeArchivePath) {
|
|
948
|
+
// macOS: Extract ffprobe from separate archive
|
|
949
|
+
console.log('š¦ Extracting FFprobe from separate archive...');
|
|
950
|
+
const ffprobeTempDir = mkdtempSync(join(tmpdir(), 'ffprobe-'));
|
|
951
|
+
try {
|
|
952
|
+
await extractZip(ffprobeArchivePath, ffprobeTempDir);
|
|
953
|
+
const ffprobeExtracted = findBinary(ffprobeTempDir, ffprobeName);
|
|
954
|
+
if (ffprobeExtracted) {
|
|
955
|
+
console.log(` Found ffprobe: ${ffprobeExtracted}`);
|
|
956
|
+
console.log(` Copying to: ${ffprobePath}`);
|
|
957
|
+
copyFileSync(ffprobeExtracted, ffprobePath);
|
|
958
|
+
chmodSync(ffprobePath, 0o755);
|
|
959
|
+
console.log('ā
ffprobe binary installed');
|
|
960
|
+
} else {
|
|
961
|
+
console.warn('ā ļø ffprobe not found in separate archive');
|
|
962
|
+
}
|
|
963
|
+
rmSync(ffprobeTempDir, { recursive: true, force: true });
|
|
964
|
+
rmSync(ffprobeArchivePath, { force: true });
|
|
965
|
+
} catch (ffprobeError) {
|
|
966
|
+
console.warn('ā ļø Failed to extract ffprobe:', ffprobeError.message);
|
|
967
|
+
rmSync(ffprobeTempDir, { recursive: true, force: true });
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
console.warn('ā ļø ffprobe binary not found in archive');
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Clean up
|
|
974
|
+
console.log('š§¹ Cleaning up temporary files...');
|
|
975
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
976
|
+
rmSync(archivePath, { force: true });
|
|
977
|
+
|
|
978
|
+
console.log('ā
FFmpeg installation complete');
|
|
979
|
+
if (onProgress) onProgress('complete', 'FFmpeg installed');
|
|
980
|
+
return { success: true, ffmpegPath, ffprobePath };
|
|
981
|
+
} catch (extractError) {
|
|
982
|
+
console.error('ā FFmpeg extraction failed');
|
|
983
|
+
console.error('Error:', extractError);
|
|
984
|
+
console.error('Stack:', extractError.stack);
|
|
985
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
986
|
+
throw extractError;
|
|
987
|
+
}
|
|
988
|
+
} catch (error) {
|
|
989
|
+
console.error('ā FFmpeg installation failed');
|
|
990
|
+
console.error('Error:', error);
|
|
991
|
+
console.error('Stack:', error.stack);
|
|
992
|
+
return { success: false, error: error.message };
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Install all components in order
|
|
998
|
+
*/
|
|
999
|
+
export async function installAllComponents(onProgress = null) {
|
|
1000
|
+
console.log('š Starting installation of all components...');
|
|
1001
|
+
console.log(` Platform: ${process.platform}`);
|
|
1002
|
+
console.log(` Architecture: ${process.arch}`);
|
|
1003
|
+
|
|
1004
|
+
const results = {};
|
|
1005
|
+
|
|
1006
|
+
// Define steps with human-readable labels and estimated sizes
|
|
1007
|
+
const steps = [
|
|
1008
|
+
{ name: 'python', label: 'Python 3.12', fn: downloadPython, weight: 10, size: '~50 MB' },
|
|
1009
|
+
{
|
|
1010
|
+
name: 'pytorch',
|
|
1011
|
+
label: 'PyTorch',
|
|
1012
|
+
fn: () => downloadPyTorch('auto'),
|
|
1013
|
+
weight: 35,
|
|
1014
|
+
size: '~2 GB',
|
|
1015
|
+
},
|
|
1016
|
+
{ name: 'soundfile', label: 'SoundFile', fn: downloadSoundFile, weight: 2, size: '~5 MB' },
|
|
1017
|
+
{ name: 'demucs', label: 'Demucs', fn: downloadDemucs, weight: 8, size: '~100 MB' },
|
|
1018
|
+
{ name: 'whisper', label: 'Whisper', fn: downloadWhisper, weight: 8, size: '~50 MB' },
|
|
1019
|
+
{ name: 'crepe', label: 'CREPE', fn: downloadCrepe, weight: 4, size: '~20 MB' },
|
|
1020
|
+
{ name: 'ffmpeg', label: 'FFmpeg', fn: downloadFFmpeg, weight: 5, size: '~80 MB' },
|
|
1021
|
+
{
|
|
1022
|
+
name: 'whisperModel',
|
|
1023
|
+
label: 'Whisper Model',
|
|
1024
|
+
action: 'Downloading', // Custom action word instead of "Installing"
|
|
1025
|
+
fn: () => downloadWhisperModel('large-v3-turbo'),
|
|
1026
|
+
weight: 15,
|
|
1027
|
+
size: '~1.5 GB',
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
name: 'demucsModel',
|
|
1031
|
+
label: 'Demucs Model',
|
|
1032
|
+
action: 'Downloading', // Custom action word instead of "Installing"
|
|
1033
|
+
fn: () => downloadDemucsModel('htdemucs_ft'),
|
|
1034
|
+
weight: 15,
|
|
1035
|
+
size: '~300 MB',
|
|
1036
|
+
},
|
|
1037
|
+
];
|
|
1038
|
+
|
|
1039
|
+
console.log(`š Installation plan: ${steps.length} components`);
|
|
1040
|
+
steps.forEach((s, i) => {
|
|
1041
|
+
console.log(` ${i + 1}. ${s.label} (${s.size})`);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
let completedWeight = 0;
|
|
1045
|
+
const totalWeight = steps.reduce((sum, s) => sum + s.weight, 0);
|
|
1046
|
+
|
|
1047
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1048
|
+
const step = steps[i];
|
|
1049
|
+
const stepNumber = i + 1;
|
|
1050
|
+
const totalSteps = steps.length;
|
|
1051
|
+
const action = step.action || 'Installing';
|
|
1052
|
+
|
|
1053
|
+
console.log(`\nš¦ [${stepNumber}/${totalSteps}] ${action} ${step.label}...`);
|
|
1054
|
+
|
|
1055
|
+
if (onProgress) {
|
|
1056
|
+
const percent = Math.floor((completedWeight / totalWeight) * 100);
|
|
1057
|
+
onProgress(
|
|
1058
|
+
percent,
|
|
1059
|
+
`[${stepNumber}/${totalSteps}] ${action} ${step.label} (${step.size})...`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const result = await step.fn((stage, msg) => {
|
|
1064
|
+
if (onProgress && stage !== 'complete') {
|
|
1065
|
+
// Calculate sub-progress within this step
|
|
1066
|
+
const basePercent = Math.floor((completedWeight / totalWeight) * 100);
|
|
1067
|
+
|
|
1068
|
+
// For download stages, try to extract percent from message
|
|
1069
|
+
let subProgress = 0;
|
|
1070
|
+
const percentMatch = msg.match(/(\d+)%/);
|
|
1071
|
+
if (percentMatch) {
|
|
1072
|
+
subProgress = parseInt(percentMatch[1], 10);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Add sub-progress contribution
|
|
1076
|
+
const stepContribution = Math.floor((step.weight / totalWeight) * subProgress);
|
|
1077
|
+
const totalPercent = Math.min(basePercent + stepContribution, 99);
|
|
1078
|
+
|
|
1079
|
+
onProgress(totalPercent, `[${stepNumber}/${totalSteps}] ${msg}`);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
results[step.name] = result;
|
|
1084
|
+
|
|
1085
|
+
if (!result.success) {
|
|
1086
|
+
console.error(`ā [${stepNumber}/${totalSteps}] Failed to install ${step.label}`);
|
|
1087
|
+
console.error(' Error:', result.error);
|
|
1088
|
+
if (onProgress) {
|
|
1089
|
+
onProgress(
|
|
1090
|
+
Math.floor((completedWeight / totalWeight) * 100),
|
|
1091
|
+
`Failed to install ${step.label}: ${result.error}`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
return { success: false, failed: step.name, error: result.error, results };
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
console.log(`ā
[${stepNumber}/${totalSteps}] ${step.label} installed successfully`);
|
|
1098
|
+
|
|
1099
|
+
completedWeight += step.weight;
|
|
1100
|
+
|
|
1101
|
+
if (onProgress) {
|
|
1102
|
+
const percent = Math.floor((completedWeight / totalWeight) * 100);
|
|
1103
|
+
onProgress(percent, `[${stepNumber}/${totalSteps}] ${step.label} installed`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
console.log('\nš All components installed successfully!');
|
|
1108
|
+
console.log('Installation results:');
|
|
1109
|
+
Object.entries(results).forEach(([name, result]) => {
|
|
1110
|
+
console.log(` ${result.success ? 'ā
' : 'ā'} ${name}`);
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
if (onProgress) onProgress(100, 'All components installed successfully!');
|
|
1114
|
+
return { success: true, results };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export default {
|
|
1118
|
+
downloadPython,
|
|
1119
|
+
downloadPyTorch,
|
|
1120
|
+
downloadSoundFile,
|
|
1121
|
+
downloadDemucs,
|
|
1122
|
+
downloadWhisper,
|
|
1123
|
+
downloadCrepe,
|
|
1124
|
+
downloadWhisperModel,
|
|
1125
|
+
downloadDemucsModel,
|
|
1126
|
+
downloadFFmpeg,
|
|
1127
|
+
installAllComponents,
|
|
1128
|
+
};
|