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,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversion Service - Orchestrates the full karaoke creation pipeline
|
|
3
|
+
*
|
|
4
|
+
* Steps:
|
|
5
|
+
* 1. Convert input to WAV (if needed)
|
|
6
|
+
* 2. Run Demucs stem separation
|
|
7
|
+
* 3. Run Whisper transcription on vocals
|
|
8
|
+
* 4. Run CREPE pitch detection on vocals (optional)
|
|
9
|
+
* 5. Assemble into .stem.m4a file
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { existsSync, mkdirSync, rmSync, copyFileSync } from 'fs';
|
|
14
|
+
import { tmpdir } from 'os';
|
|
15
|
+
import { randomUUID } from 'crypto';
|
|
16
|
+
import { convertToWav, encodeToAAC, extractStemTrack } from './ffmpegService.js';
|
|
17
|
+
import { runDemucs, runWhisper, runCrepe } from './pythonRunner.js';
|
|
18
|
+
import { prepareWhisperContext } from './lrclibService.js';
|
|
19
|
+
import { buildStemM4a, injectLyricsIntoStemFile } from './stemBuilder.js';
|
|
20
|
+
import * as llmService from './llmService.js';
|
|
21
|
+
import { detectKey } from './keyDetection.js';
|
|
22
|
+
|
|
23
|
+
// Active conversion state
|
|
24
|
+
let conversionInProgress = false;
|
|
25
|
+
let conversionCancelled = false;
|
|
26
|
+
let currentProcess = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if conversion is in progress
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
export function isConversionInProgress() {
|
|
33
|
+
return conversionInProgress;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cancel the current conversion
|
|
38
|
+
* @returns {boolean} True if cancellation was initiated
|
|
39
|
+
*/
|
|
40
|
+
export function cancelConversion() {
|
|
41
|
+
if (!conversionInProgress) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
conversionCancelled = true;
|
|
46
|
+
|
|
47
|
+
// Kill current subprocess if any
|
|
48
|
+
if (currentProcess && typeof currentProcess.kill === 'function') {
|
|
49
|
+
try {
|
|
50
|
+
currentProcess.kill('SIGTERM');
|
|
51
|
+
} catch {
|
|
52
|
+
// Process may have already ended
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Reset flag immediately so user can start a new conversion
|
|
57
|
+
// The cancelled subprocess will clean up when it eventually errors
|
|
58
|
+
conversionInProgress = false;
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if conversion was cancelled
|
|
65
|
+
* @throws {Error} If cancelled
|
|
66
|
+
*/
|
|
67
|
+
function checkCancelled() {
|
|
68
|
+
if (conversionCancelled) {
|
|
69
|
+
throw new Error('Conversion cancelled');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run the full conversion pipeline
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} options - Conversion options
|
|
77
|
+
* @param {string} options.inputPath - Path to input audio/video file
|
|
78
|
+
* @param {string} options.title - Song title
|
|
79
|
+
* @param {string} options.artist - Artist name
|
|
80
|
+
* @param {Object} options.tags - All original ID3 tags to preserve
|
|
81
|
+
* @param {number} options.numStems - Number of stems (2 or 4)
|
|
82
|
+
* @param {string} options.whisperModel - Whisper model to use
|
|
83
|
+
* @param {string} options.language - Language code
|
|
84
|
+
* @param {boolean} options.enableCrepe - Whether to run pitch detection
|
|
85
|
+
* @param {string} options.referenceLyrics - Reference lyrics for Whisper hints
|
|
86
|
+
* @param {string} options.outputDir - Output directory (defaults to input file directory)
|
|
87
|
+
* @param {boolean} options.lyricsOnlyMode - Skip stem separation (for existing stem files)
|
|
88
|
+
* @param {number} options.vocalsTrackIndex - Track index for vocals (for lyrics-only mode)
|
|
89
|
+
* @param {Function} onProgress - Progress callback (step, message, progress)
|
|
90
|
+
* @param {Function} onConsoleOutput - Console output callback (line)
|
|
91
|
+
* @param {Object} settingsManager - Settings manager for LLM settings
|
|
92
|
+
* @returns {Promise<Object>} Result with outputPath
|
|
93
|
+
*/
|
|
94
|
+
export async function runConversion(
|
|
95
|
+
options,
|
|
96
|
+
onProgress = () => {},
|
|
97
|
+
onConsoleOutput = null,
|
|
98
|
+
settingsManager = null
|
|
99
|
+
) {
|
|
100
|
+
const {
|
|
101
|
+
inputPath,
|
|
102
|
+
title,
|
|
103
|
+
artist,
|
|
104
|
+
tags = {},
|
|
105
|
+
numStems = 4,
|
|
106
|
+
whisperModel = 'large-v3-turbo',
|
|
107
|
+
language = 'en',
|
|
108
|
+
enableCrepe = true,
|
|
109
|
+
referenceLyrics = '',
|
|
110
|
+
outputDir = dirname(inputPath),
|
|
111
|
+
lyricsOnlyMode = false,
|
|
112
|
+
vocalsTrackIndex = 4, // Default: vocals is typically track 4 in NI Stems format (0=master, 1=drums, 2=bass, 3=other, 4=vocals)
|
|
113
|
+
} = options;
|
|
114
|
+
|
|
115
|
+
if (conversionInProgress) {
|
|
116
|
+
throw new Error('Conversion already in progress');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
conversionInProgress = true;
|
|
120
|
+
conversionCancelled = false;
|
|
121
|
+
currentProcess = null;
|
|
122
|
+
|
|
123
|
+
// Create temp directory for intermediate files
|
|
124
|
+
const tempDir = join(tmpdir(), `kai-convert-${randomUUID()}`);
|
|
125
|
+
mkdirSync(tempDir, { recursive: true });
|
|
126
|
+
|
|
127
|
+
// Create stems temp directory
|
|
128
|
+
const stemsDir = join(tempDir, 'stems');
|
|
129
|
+
mkdirSync(stemsDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const safeFileName = (artist ? `${artist} - ${title}` : title).replace(/[<>:"/\\|?*]/g, '_');
|
|
133
|
+
|
|
134
|
+
const setCurrentProcess = (proc) => {
|
|
135
|
+
currentProcess = proc;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Different step labels for lyrics-only mode vs full conversion
|
|
139
|
+
const STEPS = lyricsOnlyMode
|
|
140
|
+
? {
|
|
141
|
+
extract: '1/4 Extract',
|
|
142
|
+
context: '2/4 Context',
|
|
143
|
+
whisper: '3/4 Lyrics',
|
|
144
|
+
crepe: '4/4 Pitch',
|
|
145
|
+
inject: '✓ Inject',
|
|
146
|
+
}
|
|
147
|
+
: {
|
|
148
|
+
wav: '1/7 Prepare',
|
|
149
|
+
demucs: '2/7 Stems',
|
|
150
|
+
context: '3/7 Context',
|
|
151
|
+
whisper: '4/7 Lyrics',
|
|
152
|
+
crepe: '5/7 Pitch',
|
|
153
|
+
encode: '6/7 Encode',
|
|
154
|
+
build: '7/7 Build',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let vocalsWavPath;
|
|
158
|
+
let stemPaths = null;
|
|
159
|
+
let demucsResult = null;
|
|
160
|
+
|
|
161
|
+
if (lyricsOnlyMode) {
|
|
162
|
+
// ========================================
|
|
163
|
+
// LYRICS-ONLY MODE: Skip stem separation
|
|
164
|
+
// ========================================
|
|
165
|
+
console.log('🎤 Lyrics-only mode: extracting vocals from existing stem file');
|
|
166
|
+
|
|
167
|
+
// Step 1: Extract vocals track to temp WAV (0-10%)
|
|
168
|
+
onProgress('extract', `[${STEPS.extract}] Extracting vocals track...`, 0);
|
|
169
|
+
checkCancelled();
|
|
170
|
+
|
|
171
|
+
vocalsWavPath = join(tempDir, 'vocals.wav');
|
|
172
|
+
await extractStemTrack(inputPath, vocalsWavPath, vocalsTrackIndex, { sampleRate: 44100 });
|
|
173
|
+
|
|
174
|
+
onProgress('extract', `[${STEPS.extract}] Vocals extracted`, 10);
|
|
175
|
+
checkCancelled();
|
|
176
|
+
} else {
|
|
177
|
+
// ========================================
|
|
178
|
+
// FULL CONVERSION MODE: Stem separation
|
|
179
|
+
// ========================================
|
|
180
|
+
|
|
181
|
+
// Step 1: Convert to WAV (0-5%)
|
|
182
|
+
onProgress('wav', `[${STEPS.wav}] Converting to WAV...`, 0);
|
|
183
|
+
checkCancelled();
|
|
184
|
+
|
|
185
|
+
const wavPath = join(tempDir, 'input.wav');
|
|
186
|
+
await convertToWav(inputPath, wavPath, { sampleRate: 44100 }, (progress) => {
|
|
187
|
+
onProgress(
|
|
188
|
+
'wav',
|
|
189
|
+
`[${STEPS.wav}] Converting to WAV... ${Math.round(progress)}%`,
|
|
190
|
+
Math.floor(progress * 0.05)
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
checkCancelled();
|
|
195
|
+
|
|
196
|
+
// Step 2: Run Demucs (5-50%)
|
|
197
|
+
onProgress('demucs', `[${STEPS.demucs}] Loading Demucs...`, 5);
|
|
198
|
+
checkCancelled();
|
|
199
|
+
|
|
200
|
+
demucsResult = await runDemucs(
|
|
201
|
+
wavPath,
|
|
202
|
+
stemsDir,
|
|
203
|
+
{ numStems },
|
|
204
|
+
(progress, message) => {
|
|
205
|
+
onProgress('demucs', `[${STEPS.demucs}] ${message}`, 5 + Math.floor(progress * 0.45));
|
|
206
|
+
},
|
|
207
|
+
onConsoleOutput,
|
|
208
|
+
setCurrentProcess
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
checkCancelled();
|
|
212
|
+
|
|
213
|
+
// Use stem paths returned by demucs_runner.py
|
|
214
|
+
stemPaths = demucsResult.stems;
|
|
215
|
+
|
|
216
|
+
if (!stemPaths || Object.keys(stemPaths).length === 0) {
|
|
217
|
+
throw new Error('Demucs did not return stem paths');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Verify stem files exist
|
|
221
|
+
for (const [name, path] of Object.entries(stemPaths)) {
|
|
222
|
+
if (!existsSync(path)) {
|
|
223
|
+
throw new Error(`Stem file not found: ${name} at ${path}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
vocalsWavPath = stemPaths.vocals;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ========================================
|
|
231
|
+
// COMMON: Whisper + CREPE + Output
|
|
232
|
+
// ========================================
|
|
233
|
+
|
|
234
|
+
// Progress offsets differ between modes
|
|
235
|
+
const contextStart = lyricsOnlyMode ? 10 : 50;
|
|
236
|
+
const whisperStart = lyricsOnlyMode ? 15 : 52;
|
|
237
|
+
const whisperEnd = lyricsOnlyMode ? 70 : 80;
|
|
238
|
+
const crepeStart = lyricsOnlyMode ? 70 : 80;
|
|
239
|
+
const crepeEnd = lyricsOnlyMode ? 95 : 90;
|
|
240
|
+
|
|
241
|
+
// Step: Prepare Whisper context
|
|
242
|
+
onProgress('context', `[${STEPS.context}] Preparing vocabulary hints...`, contextStart);
|
|
243
|
+
checkCancelled();
|
|
244
|
+
|
|
245
|
+
let initialPrompt = '';
|
|
246
|
+
if (referenceLyrics) {
|
|
247
|
+
const context = await prepareWhisperContext(title, artist, referenceLyrics);
|
|
248
|
+
initialPrompt = context.initialPrompt || '';
|
|
249
|
+
onProgress(
|
|
250
|
+
'context',
|
|
251
|
+
`[${STEPS.context}] Using ${initialPrompt.split(' ').length} vocabulary hints`,
|
|
252
|
+
contextStart + 1
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Step: Run Whisper
|
|
257
|
+
onProgress('whisper', `[${STEPS.whisper}] Loading Whisper...`, whisperStart);
|
|
258
|
+
checkCancelled();
|
|
259
|
+
|
|
260
|
+
if (initialPrompt) {
|
|
261
|
+
console.log(`🎤 Whisper prompt: ${initialPrompt}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let whisperResult = await runWhisper(
|
|
265
|
+
vocalsWavPath,
|
|
266
|
+
{
|
|
267
|
+
model: whisperModel,
|
|
268
|
+
language,
|
|
269
|
+
initialPrompt,
|
|
270
|
+
},
|
|
271
|
+
(progress, message) => {
|
|
272
|
+
const whisperProgressRange = whisperEnd - whisperStart;
|
|
273
|
+
onProgress(
|
|
274
|
+
'whisper',
|
|
275
|
+
`[${STEPS.whisper}] ${message}`,
|
|
276
|
+
whisperStart + Math.floor(progress * (whisperProgressRange / 100))
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
onConsoleOutput,
|
|
280
|
+
setCurrentProcess
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
checkCancelled();
|
|
284
|
+
|
|
285
|
+
// LLM lyrics correction (optional)
|
|
286
|
+
let llmStats = null;
|
|
287
|
+
if (settingsManager && referenceLyrics) {
|
|
288
|
+
try {
|
|
289
|
+
const llmSettings = llmService.getLLMSettings(settingsManager);
|
|
290
|
+
// Local LLM (lmstudio) doesn't require API key
|
|
291
|
+
const hasValidConfig = llmSettings.provider === 'lmstudio' || llmSettings.apiKey;
|
|
292
|
+
if (llmSettings.enabled && hasValidConfig) {
|
|
293
|
+
onProgress('whisper', `[${STEPS.whisper}] 🤖 AI correction...`, whisperEnd - 2);
|
|
294
|
+
const llmResult = await llmService.correctLyrics(
|
|
295
|
+
whisperResult,
|
|
296
|
+
referenceLyrics,
|
|
297
|
+
llmSettings
|
|
298
|
+
);
|
|
299
|
+
whisperResult = llmResult.output;
|
|
300
|
+
llmStats = llmResult.stats;
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.warn('⚠️ LLM correction failed, using original Whisper output:', error.message);
|
|
304
|
+
// Continue with original Whisper output
|
|
305
|
+
llmStats = {
|
|
306
|
+
corrections_applied: 0,
|
|
307
|
+
suggestions_made: 0,
|
|
308
|
+
corrections_rejected: 0,
|
|
309
|
+
failed: true,
|
|
310
|
+
error: error.message,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
checkCancelled();
|
|
316
|
+
|
|
317
|
+
// Run CREPE (optional)
|
|
318
|
+
let pitchData = null;
|
|
319
|
+
if (enableCrepe) {
|
|
320
|
+
onProgress('crepe', `[${STEPS.crepe}] Loading CREPE...`, crepeStart);
|
|
321
|
+
checkCancelled();
|
|
322
|
+
|
|
323
|
+
const crepeProgressRange = crepeEnd - crepeStart;
|
|
324
|
+
const crepeResult = await runCrepe(
|
|
325
|
+
vocalsWavPath,
|
|
326
|
+
null,
|
|
327
|
+
{},
|
|
328
|
+
(progress, message) => {
|
|
329
|
+
onProgress(
|
|
330
|
+
'crepe',
|
|
331
|
+
`[${STEPS.crepe}] ${message}`,
|
|
332
|
+
crepeStart + Math.floor(progress * (crepeProgressRange / 100))
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
onConsoleOutput,
|
|
336
|
+
setCurrentProcess
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
pitchData = crepeResult;
|
|
340
|
+
|
|
341
|
+
// Detect musical key from pitch data
|
|
342
|
+
if (pitchData?.pitch_data) {
|
|
343
|
+
const keyResult = detectKey(pitchData);
|
|
344
|
+
if (keyResult.key !== 'unknown') {
|
|
345
|
+
console.log(
|
|
346
|
+
`🎵 Detected key: ${keyResult.key} (confidence: ${(keyResult.confidence * 100).toFixed(0)}%)`
|
|
347
|
+
);
|
|
348
|
+
pitchData.detected_key = keyResult;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
checkCancelled();
|
|
354
|
+
|
|
355
|
+
// Build tags array for filtering
|
|
356
|
+
const karaTags = [];
|
|
357
|
+
if (llmStats && llmStats.corrections_applied > 0) {
|
|
358
|
+
karaTags.push('ai_corrected');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let outputPath;
|
|
362
|
+
|
|
363
|
+
if (lyricsOnlyMode) {
|
|
364
|
+
// ========================================
|
|
365
|
+
// LYRICS-ONLY: Inject kara atom into existing file
|
|
366
|
+
// ========================================
|
|
367
|
+
onProgress('inject', `[${STEPS.inject}] Adding lyrics to stem file...`, 95);
|
|
368
|
+
checkCancelled();
|
|
369
|
+
|
|
370
|
+
// Output to same directory with modified name, or optionally overwrite in place
|
|
371
|
+
outputPath = join(outputDir, `${safeFileName}.stem.m4a`);
|
|
372
|
+
|
|
373
|
+
// Copy original file to output location if different
|
|
374
|
+
if (inputPath !== outputPath) {
|
|
375
|
+
copyFileSync(inputPath, outputPath);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Inject lyrics into the copied/original file
|
|
379
|
+
await injectLyricsIntoStemFile({
|
|
380
|
+
filePath: outputPath,
|
|
381
|
+
lyrics: whisperResult,
|
|
382
|
+
pitch: pitchData,
|
|
383
|
+
llmCorrections: llmStats,
|
|
384
|
+
tags: karaTags,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
onProgress('complete', '✓ Lyrics added to stem file!', 100);
|
|
388
|
+
} else {
|
|
389
|
+
// ========================================
|
|
390
|
+
// FULL CONVERSION: Encode stems and build new file
|
|
391
|
+
// ========================================
|
|
392
|
+
const stemLabels = {
|
|
393
|
+
master: '🎵 Master',
|
|
394
|
+
vocals: '🎤 Vocals',
|
|
395
|
+
drums: '🥁 Drums',
|
|
396
|
+
bass: '🎸 Bass',
|
|
397
|
+
other: '🎹 Other',
|
|
398
|
+
no_vocals: '🎵 Instrumental',
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
onProgress('encode', `[${STEPS.encode}] Encoding stems to AAC...`, 90);
|
|
402
|
+
checkCancelled();
|
|
403
|
+
|
|
404
|
+
const aacPaths = {};
|
|
405
|
+
const wavPath = join(tempDir, 'input.wav');
|
|
406
|
+
|
|
407
|
+
// First encode the master (original mix) - required by NI Stems spec
|
|
408
|
+
const masterAacPath = join(tempDir, 'master.m4a');
|
|
409
|
+
onProgress('encode', `[${STEPS.encode}] Encoding ${stemLabels.master}...`, 90);
|
|
410
|
+
await encodeToAAC(wavPath, masterAacPath, { codec: 'aac', bitrate: '192k' });
|
|
411
|
+
aacPaths.master = masterAacPath;
|
|
412
|
+
|
|
413
|
+
checkCancelled();
|
|
414
|
+
|
|
415
|
+
// Then encode the individual stems in NI Stems order: drums, bass, other, vocals
|
|
416
|
+
const stemOrder = ['drums', 'bass', 'other', 'vocals'];
|
|
417
|
+
const encodeProgress = 4 / stemOrder.length;
|
|
418
|
+
|
|
419
|
+
for (let i = 0; i < stemOrder.length; i++) {
|
|
420
|
+
const stemName = stemOrder[i];
|
|
421
|
+
const stemPath = stemPaths[stemName];
|
|
422
|
+
if (!stemPath) continue; // Skip if stem doesn't exist
|
|
423
|
+
|
|
424
|
+
const aacPath = join(tempDir, `${stemName}.m4a`);
|
|
425
|
+
const label = stemLabels[stemName] || stemName;
|
|
426
|
+
|
|
427
|
+
onProgress(
|
|
428
|
+
'encode',
|
|
429
|
+
`[${STEPS.encode}] Encoding ${label}...`,
|
|
430
|
+
91 + Math.floor(i * encodeProgress)
|
|
431
|
+
);
|
|
432
|
+
await encodeToAAC(stemPath, aacPath, { codec: 'aac', bitrate: '192k' });
|
|
433
|
+
aacPaths[stemName] = aacPath;
|
|
434
|
+
|
|
435
|
+
checkCancelled();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Build .stem.m4a (95-100%)
|
|
439
|
+
onProgress('build', `[${STEPS.build}] Packaging stem.m4a file...`, 95);
|
|
440
|
+
checkCancelled();
|
|
441
|
+
|
|
442
|
+
outputPath = join(outputDir, `${safeFileName}.stem.m4a`);
|
|
443
|
+
|
|
444
|
+
await buildStemM4a({
|
|
445
|
+
outputPath,
|
|
446
|
+
stems: aacPaths,
|
|
447
|
+
metadata: {
|
|
448
|
+
title,
|
|
449
|
+
artist,
|
|
450
|
+
duration: demucsResult?.duration || 0,
|
|
451
|
+
tags, // Preserve all original ID3 tags
|
|
452
|
+
},
|
|
453
|
+
lyrics: whisperResult,
|
|
454
|
+
pitch: pitchData,
|
|
455
|
+
llmCorrections: llmStats, // LLM corrections metadata
|
|
456
|
+
tags: karaTags, // Kara atom tags for filtering
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
onProgress('complete', '✓ Karaoke file created!', 100);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Cleanup temp directory
|
|
463
|
+
try {
|
|
464
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
465
|
+
} catch {
|
|
466
|
+
console.warn('Failed to cleanup temp directory:', tempDir);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
conversionInProgress = false;
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
success: true,
|
|
473
|
+
outputPath,
|
|
474
|
+
duration: demucsResult?.duration || 0,
|
|
475
|
+
stems: lyricsOnlyMode ? [] : Object.keys(stemPaths || {}),
|
|
476
|
+
hasLyrics: Boolean(whisperResult?.words?.length),
|
|
477
|
+
hasPitch: Boolean(pitchData),
|
|
478
|
+
llmStats,
|
|
479
|
+
lyricsOnlyMode,
|
|
480
|
+
};
|
|
481
|
+
} catch (error) {
|
|
482
|
+
conversionInProgress = false;
|
|
483
|
+
|
|
484
|
+
// Cleanup on error
|
|
485
|
+
try {
|
|
486
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
487
|
+
} catch {
|
|
488
|
+
// Ignore cleanup errors
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (conversionCancelled) {
|
|
492
|
+
return { success: false, cancelled: true };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export default {
|
|
500
|
+
runConversion,
|
|
501
|
+
cancelConversion,
|
|
502
|
+
isConversionInProgress,
|
|
503
|
+
};
|