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,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Musical Key Detection
|
|
3
|
+
* Detects the musical key from CREPE pitch data using Krumhansl-Schmuckler algorithm
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Note names for output
|
|
7
|
+
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
|
8
|
+
|
|
9
|
+
// Krumhansl-Schmuckler key profiles (normalized weights for each pitch class)
|
|
10
|
+
// These represent how often each scale degree appears in typical major/minor music
|
|
11
|
+
const MAJOR_PROFILE = [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88];
|
|
12
|
+
const MINOR_PROFILE = [6.33, 2.68, 3.52, 5.38, 2.6, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalize an array so values sum to 1
|
|
16
|
+
*/
|
|
17
|
+
function normalize(arr) {
|
|
18
|
+
const sum = arr.reduce((a, b) => a + b, 0);
|
|
19
|
+
if (sum === 0) return arr.map(() => 0);
|
|
20
|
+
return arr.map((v) => v / sum);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Calculate Pearson correlation coefficient between two arrays
|
|
25
|
+
*/
|
|
26
|
+
function correlation(arr1, arr2) {
|
|
27
|
+
const n = arr1.length;
|
|
28
|
+
if (n !== arr2.length || n === 0) return 0;
|
|
29
|
+
|
|
30
|
+
const mean1 = arr1.reduce((a, b) => a + b, 0) / n;
|
|
31
|
+
const mean2 = arr2.reduce((a, b) => a + b, 0) / n;
|
|
32
|
+
|
|
33
|
+
let numerator = 0;
|
|
34
|
+
let sum1Sq = 0;
|
|
35
|
+
let sum2Sq = 0;
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < n; i++) {
|
|
38
|
+
const diff1 = arr1[i] - mean1;
|
|
39
|
+
const diff2 = arr2[i] - mean2;
|
|
40
|
+
numerator += diff1 * diff2;
|
|
41
|
+
sum1Sq += diff1 * diff1;
|
|
42
|
+
sum2Sq += diff2 * diff2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const denominator = Math.sqrt(sum1Sq * sum2Sq);
|
|
46
|
+
if (denominator === 0) return 0;
|
|
47
|
+
|
|
48
|
+
return numerator / denominator;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Rotate array by n positions (for testing different root notes)
|
|
53
|
+
*/
|
|
54
|
+
function rotate(arr, n) {
|
|
55
|
+
const len = arr.length;
|
|
56
|
+
const shift = ((n % len) + len) % len;
|
|
57
|
+
return [...arr.slice(shift), ...arr.slice(0, shift)];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect musical key from CREPE pitch data
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} pitchData - CREPE output with pitch_data containing midi array and confidence
|
|
64
|
+
* @param {number} confidenceThreshold - Minimum confidence to include a pitch (0-1)
|
|
65
|
+
* @returns {Object} { key: 'C major', confidence: 0.85, pitchHistogram: [...] }
|
|
66
|
+
*/
|
|
67
|
+
export function detectKey(pitchData, confidenceThreshold = 0.7) {
|
|
68
|
+
if (!pitchData?.pitch_data) {
|
|
69
|
+
return { key: 'unknown', confidence: 0, method: 'no_data' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { midi, confidence } = pitchData.pitch_data;
|
|
73
|
+
|
|
74
|
+
if (!midi || !confidence || midi.length === 0) {
|
|
75
|
+
return { key: 'unknown', confidence: 0, method: 'no_pitch_data' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Build pitch class histogram (0-11) from valid pitches
|
|
79
|
+
const pitchHistogram = new Array(12).fill(0);
|
|
80
|
+
let validCount = 0;
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < midi.length; i++) {
|
|
83
|
+
const midiNote = midi[i];
|
|
84
|
+
const conf = confidence[i];
|
|
85
|
+
|
|
86
|
+
// Skip low confidence or invalid pitches
|
|
87
|
+
if (conf < confidenceThreshold || midiNote <= 0 || !isFinite(midiNote)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get pitch class (0-11) from MIDI note
|
|
92
|
+
const pitchClass = Math.round(midiNote) % 12;
|
|
93
|
+
pitchHistogram[pitchClass]++;
|
|
94
|
+
validCount++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (validCount < 10) {
|
|
98
|
+
return { key: 'unknown', confidence: 0, method: 'insufficient_data' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Normalize histogram and profiles
|
|
102
|
+
const normalizedHistogram = normalize(pitchHistogram);
|
|
103
|
+
const normalizedMajor = normalize(MAJOR_PROFILE);
|
|
104
|
+
const normalizedMinor = normalize(MINOR_PROFILE);
|
|
105
|
+
|
|
106
|
+
// Test all 24 keys (12 major + 12 minor) and find best correlation
|
|
107
|
+
let bestKey = 'C';
|
|
108
|
+
let bestMode = 'major';
|
|
109
|
+
let bestCorrelation = -1;
|
|
110
|
+
|
|
111
|
+
for (let root = 0; root < 12; root++) {
|
|
112
|
+
// Test major key with this root
|
|
113
|
+
const shiftedMajor = rotate(normalizedMajor, root);
|
|
114
|
+
const corrMajor = correlation(normalizedHistogram, shiftedMajor);
|
|
115
|
+
|
|
116
|
+
if (corrMajor > bestCorrelation) {
|
|
117
|
+
bestCorrelation = corrMajor;
|
|
118
|
+
bestKey = NOTE_NAMES[root];
|
|
119
|
+
bestMode = 'major';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Test minor key with this root
|
|
123
|
+
const shiftedMinor = rotate(normalizedMinor, root);
|
|
124
|
+
const corrMinor = correlation(normalizedHistogram, shiftedMinor);
|
|
125
|
+
|
|
126
|
+
if (corrMinor > bestCorrelation) {
|
|
127
|
+
bestCorrelation = corrMinor;
|
|
128
|
+
bestKey = NOTE_NAMES[root];
|
|
129
|
+
bestMode = 'minor';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const keyString = bestCorrelation > 0 ? `${bestKey} ${bestMode}` : 'unknown';
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
key: keyString,
|
|
137
|
+
confidence: Math.max(0, bestCorrelation),
|
|
138
|
+
method: 'krumhansl_schmuckler',
|
|
139
|
+
pitchHistogram: pitchHistogram,
|
|
140
|
+
validPitchCount: validCount,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Convert key string to Camelot notation (used by DJs)
|
|
146
|
+
* e.g., "C major" -> "8B", "A minor" -> "8A"
|
|
147
|
+
*/
|
|
148
|
+
export function keyToCamelot(keyString) {
|
|
149
|
+
const camelotMap = {
|
|
150
|
+
'C major': '8B',
|
|
151
|
+
'G major': '9B',
|
|
152
|
+
'D major': '10B',
|
|
153
|
+
'A major': '11B',
|
|
154
|
+
'E major': '12B',
|
|
155
|
+
'B major': '1B',
|
|
156
|
+
'F# major': '2B',
|
|
157
|
+
'C# major': '3B',
|
|
158
|
+
'G# major': '4B',
|
|
159
|
+
'D# major': '5B',
|
|
160
|
+
'A# major': '6B',
|
|
161
|
+
'F major': '7B',
|
|
162
|
+
'A minor': '8A',
|
|
163
|
+
'E minor': '9A',
|
|
164
|
+
'B minor': '10A',
|
|
165
|
+
'F# minor': '11A',
|
|
166
|
+
'C# minor': '12A',
|
|
167
|
+
'G# minor': '1A',
|
|
168
|
+
'D# minor': '2A',
|
|
169
|
+
'A# minor': '3A',
|
|
170
|
+
'F minor': '4A',
|
|
171
|
+
'C minor': '5A',
|
|
172
|
+
'G minor': '6A',
|
|
173
|
+
'D minor': '7A',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return camelotMap[keyString] || '';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Convert key string to Open Key notation (alternative to Camelot)
|
|
181
|
+
* e.g., "C major" -> "1d", "A minor" -> "1m"
|
|
182
|
+
*/
|
|
183
|
+
export function keyToOpenKey(keyString) {
|
|
184
|
+
const openKeyMap = {
|
|
185
|
+
'C major': '1d',
|
|
186
|
+
'G major': '2d',
|
|
187
|
+
'D major': '3d',
|
|
188
|
+
'A major': '4d',
|
|
189
|
+
'E major': '5d',
|
|
190
|
+
'B major': '6d',
|
|
191
|
+
'F# major': '7d',
|
|
192
|
+
'C# major': '8d',
|
|
193
|
+
'G# major': '9d',
|
|
194
|
+
'D# major': '10d',
|
|
195
|
+
'A# major': '11d',
|
|
196
|
+
'F major': '12d',
|
|
197
|
+
'A minor': '1m',
|
|
198
|
+
'E minor': '2m',
|
|
199
|
+
'B minor': '3m',
|
|
200
|
+
'F# minor': '4m',
|
|
201
|
+
'C# minor': '5m',
|
|
202
|
+
'G# minor': '6m',
|
|
203
|
+
'D# minor': '7m',
|
|
204
|
+
'A# minor': '8m',
|
|
205
|
+
'F minor': '9m',
|
|
206
|
+
'C minor': '10m',
|
|
207
|
+
'G minor': '11m',
|
|
208
|
+
'D minor': '12m',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return openKeyMap[keyString] || '';
|
|
212
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Service for lyrics correction
|
|
3
|
+
* Supports OpenAI, Anthropic Claude, Google Gemini, and local LM Studio
|
|
4
|
+
* Pure Node.js implementation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
8
|
+
import OpenAI from 'openai';
|
|
9
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
10
|
+
import { LLM_DEFAULTS } from '../../shared/defaults.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get LLM provider instance based on settings
|
|
14
|
+
*/
|
|
15
|
+
function getLLMProvider(settings) {
|
|
16
|
+
const { provider, apiKey, baseUrl } = settings;
|
|
17
|
+
|
|
18
|
+
switch (provider) {
|
|
19
|
+
case 'anthropic':
|
|
20
|
+
if (!apiKey) throw new Error('Anthropic API key required');
|
|
21
|
+
return new Anthropic({ apiKey });
|
|
22
|
+
|
|
23
|
+
case 'openai':
|
|
24
|
+
if (!apiKey) throw new Error('OpenAI API key required');
|
|
25
|
+
return new OpenAI({ apiKey });
|
|
26
|
+
|
|
27
|
+
case 'gemini':
|
|
28
|
+
if (!apiKey) throw new Error('Gemini API key required');
|
|
29
|
+
return new GoogleGenerativeAI(apiKey);
|
|
30
|
+
|
|
31
|
+
case 'lmstudio':
|
|
32
|
+
// LM Studio uses OpenAI-compatible API
|
|
33
|
+
return new OpenAI({
|
|
34
|
+
apiKey: 'lm-studio', // dummy key
|
|
35
|
+
baseURL: baseUrl || 'http://localhost:1234/v1',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`Unknown LLM provider: ${provider}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Correct lyrics using LLM
|
|
45
|
+
* @param {Object} whisperOutput - Output from Whisper (words array)
|
|
46
|
+
* @param {string} referenceLyrics - Reference lyrics from LRCLIB
|
|
47
|
+
* @param {Object} settings - LLM settings (provider, model, apiKey, etc.)
|
|
48
|
+
* @returns {Object} Corrected lyrics with same structure as Whisper output
|
|
49
|
+
*/
|
|
50
|
+
export async function correctLyrics(whisperOutput, referenceLyrics, settings) {
|
|
51
|
+
if (!settings.enabled) {
|
|
52
|
+
console.log('🤖 LLM correction disabled, using Whisper output as-is');
|
|
53
|
+
return {
|
|
54
|
+
output: whisperOutput,
|
|
55
|
+
stats: null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!referenceLyrics || !referenceLyrics.trim()) {
|
|
60
|
+
console.log('🤖 No reference lyrics provided, skipping LLM correction');
|
|
61
|
+
return {
|
|
62
|
+
output: whisperOutput,
|
|
63
|
+
stats: null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`🤖 Starting LLM lyrics correction with ${settings.provider}...`);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const provider = getLLMProvider(settings);
|
|
71
|
+
const llmResponse = await callLLM(provider, settings, whisperOutput, referenceLyrics);
|
|
72
|
+
|
|
73
|
+
// Parse JSON response and apply corrections
|
|
74
|
+
const {
|
|
75
|
+
lines: correctedLines,
|
|
76
|
+
corrections,
|
|
77
|
+
missingLines,
|
|
78
|
+
} = parseCorrection(llmResponse, whisperOutput);
|
|
79
|
+
|
|
80
|
+
console.log(`✅ LLM correction complete (${corrections.length} lines changed)`);
|
|
81
|
+
if (missingLines.length > 0) {
|
|
82
|
+
console.log(`📝 LLM suggested ${missingLines.length} missing lines`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
output: {
|
|
87
|
+
...whisperOutput,
|
|
88
|
+
lines: correctedLines,
|
|
89
|
+
},
|
|
90
|
+
stats: {
|
|
91
|
+
corrections_applied: corrections.length,
|
|
92
|
+
suggestions_made: corrections.length + missingLines.length, // Total suggestions (applied + not applied)
|
|
93
|
+
corrections_rejected: 0, // We auto-apply all matching corrections
|
|
94
|
+
missing_lines_suggested: missingLines.length,
|
|
95
|
+
corrections: corrections, // Detailed correction list (applied)
|
|
96
|
+
missing_lines: missingLines, // Detailed missing lines list (suggestions not applied)
|
|
97
|
+
failed: false,
|
|
98
|
+
provider: settings.provider,
|
|
99
|
+
model: settings.model,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('❌ LLM correction failed:', error.message);
|
|
104
|
+
console.log('⚠️ Falling back to original Whisper output');
|
|
105
|
+
return {
|
|
106
|
+
output: whisperOutput,
|
|
107
|
+
stats: {
|
|
108
|
+
corrections_applied: 0,
|
|
109
|
+
suggestions_made: 0,
|
|
110
|
+
corrections_rejected: 0,
|
|
111
|
+
missing_lines_suggested: 0,
|
|
112
|
+
corrections: [],
|
|
113
|
+
missing_lines: [],
|
|
114
|
+
failed: true,
|
|
115
|
+
error: error.message,
|
|
116
|
+
provider: settings.provider,
|
|
117
|
+
model: settings.model,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Call LLM API with lyrics correction prompt
|
|
125
|
+
*/
|
|
126
|
+
async function callLLM(provider, settings, whisperOutput, referenceLyrics) {
|
|
127
|
+
const { provider: providerType, model } = settings;
|
|
128
|
+
|
|
129
|
+
// Build structured line data for LLM (work with lines, not words)
|
|
130
|
+
const lineData = whisperOutput.lines.map((line, i) => ({
|
|
131
|
+
line_num: i + 1,
|
|
132
|
+
text: line.text || '',
|
|
133
|
+
start: line.start,
|
|
134
|
+
end: line.end,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Build prompt
|
|
138
|
+
const systemPrompt = `You are an automated speech recognition (ASR) error correction specialist. You fix technical errors from speech-to-text systems while preserving the original transcription structure. Return ONLY valid JSON.`;
|
|
139
|
+
|
|
140
|
+
const userPrompt = `AUTOMATED SPEECH RECOGNITION (ASR) ERROR CORRECTION TASK
|
|
141
|
+
|
|
142
|
+
CONTEXT: You are correcting errors from Whisper AI that transcribed sung vocals. The system sometimes mishears words due to singing pronunciation, background music, and audio quality.
|
|
143
|
+
|
|
144
|
+
YOUR TASK: Fix ONLY obvious speech recognition errors where the ASR clearly misheard spoken/sung words.
|
|
145
|
+
|
|
146
|
+
REFERENCE TEXT (for identifying ASR mishearings - DO NOT copy verbatim):
|
|
147
|
+
${referenceLyrics}
|
|
148
|
+
|
|
149
|
+
ASR OUTPUT TO CORRECT (song lines with timing):
|
|
150
|
+
${JSON.stringify(lineData, null, 2)}
|
|
151
|
+
|
|
152
|
+
CRITICAL RULES:
|
|
153
|
+
1. ONLY correct obvious phonetic mishearings where automated speech recognition failed
|
|
154
|
+
2. DO NOT substitute entire phrases even if they don't match the reference
|
|
155
|
+
3. ONLY fix clear technical errors (e.g., "foamy" → "for me", "sancti" → "sanity")
|
|
156
|
+
4. When in doubt, DO NOT fix - leave the line unchanged
|
|
157
|
+
5. Return ONLY valid JSON, no markdown, no explanations
|
|
158
|
+
|
|
159
|
+
RESPONSE FORMAT (MUST BE VALID JSON):
|
|
160
|
+
{
|
|
161
|
+
"corrections": [
|
|
162
|
+
{"line_num": 1, "old_text": "original line", "new_text": "corrected line"}
|
|
163
|
+
],
|
|
164
|
+
"missing_lines": [
|
|
165
|
+
{
|
|
166
|
+
"suggested_text": "Text of missing line from reference",
|
|
167
|
+
"start": 15.5,
|
|
168
|
+
"end": 19.5,
|
|
169
|
+
"confidence": "high|medium|low",
|
|
170
|
+
"reason": "Why this line is likely missing (e.g., 'Large gap in timing', 'Reference shows chorus missing')"
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
IMPORTANT ABOUT MISSING LINES:
|
|
176
|
+
- ONLY suggest missing lines if you have strong evidence:
|
|
177
|
+
1. Large timing gaps between transcribed lines (>4 seconds)
|
|
178
|
+
2. Reference lyrics having content that clearly fits in gaps
|
|
179
|
+
- Set confidence based on evidence strength
|
|
180
|
+
- DO NOT invent lyrics - only use what's in the reference
|
|
181
|
+
- Return ONLY valid JSON, no markdown blocks, no additional text`;
|
|
182
|
+
|
|
183
|
+
// Call appropriate API
|
|
184
|
+
if (providerType === 'anthropic') {
|
|
185
|
+
const response = await provider.messages.create({
|
|
186
|
+
model: model || 'claude-3-5-sonnet-20241022',
|
|
187
|
+
max_tokens: 4096,
|
|
188
|
+
temperature: 0.1,
|
|
189
|
+
system: systemPrompt,
|
|
190
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
191
|
+
});
|
|
192
|
+
return response.content[0].text;
|
|
193
|
+
} else if (providerType === 'openai' || providerType === 'lmstudio') {
|
|
194
|
+
const response = await provider.chat.completions.create({
|
|
195
|
+
model: model || 'gpt-4o',
|
|
196
|
+
temperature: 0.1,
|
|
197
|
+
max_tokens: 16384,
|
|
198
|
+
messages: [
|
|
199
|
+
{ role: 'system', content: systemPrompt },
|
|
200
|
+
{ role: 'user', content: userPrompt },
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
return response.choices[0].message.content;
|
|
204
|
+
} else if (providerType === 'gemini') {
|
|
205
|
+
const genModel = provider.getGenerativeModel({
|
|
206
|
+
model: model || 'gemini-2.0-flash-exp',
|
|
207
|
+
});
|
|
208
|
+
const result = await genModel.generateContent({
|
|
209
|
+
contents: [{ role: 'user', parts: [{ text: `${systemPrompt}\n\n${userPrompt}` }] }],
|
|
210
|
+
generationConfig: {
|
|
211
|
+
temperature: 0.1,
|
|
212
|
+
maxOutputTokens: 4000,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
return result.response.text();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
throw new Error(`Unsupported provider: ${providerType}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parse LLM JSON response and apply corrections to lines
|
|
223
|
+
* Preserves timing information from original Whisper output
|
|
224
|
+
* @returns {Object} { lines: correctedLines[], corrections: [], missingLines: [] }
|
|
225
|
+
*/
|
|
226
|
+
function parseCorrection(llmResponse, originalOutput) {
|
|
227
|
+
// Parse JSON response
|
|
228
|
+
let responseData;
|
|
229
|
+
try {
|
|
230
|
+
// Try to parse as-is
|
|
231
|
+
responseData = JSON.parse(llmResponse);
|
|
232
|
+
} catch {
|
|
233
|
+
// Try to extract JSON from markdown blocks
|
|
234
|
+
let cleaned = llmResponse.trim();
|
|
235
|
+
if (cleaned.includes('```json')) {
|
|
236
|
+
cleaned = cleaned.split('```json')[1].split('```')[0];
|
|
237
|
+
} else if (cleaned.includes('```')) {
|
|
238
|
+
cleaned = cleaned.split('```')[1].split('```')[0];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Try to extract just the JSON object
|
|
242
|
+
const start = cleaned.indexOf('{');
|
|
243
|
+
const end = cleaned.lastIndexOf('}');
|
|
244
|
+
if (start !== -1 && end !== -1) {
|
|
245
|
+
cleaned = cleaned.substring(start, end + 1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
responseData = JSON.parse(cleaned);
|
|
250
|
+
} catch (e2) {
|
|
251
|
+
console.error('Failed to parse LLM JSON response:', e2);
|
|
252
|
+
console.error('Response:', llmResponse.substring(0, 500));
|
|
253
|
+
throw new Error(`Invalid JSON response from LLM: ${e2.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const corrections = responseData.corrections || [];
|
|
258
|
+
const missingLines = responseData.missing_lines || [];
|
|
259
|
+
|
|
260
|
+
// Apply corrections to lines
|
|
261
|
+
const originalLines = originalOutput.lines || [];
|
|
262
|
+
const correctedLines = [...originalLines];
|
|
263
|
+
const appliedCorrections = [];
|
|
264
|
+
|
|
265
|
+
for (const correction of corrections) {
|
|
266
|
+
const lineNum = correction.line_num;
|
|
267
|
+
const oldText = correction.old_text || '';
|
|
268
|
+
const newText = correction.new_text || '';
|
|
269
|
+
|
|
270
|
+
const lineIdx = lineNum - 1;
|
|
271
|
+
if (lineIdx >= 0 && lineIdx < correctedLines.length) {
|
|
272
|
+
const originalLine = originalLines[lineIdx];
|
|
273
|
+
const originalText = originalLine.text || '';
|
|
274
|
+
|
|
275
|
+
// Only apply if old_text matches exactly
|
|
276
|
+
if (oldText === originalText && newText && newText !== originalText) {
|
|
277
|
+
correctedLines[lineIdx] = {
|
|
278
|
+
...originalLine,
|
|
279
|
+
text: newText,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
appliedCorrections.push({
|
|
283
|
+
line_num: lineNum,
|
|
284
|
+
old_text: oldText,
|
|
285
|
+
new_text: newText,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
lines: correctedLines,
|
|
293
|
+
corrections: appliedCorrections,
|
|
294
|
+
missingLines: missingLines,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get LLM settings from app settings
|
|
300
|
+
* Uses unified defaults from shared/defaults.js
|
|
301
|
+
*/
|
|
302
|
+
export function getLLMSettings(settingsManager) {
|
|
303
|
+
const llmConfig = settingsManager.get('creator.llm', {});
|
|
304
|
+
const apiKey = llmConfig.apiKey || LLM_DEFAULTS.apiKey;
|
|
305
|
+
|
|
306
|
+
// SECURITY FIX (#25): Mask API key - only show last 4 chars to renderer
|
|
307
|
+
const maskedApiKey = apiKey && apiKey.length > 8 ? `${'•'.repeat(apiKey.length - 4)}${apiKey.slice(-4)}` : apiKey ? '••••••••' : '';
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
enabled: llmConfig.enabled ?? LLM_DEFAULTS.enabled,
|
|
311
|
+
provider: llmConfig.provider || LLM_DEFAULTS.provider,
|
|
312
|
+
model: llmConfig.model || getDefaultModel(llmConfig.provider),
|
|
313
|
+
apiKey: maskedApiKey,
|
|
314
|
+
hasApiKey: Boolean(apiKey), // Let renderer know if key is set
|
|
315
|
+
baseUrl: llmConfig.baseUrl || LLM_DEFAULTS.baseUrl,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Save LLM settings
|
|
321
|
+
*/
|
|
322
|
+
export function saveLLMSettings(settingsManager, llmSettings) {
|
|
323
|
+
settingsManager.set('creator.llm', llmSettings);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get default model for a provider
|
|
328
|
+
*/
|
|
329
|
+
function getDefaultModel(provider) {
|
|
330
|
+
const defaults = {
|
|
331
|
+
anthropic: 'claude-3-5-sonnet-20241022',
|
|
332
|
+
openai: 'gpt-4o',
|
|
333
|
+
gemini: 'gemini-2.0-flash-exp',
|
|
334
|
+
lmstudio: 'local-model',
|
|
335
|
+
};
|
|
336
|
+
return defaults[provider] || 'gpt-4o';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Test LLM connection
|
|
341
|
+
*/
|
|
342
|
+
export async function testLLMConnection(settings) {
|
|
343
|
+
try {
|
|
344
|
+
const provider = getLLMProvider(settings);
|
|
345
|
+
|
|
346
|
+
// Send simple test message
|
|
347
|
+
const testPrompt = 'Reply with just the word "OK"';
|
|
348
|
+
|
|
349
|
+
if (settings.provider === 'anthropic') {
|
|
350
|
+
await provider.messages.create({
|
|
351
|
+
model: settings.model,
|
|
352
|
+
max_tokens: 10,
|
|
353
|
+
messages: [{ role: 'user', content: testPrompt }],
|
|
354
|
+
});
|
|
355
|
+
} else if (settings.provider === 'openai' || settings.provider === 'lmstudio') {
|
|
356
|
+
await provider.chat.completions.create({
|
|
357
|
+
model: settings.model,
|
|
358
|
+
max_tokens: 10,
|
|
359
|
+
messages: [{ role: 'user', content: testPrompt }],
|
|
360
|
+
});
|
|
361
|
+
} else if (settings.provider === 'gemini') {
|
|
362
|
+
const genModel = provider.getGenerativeModel({ model: settings.model });
|
|
363
|
+
await genModel.generateContent(testPrompt);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { success: true };
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return { success: false, error: error.message };
|
|
369
|
+
}
|
|
370
|
+
}
|