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,1236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreateTab - Create karaoke files from audio
|
|
3
|
+
*
|
|
4
|
+
* Handles the full workflow:
|
|
5
|
+
* 1. Check/install Python dependencies
|
|
6
|
+
* 2. Select audio file
|
|
7
|
+
* 3. Configure options (stems, whisper model, etc.)
|
|
8
|
+
* 4. Run conversion pipeline
|
|
9
|
+
* 5. Output .stem.m4a file
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
13
|
+
import { LLM_DEFAULTS, CREATOR_DEFAULTS } from '../../../shared/defaults.js';
|
|
14
|
+
import { PortalSelect } from '../PortalSelect.jsx';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Shared Styles
|
|
18
|
+
// ============================================================================
|
|
19
|
+
const STYLES = {
|
|
20
|
+
input:
|
|
21
|
+
'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white',
|
|
22
|
+
select:
|
|
23
|
+
'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white',
|
|
24
|
+
btnPrimary:
|
|
25
|
+
'px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors',
|
|
26
|
+
btnSecondary:
|
|
27
|
+
'px-6 py-3 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors',
|
|
28
|
+
btnSuccess:
|
|
29
|
+
'px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors',
|
|
30
|
+
sectionTitle: 'text-lg font-semibold text-gray-900 dark:text-white mb-4',
|
|
31
|
+
card: 'bg-gray-100 dark:bg-gray-800 rounded-lg p-6',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Helper Components
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
function Spinner({ message, size = 'md' }) {
|
|
39
|
+
const sizeClasses = {
|
|
40
|
+
sm: 'h-8 w-8',
|
|
41
|
+
md: 'h-12 w-12',
|
|
42
|
+
};
|
|
43
|
+
return (
|
|
44
|
+
<div className="text-center">
|
|
45
|
+
<div
|
|
46
|
+
className={`animate-spin rounded-full ${sizeClasses[size]} border-b-2 border-blue-500 mx-auto mb-3`}
|
|
47
|
+
/>
|
|
48
|
+
{message && <p className="text-gray-600 dark:text-gray-400">{message}</p>}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ErrorDisplay({ error, onDismiss }) {
|
|
54
|
+
if (!error) return null;
|
|
55
|
+
return (
|
|
56
|
+
<div className="bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-400 px-4 py-3 rounded mb-6 select-text">
|
|
57
|
+
{onDismiss && (
|
|
58
|
+
<button
|
|
59
|
+
className="float-right text-red-700 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 text-xl leading-none"
|
|
60
|
+
onClick={onDismiss}
|
|
61
|
+
>
|
|
62
|
+
×
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
<div className="font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-96">{error}</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function MissingLinesDetails({ missingLines }) {
|
|
71
|
+
if (!missingLines || missingLines.length === 0) return null;
|
|
72
|
+
return (
|
|
73
|
+
<details className="text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 rounded p-2">
|
|
74
|
+
<summary className="cursor-pointer font-semibold">
|
|
75
|
+
💡 {missingLines.length} missing line{missingLines.length !== 1 ? 's' : ''} suggested (not
|
|
76
|
+
applied)
|
|
77
|
+
</summary>
|
|
78
|
+
<ul className="mt-2 space-y-1 ml-4 list-disc max-h-40 overflow-y-auto">
|
|
79
|
+
{missingLines.map((line, i) => (
|
|
80
|
+
<li key={i}>
|
|
81
|
+
<span className="text-blue-600 dark:text-blue-400">"{line.suggested_text}"</span>{' '}
|
|
82
|
+
<span className="text-gray-500 dark:text-gray-400">
|
|
83
|
+
({line.start?.toFixed(1)}s-{line.end?.toFixed(1)}s, {line.confidence} confidence)
|
|
84
|
+
</span>
|
|
85
|
+
{line.reason && (
|
|
86
|
+
<div className="text-gray-500 dark:text-gray-400 ml-2">→ {line.reason}</div>
|
|
87
|
+
)}
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
</details>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function SongTitle({ artist, title }) {
|
|
96
|
+
return artist ? `${artist} - ${title}` : title;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Format LLM provider name for display
|
|
100
|
+
function formatProviderName(provider) {
|
|
101
|
+
const names = {
|
|
102
|
+
anthropic: 'Anthropic Claude',
|
|
103
|
+
openai: 'OpenAI',
|
|
104
|
+
gemini: 'Google Gemini',
|
|
105
|
+
lmstudio: 'Local LLM Server',
|
|
106
|
+
};
|
|
107
|
+
return names[provider] || provider;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function CreateTab({ bridge: _bridge }) {
|
|
111
|
+
const [status, setStatus] = useState('checking'); // checking, setup, ready, creating, complete, installing
|
|
112
|
+
const [components, setComponents] = useState(null);
|
|
113
|
+
const [installProgress, setInstallProgress] = useState(null);
|
|
114
|
+
const [error, setError] = useState(null);
|
|
115
|
+
|
|
116
|
+
// Sub-tab state: 'create' or 'settings'
|
|
117
|
+
const [activeSubTab, setActiveSubTab] = useState('create');
|
|
118
|
+
|
|
119
|
+
// File and conversion state
|
|
120
|
+
const [selectedFile, setSelectedFile] = useState(null);
|
|
121
|
+
const [fileLoading, setFileLoading] = useState(false); // Loading state for file selection
|
|
122
|
+
const [conversionProgress, setConversionProgress] = useState(null);
|
|
123
|
+
const [completedFile, setCompletedFile] = useState(null);
|
|
124
|
+
const [llmStats, setLlmStats] = useState(null);
|
|
125
|
+
const [songDuration, setSongDuration] = useState(null);
|
|
126
|
+
const [processingTime, setProcessingTime] = useState(null);
|
|
127
|
+
const [consoleLog, setConsoleLog] = useState([]);
|
|
128
|
+
const [isLyricsOnlyMode, setIsLyricsOnlyMode] = useState(false); // Track if we started in lyrics-only mode
|
|
129
|
+
const consoleEndRef = useRef(null);
|
|
130
|
+
const conversionStartTimeRef = useRef(null);
|
|
131
|
+
|
|
132
|
+
// Options
|
|
133
|
+
const [options, setOptions] = useState({
|
|
134
|
+
title: '',
|
|
135
|
+
artist: '',
|
|
136
|
+
numStems: 4, // Always 4 stems for .stem.m4a format
|
|
137
|
+
language: 'en',
|
|
138
|
+
referenceLyrics: '',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// LLM settings - uses unified defaults from shared/defaults.js
|
|
142
|
+
const [llmSettings, setLlmSettings] = useState({ ...LLM_DEFAULTS });
|
|
143
|
+
const [llmTestResult, setLlmTestResult] = useState(null);
|
|
144
|
+
|
|
145
|
+
// Output settings
|
|
146
|
+
const [outputToSongsFolder, setOutputToSongsFolder] = useState(false);
|
|
147
|
+
const [whisperModel, setWhisperModel] = useState(CREATOR_DEFAULTS.whisperModel);
|
|
148
|
+
const [enableCrepe, setEnableCrepe] = useState(CREATOR_DEFAULTS.enableCrepe);
|
|
149
|
+
|
|
150
|
+
const checkComponents = useCallback(async () => {
|
|
151
|
+
setStatus('checking');
|
|
152
|
+
setError(null);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const result = await window.kaiAPI?.creator?.checkComponents();
|
|
156
|
+
|
|
157
|
+
if (result?.success) {
|
|
158
|
+
setComponents(result);
|
|
159
|
+
|
|
160
|
+
if (result.allInstalled) {
|
|
161
|
+
setStatus('ready');
|
|
162
|
+
} else {
|
|
163
|
+
setStatus('setup');
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
setError(result?.error || 'Failed to check components');
|
|
167
|
+
setStatus('setup');
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error('Error checking components:', err);
|
|
171
|
+
setError(err.message);
|
|
172
|
+
setStatus('setup');
|
|
173
|
+
}
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
checkComponents();
|
|
178
|
+
|
|
179
|
+
// Load LLM settings
|
|
180
|
+
const loadLLMSettings = async () => {
|
|
181
|
+
try {
|
|
182
|
+
const settings = await window.kaiAPI?.creator?.getLLMSettings();
|
|
183
|
+
if (settings) {
|
|
184
|
+
setLlmSettings(settings);
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error('Failed to load LLM settings:', err);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
loadLLMSettings();
|
|
191
|
+
|
|
192
|
+
// Load output settings
|
|
193
|
+
const loadOutputSettings = async () => {
|
|
194
|
+
try {
|
|
195
|
+
const outputToSongs = await window.kaiAPI?.settings?.get(
|
|
196
|
+
'creator.outputToSongsFolder',
|
|
197
|
+
false
|
|
198
|
+
);
|
|
199
|
+
setOutputToSongsFolder(outputToSongs);
|
|
200
|
+
const whisper = await window.kaiAPI?.settings?.get(
|
|
201
|
+
'creator.whisperModel',
|
|
202
|
+
CREATOR_DEFAULTS.whisperModel
|
|
203
|
+
);
|
|
204
|
+
setWhisperModel(whisper);
|
|
205
|
+
const crepe = await window.kaiAPI?.settings?.get(
|
|
206
|
+
'creator.enableCrepe',
|
|
207
|
+
CREATOR_DEFAULTS.enableCrepe
|
|
208
|
+
);
|
|
209
|
+
setEnableCrepe(crepe);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error('Failed to load output settings:', err);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
loadOutputSettings();
|
|
215
|
+
|
|
216
|
+
// Listen for installation progress
|
|
217
|
+
const onInstallProgress = (_event, progress) => {
|
|
218
|
+
setInstallProgress(progress);
|
|
219
|
+
if (progress.step === 'complete') {
|
|
220
|
+
setStatus('checking');
|
|
221
|
+
checkComponents();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const onInstallError = (_event, err) => {
|
|
226
|
+
setError(err.error);
|
|
227
|
+
setStatus('setup');
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Listen for conversion progress
|
|
231
|
+
const onConversionProgress = (_event, progress) => {
|
|
232
|
+
setConversionProgress(progress);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const onConversionConsole = (_event, data) => {
|
|
236
|
+
const line = data.line;
|
|
237
|
+
|
|
238
|
+
setConsoleLog((prev) => {
|
|
239
|
+
// If line contains progress indicators (%, |, ━), replace last line
|
|
240
|
+
// This handles tqdm and pip progress bars that use \r
|
|
241
|
+
if (line.match(/\d+%|[│┃║▌▍▎▏█]|━|█/) && prev.length > 0) {
|
|
242
|
+
// Check if last line was also a progress line
|
|
243
|
+
const lastLine = prev[prev.length - 1];
|
|
244
|
+
if (lastLine.match(/\d+%|[│┃║▌▍▎▏█]|━|█/)) {
|
|
245
|
+
// Replace last line
|
|
246
|
+
return [...prev.slice(0, -1), line];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Otherwise append new line
|
|
251
|
+
return [...prev, line];
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Auto-scroll to bottom
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
257
|
+
}, 100);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const onConversionComplete = async (_event, result) => {
|
|
261
|
+
const endTime = Date.now();
|
|
262
|
+
const elapsed = conversionStartTimeRef.current
|
|
263
|
+
? (endTime - conversionStartTimeRef.current) / 1000
|
|
264
|
+
: null;
|
|
265
|
+
|
|
266
|
+
setCompletedFile(result.outputPath);
|
|
267
|
+
setLlmStats(result.llmStats);
|
|
268
|
+
setSongDuration(result.duration);
|
|
269
|
+
setProcessingTime(elapsed);
|
|
270
|
+
setStatus('complete');
|
|
271
|
+
setConversionProgress(null);
|
|
272
|
+
|
|
273
|
+
// If saved to songs folder, trigger a library sync to pick up the new file
|
|
274
|
+
if (result.savedToSongsFolder) {
|
|
275
|
+
try {
|
|
276
|
+
await window.kaiAPI?.library?.syncLibrary?.();
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.error('Failed to sync library after creation:', err);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const onConversionError = (_event, err) => {
|
|
284
|
+
setError(err.error);
|
|
285
|
+
setStatus('ready');
|
|
286
|
+
setConversionProgress(null);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
window.kaiAPI?.creator?.onInstallProgress(onInstallProgress);
|
|
290
|
+
window.kaiAPI?.creator?.onInstallError(onInstallError);
|
|
291
|
+
window.kaiAPI?.creator?.onConversionProgress(onConversionProgress);
|
|
292
|
+
window.kaiAPI?.creator?.onConversionConsole(onConversionConsole);
|
|
293
|
+
window.kaiAPI?.creator?.onConversionComplete(onConversionComplete);
|
|
294
|
+
window.kaiAPI?.creator?.onConversionError(onConversionError);
|
|
295
|
+
|
|
296
|
+
return () => {
|
|
297
|
+
window.kaiAPI?.creator?.removeInstallProgressListener(onInstallProgress);
|
|
298
|
+
window.kaiAPI?.creator?.removeInstallErrorListener(onInstallError);
|
|
299
|
+
window.kaiAPI?.creator?.removeConversionProgressListener(onConversionProgress);
|
|
300
|
+
window.kaiAPI?.creator?.removeConversionConsoleListener(onConversionConsole);
|
|
301
|
+
window.kaiAPI?.creator?.removeConversionCompleteListener(onConversionComplete);
|
|
302
|
+
window.kaiAPI?.creator?.removeConversionErrorListener(onConversionError);
|
|
303
|
+
};
|
|
304
|
+
}, [checkComponents]);
|
|
305
|
+
|
|
306
|
+
const handleInstall = async () => {
|
|
307
|
+
setStatus('installing');
|
|
308
|
+
setInstallProgress({ step: 'starting', message: 'Starting installation...', progress: 0 });
|
|
309
|
+
setError(null);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const result = await window.kaiAPI?.creator?.installComponents();
|
|
313
|
+
if (!result?.success) {
|
|
314
|
+
setError(result?.error || 'Installation failed');
|
|
315
|
+
setStatus('setup');
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
setError(err.message);
|
|
319
|
+
setStatus('setup');
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const handleSelectFile = async () => {
|
|
324
|
+
try {
|
|
325
|
+
setFileLoading(true);
|
|
326
|
+
setError(null);
|
|
327
|
+
const result = await window.kaiAPI?.creator?.selectFile();
|
|
328
|
+
|
|
329
|
+
if (result?.cancelled) {
|
|
330
|
+
setFileLoading(false);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (result?.success && result.file) {
|
|
335
|
+
setSelectedFile(result.file);
|
|
336
|
+
setOptions((prev) => ({
|
|
337
|
+
...prev,
|
|
338
|
+
title: result.file.title || prev.title,
|
|
339
|
+
artist: result.file.artist || prev.artist,
|
|
340
|
+
// Auto-populate lyrics if found (prefer plain text)
|
|
341
|
+
referenceLyrics: result.lyrics?.plainLyrics || prev.referenceLyrics,
|
|
342
|
+
}));
|
|
343
|
+
setError(null);
|
|
344
|
+
} else {
|
|
345
|
+
setError(result?.error || 'Failed to select file');
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
setError(err.message);
|
|
349
|
+
} finally {
|
|
350
|
+
setFileLoading(false);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const handleSearchLyrics = async () => {
|
|
355
|
+
if (!options.title) {
|
|
356
|
+
setError('Please enter a title to search for lyrics');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const result = await window.kaiAPI?.creator?.searchLyrics(options.title, options.artist);
|
|
362
|
+
|
|
363
|
+
if (result?.success) {
|
|
364
|
+
setOptions((prev) => ({
|
|
365
|
+
...prev,
|
|
366
|
+
// Prefer plain lyrics (no timestamps) for Whisper reference
|
|
367
|
+
referenceLyrics: result.plainLyrics || '',
|
|
368
|
+
}));
|
|
369
|
+
setError(null);
|
|
370
|
+
} else {
|
|
371
|
+
setError(result?.error || 'No lyrics found');
|
|
372
|
+
}
|
|
373
|
+
} catch (err) {
|
|
374
|
+
setError(err.message);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const handleStartConversion = async () => {
|
|
379
|
+
if (!selectedFile) {
|
|
380
|
+
setError('Please select a file first');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
setStatus('creating');
|
|
385
|
+
setError(null);
|
|
386
|
+
setConsoleLog([]); // Clear console log
|
|
387
|
+
setConversionProgress({ step: 'starting', message: 'Starting conversion...', progress: 0 });
|
|
388
|
+
conversionStartTimeRef.current = Date.now();
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
// Get output directory based on settings
|
|
392
|
+
let outputDir = undefined; // Default: same directory as source file
|
|
393
|
+
if (outputToSongsFolder) {
|
|
394
|
+
const songsFolder = await window.kaiAPI?.library?.getSongsFolder?.();
|
|
395
|
+
if (songsFolder) {
|
|
396
|
+
outputDir = songsFolder;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Determine if this is a lyrics-only conversion (stem file without lyrics)
|
|
401
|
+
const lyricsOnlyMode = selectedFile.hasStems && !selectedFile.hasLyrics;
|
|
402
|
+
setIsLyricsOnlyMode(lyricsOnlyMode);
|
|
403
|
+
|
|
404
|
+
const result = await window.kaiAPI?.creator?.startConversion({
|
|
405
|
+
inputPath: selectedFile.path,
|
|
406
|
+
title: options.title || selectedFile.title,
|
|
407
|
+
artist: options.artist || selectedFile.artist,
|
|
408
|
+
tags: selectedFile.tags || {}, // Preserve all original ID3 tags
|
|
409
|
+
numStems: options.numStems,
|
|
410
|
+
whisperModel: whisperModel,
|
|
411
|
+
language: options.language,
|
|
412
|
+
enableCrepe: enableCrepe,
|
|
413
|
+
referenceLyrics: options.referenceLyrics,
|
|
414
|
+
outputDir,
|
|
415
|
+
// Lyrics-only mode options
|
|
416
|
+
lyricsOnlyMode,
|
|
417
|
+
vocalsTrackIndex: selectedFile.vocalsTrackIndex ?? 4,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (!result?.success) {
|
|
421
|
+
setError(result?.error || 'Conversion failed');
|
|
422
|
+
setStatus('ready');
|
|
423
|
+
setConversionProgress(null);
|
|
424
|
+
}
|
|
425
|
+
} catch (err) {
|
|
426
|
+
setError(err.message);
|
|
427
|
+
setStatus('ready');
|
|
428
|
+
setConversionProgress(null);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const handleCancelConversion = async () => {
|
|
433
|
+
try {
|
|
434
|
+
await window.kaiAPI?.creator?.cancelConversion();
|
|
435
|
+
setStatus('ready');
|
|
436
|
+
setConversionProgress(null);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
setError(err.message);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const handleCreateAnother = () => {
|
|
443
|
+
setSelectedFile(null);
|
|
444
|
+
setCompletedFile(null);
|
|
445
|
+
setLlmStats(null);
|
|
446
|
+
setSongDuration(null);
|
|
447
|
+
setProcessingTime(null);
|
|
448
|
+
setIsLyricsOnlyMode(false);
|
|
449
|
+
conversionStartTimeRef.current = null;
|
|
450
|
+
setOptions({
|
|
451
|
+
title: '',
|
|
452
|
+
artist: '',
|
|
453
|
+
numStems: 4,
|
|
454
|
+
language: 'en',
|
|
455
|
+
referenceLyrics: '',
|
|
456
|
+
});
|
|
457
|
+
setStatus('ready');
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const handleOpenInEditor = async () => {
|
|
461
|
+
if (!completedFile) return;
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
// Load the song into the editor
|
|
465
|
+
await window.kaiAPI?.editor?.loadKai?.(completedFile);
|
|
466
|
+
|
|
467
|
+
// Switch to the editor tab by manipulating DOM (same pattern as TabNavigation)
|
|
468
|
+
document.querySelectorAll('[id$="-tab"]').forEach((pane) => {
|
|
469
|
+
pane.classList.add('hidden');
|
|
470
|
+
pane.classList.remove('block', 'flex');
|
|
471
|
+
});
|
|
472
|
+
const editorPane = document.getElementById('editor-tab');
|
|
473
|
+
if (editorPane) {
|
|
474
|
+
editorPane.classList.remove('hidden');
|
|
475
|
+
editorPane.classList.add('block');
|
|
476
|
+
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
console.error('Failed to open in editor:', err);
|
|
479
|
+
setError(`Failed to open in editor: ${err.message}`);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const handleSaveLLMSettings = async () => {
|
|
484
|
+
try {
|
|
485
|
+
await window.kaiAPI?.creator?.saveLLMSettings(llmSettings);
|
|
486
|
+
setLlmTestResult({ success: true, message: 'Settings saved!' });
|
|
487
|
+
setTimeout(() => setLlmTestResult(null), 3000);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
setLlmTestResult({ success: false, message: err.message });
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const handleTestLLMConnection = async () => {
|
|
494
|
+
if (!llmSettings.apiKey && llmSettings.provider !== 'lmstudio') {
|
|
495
|
+
setLlmTestResult({ success: false, message: 'API key required' });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
setLlmTestResult({ testing: true, message: 'Testing connection...' });
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const result = await window.kaiAPI?.creator?.testLLMConnection(llmSettings);
|
|
503
|
+
setLlmTestResult(result);
|
|
504
|
+
setTimeout(() => setLlmTestResult(null), 3000);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
setLlmTestResult({ success: false, message: err.message });
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const formatDuration = (seconds) => {
|
|
511
|
+
if (!seconds) return '--:--';
|
|
512
|
+
const mins = Math.floor(seconds / 60);
|
|
513
|
+
const secs = Math.floor(seconds % 60);
|
|
514
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
if (status === 'checking') {
|
|
518
|
+
return (
|
|
519
|
+
<div className="flex items-center justify-center h-full">
|
|
520
|
+
<Spinner message="Checking AI tools..." />
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Component display configuration
|
|
526
|
+
const componentDisplay = [
|
|
527
|
+
{ key: 'python', label: 'Python 3.10+' },
|
|
528
|
+
{ key: 'pytorch', label: 'PyTorch' },
|
|
529
|
+
{ key: 'soundfile', label: 'SoundFile (Audio)' },
|
|
530
|
+
{ key: 'demucs', label: 'Demucs (Stems)' },
|
|
531
|
+
{ key: 'whisper', label: 'Whisper (Lyrics)' },
|
|
532
|
+
{ key: 'crepe', label: 'CREPE (Pitch)' },
|
|
533
|
+
{ key: 'ffmpeg', label: 'FFmpeg' },
|
|
534
|
+
{ key: 'whisperModel', label: 'Whisper Model' },
|
|
535
|
+
{ key: 'demucsModel', label: 'Demucs Model' },
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
if (status === 'installing') {
|
|
539
|
+
return (
|
|
540
|
+
<div className="flex items-center justify-center h-full p-8">
|
|
541
|
+
<div className="max-w-lg text-center">
|
|
542
|
+
<div className="text-6xl mb-6">⚡</div>
|
|
543
|
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
544
|
+
Installing AI Tools
|
|
545
|
+
</h2>
|
|
546
|
+
|
|
547
|
+
<div className="mb-6">
|
|
548
|
+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
549
|
+
<div
|
|
550
|
+
className="h-full bg-blue-600 transition-all duration-300"
|
|
551
|
+
style={{ width: `${installProgress?.progress || 0}%` }}
|
|
552
|
+
/>
|
|
553
|
+
</div>
|
|
554
|
+
<p className="text-gray-600 dark:text-gray-400 mt-2 break-words">
|
|
555
|
+
{installProgress?.message || 'Starting...'}
|
|
556
|
+
</p>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<button
|
|
560
|
+
className={STYLES.btnSecondary}
|
|
561
|
+
onClick={() => window.kaiAPI?.creator?.cancelInstall()}
|
|
562
|
+
>
|
|
563
|
+
Cancel
|
|
564
|
+
</button>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (status === 'setup') {
|
|
571
|
+
return (
|
|
572
|
+
<div className="flex items-center justify-center h-full p-8">
|
|
573
|
+
<div className="max-w-lg text-center">
|
|
574
|
+
<div className="text-6xl mb-6">⚡</div>
|
|
575
|
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
576
|
+
AI Tools Setup Required
|
|
577
|
+
</h2>
|
|
578
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
579
|
+
To create karaoke files, you need to install AI processing tools. This includes stem
|
|
580
|
+
separation (Demucs), lyrics transcription (Whisper), and pitch detection (CREPE).
|
|
581
|
+
</p>
|
|
582
|
+
|
|
583
|
+
<ErrorDisplay error={error} />
|
|
584
|
+
|
|
585
|
+
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-6 text-left">
|
|
586
|
+
<div className="space-y-2">
|
|
587
|
+
{componentDisplay.map(({ key, label }) => {
|
|
588
|
+
const comp = components?.[key];
|
|
589
|
+
const isInstalled = comp?.installed;
|
|
590
|
+
const version = comp?.version;
|
|
591
|
+
const device = comp?.device;
|
|
592
|
+
|
|
593
|
+
return (
|
|
594
|
+
<div key={key} className="flex items-center justify-between">
|
|
595
|
+
<span className="text-gray-700 dark:text-gray-300">{label}</span>
|
|
596
|
+
<span className={isInstalled ? 'text-green-500' : 'text-gray-400'}>
|
|
597
|
+
{isInstalled
|
|
598
|
+
? `✓ ${version || ''}${device ? ` (${device})` : ''}`.trim() ||
|
|
599
|
+
'✓ Installed'
|
|
600
|
+
: '○ Not installed'}
|
|
601
|
+
</span>
|
|
602
|
+
</div>
|
|
603
|
+
);
|
|
604
|
+
})}
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
609
|
+
<p>Download size: ~2-4 GB</p>
|
|
610
|
+
<p>Disk space required: ~5 GB</p>
|
|
611
|
+
</div>
|
|
612
|
+
|
|
613
|
+
<button className={STYLES.btnPrimary} onClick={handleInstall}>
|
|
614
|
+
Install AI Tools
|
|
615
|
+
</button>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Creating state - show progress
|
|
622
|
+
if (status === 'creating') {
|
|
623
|
+
return (
|
|
624
|
+
<div className="h-full flex flex-col p-8">
|
|
625
|
+
<div className="max-w-4xl mx-auto w-full">
|
|
626
|
+
<div className="text-center mb-6">
|
|
627
|
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
628
|
+
{isLyricsOnlyMode
|
|
629
|
+
? 'Adding Lyrics to Stem File 🎤'
|
|
630
|
+
: 'Creating Stems+Karaoke File ⚡'}
|
|
631
|
+
</h2>
|
|
632
|
+
|
|
633
|
+
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4">
|
|
634
|
+
<p className="text-gray-700 dark:text-gray-300 font-medium">
|
|
635
|
+
<SongTitle artist={options.artist} title={options.title} />
|
|
636
|
+
</p>
|
|
637
|
+
</div>
|
|
638
|
+
|
|
639
|
+
<div className="mb-6">
|
|
640
|
+
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
641
|
+
<div
|
|
642
|
+
className="h-full bg-blue-600 transition-all duration-300"
|
|
643
|
+
style={{ width: `${conversionProgress?.progress || 0}%` }}
|
|
644
|
+
/>
|
|
645
|
+
</div>
|
|
646
|
+
<p className="text-gray-600 dark:text-gray-400 mt-3">
|
|
647
|
+
{conversionProgress?.message || 'Starting...'}
|
|
648
|
+
</p>
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
{/* Console Log Panel */}
|
|
652
|
+
{consoleLog.length > 0 && (
|
|
653
|
+
<div className="mb-6 bg-gray-900 dark:bg-black rounded-lg p-4 h-48 overflow-y-auto">
|
|
654
|
+
<div className="text-xs font-mono text-green-400 whitespace-pre-wrap select-text leading-tight">
|
|
655
|
+
{consoleLog.map((line, i) => (
|
|
656
|
+
<div key={i}>{line}</div>
|
|
657
|
+
))}
|
|
658
|
+
<div ref={consoleEndRef} />
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
)}
|
|
662
|
+
|
|
663
|
+
<button className={STYLES.btnSecondary} onClick={handleCancelConversion}>
|
|
664
|
+
Cancel
|
|
665
|
+
</button>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Complete state - show success
|
|
673
|
+
if (status === 'complete') {
|
|
674
|
+
return (
|
|
675
|
+
<div className="flex items-center justify-center h-full p-8">
|
|
676
|
+
<div className="max-w-lg w-full text-center">
|
|
677
|
+
<div className="text-6xl mb-6">✅</div>
|
|
678
|
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
679
|
+
{isLyricsOnlyMode ? 'Lyrics Added!' : 'Karaoke File Created!'}
|
|
680
|
+
</h2>
|
|
681
|
+
|
|
682
|
+
<div className="bg-green-100 dark:bg-green-900/30 rounded-lg p-4 mb-6">
|
|
683
|
+
<p className="text-green-700 dark:text-green-400 font-medium">
|
|
684
|
+
<SongTitle artist={options.artist} title={options.title} />
|
|
685
|
+
</p>
|
|
686
|
+
|
|
687
|
+
{/* Processing Stats */}
|
|
688
|
+
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2 space-y-1">
|
|
689
|
+
{songDuration && <p>🎵 Song length: {formatDuration(songDuration)}</p>}
|
|
690
|
+
{processingTime && (
|
|
691
|
+
<p>
|
|
692
|
+
⏱️ Processing time: {formatDuration(processingTime)}
|
|
693
|
+
{songDuration && (
|
|
694
|
+
<span className="ml-2 text-xs">
|
|
695
|
+
({(songDuration / processingTime).toFixed(1)}x realtime)
|
|
696
|
+
</span>
|
|
697
|
+
)}
|
|
698
|
+
</p>
|
|
699
|
+
)}
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
{/* LLM Stats */}
|
|
703
|
+
{llmStats?.failed ? (
|
|
704
|
+
<div className="mt-2">
|
|
705
|
+
<p className="text-sm text-yellow-600 dark:text-yellow-400">
|
|
706
|
+
⚠️ AI correction failed ({formatProviderName(llmStats.provider)}):{' '}
|
|
707
|
+
{llmStats.error || 'Unknown error'}
|
|
708
|
+
</p>
|
|
709
|
+
</div>
|
|
710
|
+
) : llmStats && llmStats.corrections_applied > 0 ? (
|
|
711
|
+
<div className="mt-2 space-y-2">
|
|
712
|
+
<p className="text-sm text-green-600 dark:text-green-400">
|
|
713
|
+
✨ {formatProviderName(llmStats.provider)}: {llmStats.suggestions_made} suggestion
|
|
714
|
+
{llmStats.suggestions_made !== 1 ? 's' : ''} ({llmStats.corrections_applied}{' '}
|
|
715
|
+
applied
|
|
716
|
+
{llmStats.missing_lines_suggested > 0 &&
|
|
717
|
+
`, ${llmStats.missing_lines_suggested} for review`}
|
|
718
|
+
)
|
|
719
|
+
</p>
|
|
720
|
+
{llmStats.corrections && llmStats.corrections.length > 0 && (
|
|
721
|
+
<details className="text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 rounded p-2">
|
|
722
|
+
<summary className="cursor-pointer font-semibold">
|
|
723
|
+
✅ {llmStats.corrections.length} correction
|
|
724
|
+
{llmStats.corrections.length !== 1 ? 's' : ''} applied
|
|
725
|
+
</summary>
|
|
726
|
+
<ul className="mt-2 space-y-1 ml-4 list-disc max-h-40 overflow-y-auto">
|
|
727
|
+
{llmStats.corrections.map((corr, i) => (
|
|
728
|
+
<li key={i}>
|
|
729
|
+
Line #{corr.line_num}:{' '}
|
|
730
|
+
<span className="text-red-600 dark:text-red-400 line-through">
|
|
731
|
+
{corr.old_text}
|
|
732
|
+
</span>{' '}
|
|
733
|
+
→{' '}
|
|
734
|
+
<span className="text-green-600 dark:text-green-400">
|
|
735
|
+
{corr.new_text}
|
|
736
|
+
</span>
|
|
737
|
+
</li>
|
|
738
|
+
))}
|
|
739
|
+
</ul>
|
|
740
|
+
</details>
|
|
741
|
+
)}
|
|
742
|
+
<MissingLinesDetails missingLines={llmStats.missing_lines} />
|
|
743
|
+
</div>
|
|
744
|
+
) : llmStats && llmStats.corrections_applied === 0 ? (
|
|
745
|
+
<div className="mt-2">
|
|
746
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
747
|
+
✓ {formatProviderName(llmStats.provider)}: No corrections applied
|
|
748
|
+
{llmStats.missing_lines_suggested > 0
|
|
749
|
+
? `, ${llmStats.missing_lines_suggested} missing line${llmStats.missing_lines_suggested !== 1 ? 's' : ''} suggested`
|
|
750
|
+
: ''}
|
|
751
|
+
</p>
|
|
752
|
+
<MissingLinesDetails missingLines={llmStats.missing_lines} />
|
|
753
|
+
</div>
|
|
754
|
+
) : (
|
|
755
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
|
756
|
+
AI correction not used
|
|
757
|
+
</p>
|
|
758
|
+
)}
|
|
759
|
+
|
|
760
|
+
<p className="text-sm text-green-600 dark:text-green-500 mt-2 break-all">
|
|
761
|
+
{completedFile}
|
|
762
|
+
</p>
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
<div className="flex gap-4 justify-center">
|
|
766
|
+
{outputToSongsFolder && (
|
|
767
|
+
<button className={STYLES.btnPrimary} onClick={handleOpenInEditor}>
|
|
768
|
+
Open in Editor
|
|
769
|
+
</button>
|
|
770
|
+
)}
|
|
771
|
+
<button
|
|
772
|
+
className={outputToSongsFolder ? STYLES.btnSecondary : STYLES.btnPrimary}
|
|
773
|
+
onClick={handleCreateAnother}
|
|
774
|
+
>
|
|
775
|
+
Create Another
|
|
776
|
+
</button>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Ready state - show create interface
|
|
784
|
+
return (
|
|
785
|
+
<div className="h-full overflow-y-auto p-6">
|
|
786
|
+
<div className="max-w-2xl mx-auto">
|
|
787
|
+
{/* Sub-tab navigation */}
|
|
788
|
+
<div className="flex border-b border-gray-300 dark:border-gray-600 mb-6">
|
|
789
|
+
<button
|
|
790
|
+
className={`px-4 py-2 font-medium transition-colors ${
|
|
791
|
+
activeSubTab === 'create'
|
|
792
|
+
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'
|
|
793
|
+
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
|
794
|
+
}`}
|
|
795
|
+
onClick={() => setActiveSubTab('create')}
|
|
796
|
+
>
|
|
797
|
+
Create
|
|
798
|
+
</button>
|
|
799
|
+
<button
|
|
800
|
+
className={`px-4 py-2 font-medium transition-colors ${
|
|
801
|
+
activeSubTab === 'settings'
|
|
802
|
+
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'
|
|
803
|
+
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
|
804
|
+
}`}
|
|
805
|
+
onClick={() => setActiveSubTab('settings')}
|
|
806
|
+
>
|
|
807
|
+
Settings
|
|
808
|
+
</button>
|
|
809
|
+
</div>
|
|
810
|
+
|
|
811
|
+
<ErrorDisplay error={error} onDismiss={() => setError(null)} />
|
|
812
|
+
|
|
813
|
+
{/* Settings Sub-tab */}
|
|
814
|
+
{activeSubTab === 'settings' && (
|
|
815
|
+
<div className="space-y-6">
|
|
816
|
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Creator Settings</h2>
|
|
817
|
+
|
|
818
|
+
{/* Output Location */}
|
|
819
|
+
<div className={STYLES.card}>
|
|
820
|
+
<h3 className={STYLES.sectionTitle}>Output Location</h3>
|
|
821
|
+
<label className="flex items-center cursor-pointer">
|
|
822
|
+
<input
|
|
823
|
+
type="checkbox"
|
|
824
|
+
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
825
|
+
checked={outputToSongsFolder}
|
|
826
|
+
onChange={async (e) => {
|
|
827
|
+
const value = e.target.checked;
|
|
828
|
+
setOutputToSongsFolder(value);
|
|
829
|
+
await window.kaiAPI?.settings?.set('creator.outputToSongsFolder', value);
|
|
830
|
+
}}
|
|
831
|
+
/>
|
|
832
|
+
<span className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
|
833
|
+
Save output files to karaoke songs folder
|
|
834
|
+
</span>
|
|
835
|
+
</label>
|
|
836
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
837
|
+
When enabled, created .stem.m4a files will be saved to your configured songs library
|
|
838
|
+
folder instead of next to the source file.
|
|
839
|
+
</p>
|
|
840
|
+
</div>
|
|
841
|
+
|
|
842
|
+
{/* Whisper Model */}
|
|
843
|
+
<div className={STYLES.card}>
|
|
844
|
+
<h3 className={STYLES.sectionTitle}>Whisper Model</h3>
|
|
845
|
+
<div className="w-64">
|
|
846
|
+
<PortalSelect
|
|
847
|
+
value={whisperModel}
|
|
848
|
+
onChange={async (e) => {
|
|
849
|
+
const value = e.target.value;
|
|
850
|
+
setWhisperModel(value);
|
|
851
|
+
await window.kaiAPI?.settings?.set('creator.whisperModel', value);
|
|
852
|
+
}}
|
|
853
|
+
options={[
|
|
854
|
+
{ value: 'large-v3-turbo', label: 'Large V3 Turbo (recommended)' },
|
|
855
|
+
{ value: 'large-v3', label: 'Large V3 (slower, slightly better)' },
|
|
856
|
+
{ value: 'medium', label: 'Medium (faster)' },
|
|
857
|
+
{ value: 'small', label: 'Small (fastest)' },
|
|
858
|
+
]}
|
|
859
|
+
/>
|
|
860
|
+
</div>
|
|
861
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
862
|
+
Larger models are more accurate but slower. Large V3 Turbo is recommended for most
|
|
863
|
+
users.
|
|
864
|
+
</p>
|
|
865
|
+
</div>
|
|
866
|
+
|
|
867
|
+
{/* Pitch Detection */}
|
|
868
|
+
<div className={STYLES.card}>
|
|
869
|
+
<h3 className={STYLES.sectionTitle}>Pitch Detection</h3>
|
|
870
|
+
<label className="flex items-center cursor-pointer">
|
|
871
|
+
<input
|
|
872
|
+
type="checkbox"
|
|
873
|
+
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
874
|
+
checked={enableCrepe}
|
|
875
|
+
onChange={async (e) => {
|
|
876
|
+
const value = e.target.checked;
|
|
877
|
+
setEnableCrepe(value);
|
|
878
|
+
await window.kaiAPI?.settings?.set('creator.enableCrepe', value);
|
|
879
|
+
}}
|
|
880
|
+
/>
|
|
881
|
+
<span className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
|
882
|
+
Enable pitch detection (CREPE)
|
|
883
|
+
</span>
|
|
884
|
+
</label>
|
|
885
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
886
|
+
Analyzes vocal pitch for karaoke scoring features. Adds processing time but enables
|
|
887
|
+
pitch visualization.
|
|
888
|
+
</p>
|
|
889
|
+
</div>
|
|
890
|
+
|
|
891
|
+
{/* LLM Settings */}
|
|
892
|
+
<div className={STYLES.card}>
|
|
893
|
+
<h3 className={STYLES.sectionTitle}>AI Lyrics Correction</h3>
|
|
894
|
+
|
|
895
|
+
<div className="space-y-4">
|
|
896
|
+
<label className="flex items-center cursor-pointer">
|
|
897
|
+
<input
|
|
898
|
+
type="checkbox"
|
|
899
|
+
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
900
|
+
checked={llmSettings.enabled}
|
|
901
|
+
onChange={(e) =>
|
|
902
|
+
setLlmSettings((prev) => ({ ...prev, enabled: e.target.checked }))
|
|
903
|
+
}
|
|
904
|
+
/>
|
|
905
|
+
<span className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
|
906
|
+
Use AI to improve lyrics accuracy (compares Whisper output to reference lyrics)
|
|
907
|
+
</span>
|
|
908
|
+
</label>
|
|
909
|
+
|
|
910
|
+
{llmSettings.enabled && (
|
|
911
|
+
<>
|
|
912
|
+
<div>
|
|
913
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
914
|
+
AI Provider
|
|
915
|
+
</label>
|
|
916
|
+
<PortalSelect
|
|
917
|
+
value={llmSettings.provider}
|
|
918
|
+
onChange={(e) =>
|
|
919
|
+
setLlmSettings((prev) => ({ ...prev, provider: e.target.value }))
|
|
920
|
+
}
|
|
921
|
+
options={[
|
|
922
|
+
{
|
|
923
|
+
value: 'lmstudio',
|
|
924
|
+
label: 'Local LLM Server (LM Studio, Ollama, etc.)',
|
|
925
|
+
},
|
|
926
|
+
{ value: 'anthropic', label: 'Anthropic Claude' },
|
|
927
|
+
{ value: 'openai', label: 'OpenAI' },
|
|
928
|
+
{ value: 'gemini', label: 'Google Gemini' },
|
|
929
|
+
]}
|
|
930
|
+
/>
|
|
931
|
+
</div>
|
|
932
|
+
|
|
933
|
+
{llmSettings.provider !== 'lmstudio' && (
|
|
934
|
+
<div>
|
|
935
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
936
|
+
API Key
|
|
937
|
+
</label>
|
|
938
|
+
<input
|
|
939
|
+
type="password"
|
|
940
|
+
className={STYLES.input}
|
|
941
|
+
value={llmSettings.apiKey}
|
|
942
|
+
onChange={(e) =>
|
|
943
|
+
setLlmSettings((prev) => ({ ...prev, apiKey: e.target.value }))
|
|
944
|
+
}
|
|
945
|
+
placeholder={`Enter ${llmSettings.provider} API key...`}
|
|
946
|
+
/>
|
|
947
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
948
|
+
{llmSettings.provider === 'anthropic' && (
|
|
949
|
+
<>
|
|
950
|
+
Get your key from{' '}
|
|
951
|
+
<a
|
|
952
|
+
href="https://console.anthropic.com/"
|
|
953
|
+
target="_blank"
|
|
954
|
+
rel="noopener noreferrer"
|
|
955
|
+
className="text-blue-600 dark:text-blue-400 hover:underline"
|
|
956
|
+
>
|
|
957
|
+
console.anthropic.com
|
|
958
|
+
</a>
|
|
959
|
+
</>
|
|
960
|
+
)}
|
|
961
|
+
{llmSettings.provider === 'openai' && (
|
|
962
|
+
<>
|
|
963
|
+
Get your key from{' '}
|
|
964
|
+
<a
|
|
965
|
+
href="https://platform.openai.com/api-keys"
|
|
966
|
+
target="_blank"
|
|
967
|
+
rel="noopener noreferrer"
|
|
968
|
+
className="text-blue-600 dark:text-blue-400 hover:underline"
|
|
969
|
+
>
|
|
970
|
+
platform.openai.com
|
|
971
|
+
</a>
|
|
972
|
+
</>
|
|
973
|
+
)}
|
|
974
|
+
{llmSettings.provider === 'gemini' && (
|
|
975
|
+
<>
|
|
976
|
+
Get your key from{' '}
|
|
977
|
+
<a
|
|
978
|
+
href="https://aistudio.google.com/app/apikey"
|
|
979
|
+
target="_blank"
|
|
980
|
+
rel="noopener noreferrer"
|
|
981
|
+
className="text-blue-600 dark:text-blue-400 hover:underline"
|
|
982
|
+
>
|
|
983
|
+
Google AI Studio
|
|
984
|
+
</a>
|
|
985
|
+
</>
|
|
986
|
+
)}
|
|
987
|
+
</p>
|
|
988
|
+
</div>
|
|
989
|
+
)}
|
|
990
|
+
|
|
991
|
+
{llmSettings.provider === 'lmstudio' && (
|
|
992
|
+
<div>
|
|
993
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
994
|
+
Server Base URL
|
|
995
|
+
</label>
|
|
996
|
+
<input
|
|
997
|
+
type="text"
|
|
998
|
+
className={STYLES.input}
|
|
999
|
+
value={llmSettings.baseUrl}
|
|
1000
|
+
onChange={(e) =>
|
|
1001
|
+
setLlmSettings((prev) => ({ ...prev, baseUrl: e.target.value }))
|
|
1002
|
+
}
|
|
1003
|
+
placeholder="http://localhost:1234/v1"
|
|
1004
|
+
/>
|
|
1005
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
1006
|
+
OpenAI-compatible API endpoint (LM Studio, Ollama, text-generation-webui,
|
|
1007
|
+
etc.)
|
|
1008
|
+
</p>
|
|
1009
|
+
</div>
|
|
1010
|
+
)}
|
|
1011
|
+
|
|
1012
|
+
<div className="flex gap-2">
|
|
1013
|
+
<button
|
|
1014
|
+
className={STYLES.btnSuccess}
|
|
1015
|
+
onClick={handleTestLLMConnection}
|
|
1016
|
+
disabled={llmTestResult?.testing}
|
|
1017
|
+
>
|
|
1018
|
+
{llmTestResult?.testing ? 'Testing...' : 'Test Connection'}
|
|
1019
|
+
</button>
|
|
1020
|
+
<button
|
|
1021
|
+
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
|
|
1022
|
+
onClick={handleSaveLLMSettings}
|
|
1023
|
+
>
|
|
1024
|
+
Save Settings
|
|
1025
|
+
</button>
|
|
1026
|
+
</div>
|
|
1027
|
+
|
|
1028
|
+
{llmTestResult && !llmTestResult.testing && (
|
|
1029
|
+
<div
|
|
1030
|
+
className={`px-4 py-2 rounded-lg ${
|
|
1031
|
+
llmTestResult.success
|
|
1032
|
+
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400'
|
|
1033
|
+
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400'
|
|
1034
|
+
}`}
|
|
1035
|
+
>
|
|
1036
|
+
{llmTestResult.success ? '✓' : '✗'}{' '}
|
|
1037
|
+
{llmTestResult.message || llmTestResult.error}
|
|
1038
|
+
</div>
|
|
1039
|
+
)}
|
|
1040
|
+
</>
|
|
1041
|
+
)}
|
|
1042
|
+
</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
)}
|
|
1046
|
+
|
|
1047
|
+
{/* Create Sub-tab */}
|
|
1048
|
+
{activeSubTab === 'create' && (
|
|
1049
|
+
<>
|
|
1050
|
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
|
1051
|
+
Create Stems+Karaoke File
|
|
1052
|
+
</h2>
|
|
1053
|
+
|
|
1054
|
+
{/* File Selection */}
|
|
1055
|
+
<div className={`${STYLES.card} mb-6`}>
|
|
1056
|
+
<h3 className={STYLES.sectionTitle}>1. Select Audio File</h3>
|
|
1057
|
+
|
|
1058
|
+
{fileLoading ? (
|
|
1059
|
+
<div className="flex items-center justify-center py-8">
|
|
1060
|
+
<Spinner size="sm" message="Reading file info & searching lyrics..." />
|
|
1061
|
+
</div>
|
|
1062
|
+
) : selectedFile ? (
|
|
1063
|
+
<div>
|
|
1064
|
+
<div className="flex items-center justify-between">
|
|
1065
|
+
<div className="flex-1 min-w-0">
|
|
1066
|
+
<p className="text-gray-900 dark:text-white font-medium truncate">
|
|
1067
|
+
{selectedFile.name}
|
|
1068
|
+
</p>
|
|
1069
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
1070
|
+
{formatDuration(selectedFile.duration)} •{' '}
|
|
1071
|
+
{selectedFile.codec?.toUpperCase() || 'Unknown'}{' '}
|
|
1072
|
+
{selectedFile.isVideo && '• Video'}
|
|
1073
|
+
</p>
|
|
1074
|
+
</div>
|
|
1075
|
+
<button
|
|
1076
|
+
className="ml-4 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors"
|
|
1077
|
+
onClick={handleSelectFile}
|
|
1078
|
+
>
|
|
1079
|
+
Change
|
|
1080
|
+
</button>
|
|
1081
|
+
</div>
|
|
1082
|
+
{/* Stem file detection indicator */}
|
|
1083
|
+
{selectedFile.hasStems && !selectedFile.hasLyrics && (
|
|
1084
|
+
<div className="mt-3 px-3 py-2 bg-purple-100 dark:bg-purple-900/30 border border-purple-300 dark:border-purple-700 rounded-lg">
|
|
1085
|
+
<p className="text-sm text-purple-700 dark:text-purple-300 font-medium">
|
|
1086
|
+
🎛️ Stem file detected ({selectedFile.audioStreamCount} tracks)
|
|
1087
|
+
</p>
|
|
1088
|
+
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1">
|
|
1089
|
+
Stems: {selectedFile.stemNames?.join(', ')} • Will add lyrics only (no stem
|
|
1090
|
+
separation needed)
|
|
1091
|
+
</p>
|
|
1092
|
+
</div>
|
|
1093
|
+
)}
|
|
1094
|
+
{selectedFile.hasStems && selectedFile.hasLyrics && (
|
|
1095
|
+
<div className="mt-3 px-3 py-2 bg-yellow-100 dark:bg-yellow-900/30 border border-yellow-300 dark:border-yellow-700 rounded-lg">
|
|
1096
|
+
<p className="text-sm text-yellow-700 dark:text-yellow-300 font-medium">
|
|
1097
|
+
⚠️ This file already has karaoke lyrics
|
|
1098
|
+
</p>
|
|
1099
|
+
<p className="text-xs text-yellow-600 dark:text-yellow-400 mt-1">
|
|
1100
|
+
Processing will replace existing lyrics with new transcription
|
|
1101
|
+
</p>
|
|
1102
|
+
</div>
|
|
1103
|
+
)}
|
|
1104
|
+
</div>
|
|
1105
|
+
) : (
|
|
1106
|
+
<button
|
|
1107
|
+
className="w-full px-6 py-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 transition-colors"
|
|
1108
|
+
onClick={handleSelectFile}
|
|
1109
|
+
>
|
|
1110
|
+
<div className="text-gray-600 dark:text-gray-400">
|
|
1111
|
+
<div className="text-3xl mb-2">🎵</div>
|
|
1112
|
+
<p>Click to select an audio or video file</p>
|
|
1113
|
+
<p className="text-sm mt-1">
|
|
1114
|
+
MP3, WAV, FLAC, OGG, M4A, MP4, MKV, AVI, MOV, WEBM
|
|
1115
|
+
</p>
|
|
1116
|
+
</div>
|
|
1117
|
+
</button>
|
|
1118
|
+
)}
|
|
1119
|
+
</div>
|
|
1120
|
+
|
|
1121
|
+
{/* Song Info */}
|
|
1122
|
+
<div className={`${STYLES.card} mb-6`}>
|
|
1123
|
+
<h3 className={STYLES.sectionTitle}>2. Song Information</h3>
|
|
1124
|
+
|
|
1125
|
+
{fileLoading ? (
|
|
1126
|
+
<div className="flex items-center justify-center py-8">
|
|
1127
|
+
<Spinner size="sm" message="Loading song metadata..." />
|
|
1128
|
+
</div>
|
|
1129
|
+
) : (
|
|
1130
|
+
<>
|
|
1131
|
+
<div className="grid grid-cols-3 gap-4 mb-4">
|
|
1132
|
+
<div>
|
|
1133
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
1134
|
+
Title
|
|
1135
|
+
</label>
|
|
1136
|
+
<input
|
|
1137
|
+
type="text"
|
|
1138
|
+
className={STYLES.input}
|
|
1139
|
+
value={options.title}
|
|
1140
|
+
onChange={(e) => setOptions((prev) => ({ ...prev, title: e.target.value }))}
|
|
1141
|
+
placeholder="Song title"
|
|
1142
|
+
/>
|
|
1143
|
+
</div>
|
|
1144
|
+
<div>
|
|
1145
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
1146
|
+
Artist
|
|
1147
|
+
</label>
|
|
1148
|
+
<input
|
|
1149
|
+
type="text"
|
|
1150
|
+
className={STYLES.input}
|
|
1151
|
+
value={options.artist}
|
|
1152
|
+
onChange={(e) =>
|
|
1153
|
+
setOptions((prev) => ({ ...prev, artist: e.target.value }))
|
|
1154
|
+
}
|
|
1155
|
+
placeholder="Artist name"
|
|
1156
|
+
/>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div>
|
|
1159
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
1160
|
+
Language
|
|
1161
|
+
</label>
|
|
1162
|
+
<PortalSelect
|
|
1163
|
+
value={options.language}
|
|
1164
|
+
onChange={(e) =>
|
|
1165
|
+
setOptions((prev) => ({ ...prev, language: e.target.value }))
|
|
1166
|
+
}
|
|
1167
|
+
options={[
|
|
1168
|
+
{ value: 'en', label: 'English' },
|
|
1169
|
+
{ value: 'es', label: 'Spanish' },
|
|
1170
|
+
{ value: 'fr', label: 'French' },
|
|
1171
|
+
{ value: 'de', label: 'German' },
|
|
1172
|
+
{ value: 'it', label: 'Italian' },
|
|
1173
|
+
{ value: 'pt', label: 'Portuguese' },
|
|
1174
|
+
{ value: 'ja', label: 'Japanese' },
|
|
1175
|
+
{ value: 'ko', label: 'Korean' },
|
|
1176
|
+
{ value: 'zh', label: 'Chinese' },
|
|
1177
|
+
]}
|
|
1178
|
+
/>
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
|
|
1182
|
+
<div>
|
|
1183
|
+
<div className="flex items-center justify-between mb-1">
|
|
1184
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
1185
|
+
Reference Lyrics (optional)
|
|
1186
|
+
</label>
|
|
1187
|
+
<button
|
|
1188
|
+
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
|
1189
|
+
onClick={handleSearchLyrics}
|
|
1190
|
+
>
|
|
1191
|
+
Search LRCLIB
|
|
1192
|
+
</button>
|
|
1193
|
+
</div>
|
|
1194
|
+
<textarea
|
|
1195
|
+
className={`${STYLES.input} h-24 resize-none`}
|
|
1196
|
+
value={options.referenceLyrics}
|
|
1197
|
+
onChange={(e) =>
|
|
1198
|
+
setOptions((prev) => ({ ...prev, referenceLyrics: e.target.value }))
|
|
1199
|
+
}
|
|
1200
|
+
placeholder="Paste lyrics here to improve transcription accuracy..."
|
|
1201
|
+
/>
|
|
1202
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
1203
|
+
Reference lyrics help Whisper recognize song-specific vocabulary
|
|
1204
|
+
</p>
|
|
1205
|
+
</div>
|
|
1206
|
+
</>
|
|
1207
|
+
)}
|
|
1208
|
+
</div>
|
|
1209
|
+
|
|
1210
|
+
{/* Create Button */}
|
|
1211
|
+
<div className="text-center">
|
|
1212
|
+
<button
|
|
1213
|
+
className={`px-8 py-4 ${
|
|
1214
|
+
selectedFile?.hasStems && !selectedFile?.hasLyrics
|
|
1215
|
+
? 'bg-purple-600 hover:bg-purple-700'
|
|
1216
|
+
: 'bg-blue-600 hover:bg-blue-700'
|
|
1217
|
+
} disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-bold text-lg rounded-lg transition-colors`}
|
|
1218
|
+
onClick={handleStartConversion}
|
|
1219
|
+
disabled={!selectedFile}
|
|
1220
|
+
>
|
|
1221
|
+
{selectedFile?.hasStems && !selectedFile?.hasLyrics
|
|
1222
|
+
? 'Add Lyrics to Stem File'
|
|
1223
|
+
: 'Create Stems+Karaoke File'}
|
|
1224
|
+
</button>
|
|
1225
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-3">
|
|
1226
|
+
{selectedFile?.hasStems && !selectedFile?.hasLyrics
|
|
1227
|
+
? 'Lyrics-only mode is much faster (typically under 1 minute)'
|
|
1228
|
+
: 'Processing time depends on song length and your hardware (typically 2-10 minutes)'}
|
|
1229
|
+
</p>
|
|
1230
|
+
</div>
|
|
1231
|
+
</>
|
|
1232
|
+
)}
|
|
1233
|
+
</div>
|
|
1234
|
+
</div>
|
|
1235
|
+
);
|
|
1236
|
+
}
|