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.
Files changed (290) hide show
  1. package/README.md +558 -0
  2. package/bin/loukai.js +32 -0
  3. package/package.json +243 -0
  4. package/src/main/appState.js +250 -0
  5. package/src/main/audioEngine.js +478 -0
  6. package/src/main/creator/conversionService.js +503 -0
  7. package/src/main/creator/downloadManager.js +1128 -0
  8. package/src/main/creator/ffmpegService.js +487 -0
  9. package/src/main/creator/installLogger.js +51 -0
  10. package/src/main/creator/keyDetection.js +212 -0
  11. package/src/main/creator/llmService.js +370 -0
  12. package/src/main/creator/lrclibService.js +340 -0
  13. package/src/main/creator/python/crepe_runner.py +189 -0
  14. package/src/main/creator/python/demucs_runner.py +158 -0
  15. package/src/main/creator/python/whisper_runner.py +172 -0
  16. package/src/main/creator/pythonRunner.js +268 -0
  17. package/src/main/creator/stemBuilder.js +491 -0
  18. package/src/main/creator/systemChecker.js +474 -0
  19. package/src/main/handlers/appHandlers.js +45 -0
  20. package/src/main/handlers/audioHandlers.js +33 -0
  21. package/src/main/handlers/autotuneHandlers.js +28 -0
  22. package/src/main/handlers/canvasHandlers.js +84 -0
  23. package/src/main/handlers/creatorHandlers.js +159 -0
  24. package/src/main/handlers/editorHandlers.js +98 -0
  25. package/src/main/handlers/effectsHandlers.js +100 -0
  26. package/src/main/handlers/fileHandlers.js +45 -0
  27. package/src/main/handlers/index.js +78 -0
  28. package/src/main/handlers/libraryHandlers.js +96 -0
  29. package/src/main/handlers/mixerHandlers.js +64 -0
  30. package/src/main/handlers/playerHandlers.js +39 -0
  31. package/src/main/handlers/preferencesHandlers.js +46 -0
  32. package/src/main/handlers/queueHandlers.js +81 -0
  33. package/src/main/handlers/rendererHandlers.js +63 -0
  34. package/src/main/handlers/settingsHandlers.js +42 -0
  35. package/src/main/handlers/webServerHandlers.js +105 -0
  36. package/src/main/main.js +2351 -0
  37. package/src/main/preload.js +252 -0
  38. package/src/main/settingsManager.js +139 -0
  39. package/src/main/statePersistence.js +193 -0
  40. package/src/main/utils/pathValidator.js +112 -0
  41. package/src/main/webServer.js +2535 -0
  42. package/src/native/autotune.js +417 -0
  43. package/src/renderer/adapters/ElectronBridge.js +677 -0
  44. package/src/renderer/canvas.html +80 -0
  45. package/src/renderer/components/App.jsx +303 -0
  46. package/src/renderer/components/AppRoot.jsx +37 -0
  47. package/src/renderer/components/AudioDeviceSettings.jsx +145 -0
  48. package/src/renderer/components/EffectsPanelWrapper.jsx +267 -0
  49. package/src/renderer/components/MixerTab.jsx +233 -0
  50. package/src/renderer/components/MixerTabWrapper.jsx +31 -0
  51. package/src/renderer/components/PortalSelect.jsx +239 -0
  52. package/src/renderer/components/QueueTab.jsx +116 -0
  53. package/src/renderer/components/RequestsListWrapper.jsx +78 -0
  54. package/src/renderer/components/ServerTab.jsx +472 -0
  55. package/src/renderer/components/SongInfoBarWrapper.jsx +77 -0
  56. package/src/renderer/components/StatusBar.jsx +92 -0
  57. package/src/renderer/components/TabNavigation.jsx +77 -0
  58. package/src/renderer/components/TransportControlsWrapper.jsx +69 -0
  59. package/src/renderer/components/creator/CreateTab.jsx +1236 -0
  60. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js +2 -0
  61. package/src/renderer/dist/assets/kaiPlayer-CoMx__a_.js.map +1 -0
  62. package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js +2 -0
  63. package/src/renderer/dist/assets/microphoneEngine-BaCUhhQc.js.map +1 -0
  64. package/src/renderer/dist/assets/player-DVrqp7N5.js +3 -0
  65. package/src/renderer/dist/assets/player-DVrqp7N5.js.map +1 -0
  66. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js +2 -0
  67. package/src/renderer/dist/assets/songLoaders-BaTgGib4.js.map +1 -0
  68. package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js +2 -0
  69. package/src/renderer/dist/assets/webrtcManager-BhCHWceK.js.map +1 -0
  70. package/src/renderer/dist/js/autoTuneWorklet.js +224 -0
  71. package/src/renderer/dist/js/micPitchDetectorWorklet.js +137 -0
  72. package/src/renderer/dist/js/musicAnalysisWorklet.js +216 -0
  73. package/src/renderer/dist/js/phaseVocoderWorklet.js +341 -0
  74. package/src/renderer/dist/js/soundtouch-worklet.js +1395 -0
  75. package/src/renderer/dist/renderer.css +1 -0
  76. package/src/renderer/dist/renderer.js +62 -0
  77. package/src/renderer/dist/renderer.js.map +1 -0
  78. package/src/renderer/dist/renderer.woff2 +0 -0
  79. package/src/renderer/hooks/useKeyboardShortcuts.js +154 -0
  80. package/src/renderer/index.html +24 -0
  81. package/src/renderer/index.html.backup +372 -0
  82. package/src/renderer/js/PlayerInterface.js +267 -0
  83. package/src/renderer/js/autoTuneWorklet.js +224 -0
  84. package/src/renderer/js/butterchurnVerify.js +46 -0
  85. package/src/renderer/js/canvas-app.js +114 -0
  86. package/src/renderer/js/cdgPlayer.js +685 -0
  87. package/src/renderer/js/kaiPlayer.js +1200 -0
  88. package/src/renderer/js/karaokeRenderer.js +3392 -0
  89. package/src/renderer/js/micPitchDetectorWorklet.js +137 -0
  90. package/src/renderer/js/microphoneEngine.js +656 -0
  91. package/src/renderer/js/musicAnalysisWorklet.js +216 -0
  92. package/src/renderer/js/phaseVocoderWorklet.js +341 -0
  93. package/src/renderer/js/player.js +232 -0
  94. package/src/renderer/js/referencePitchTracker.js +130 -0
  95. package/src/renderer/js/songLoaders.js +334 -0
  96. package/src/renderer/js/soundtouch-worklet.js +1395 -0
  97. package/src/renderer/js/webrtcManager.js +511 -0
  98. package/src/renderer/lib/butterchurn.min.js +6739 -0
  99. package/src/renderer/lib/butterchurnPresets.min.js +1 -0
  100. package/src/renderer/lib/cdgraphics-wrapper.js +16 -0
  101. package/src/renderer/lib/cdgraphics.js +299 -0
  102. package/src/renderer/public/js/autoTuneWorklet.js +224 -0
  103. package/src/renderer/public/js/micPitchDetectorWorklet.js +137 -0
  104. package/src/renderer/public/js/musicAnalysisWorklet.js +216 -0
  105. package/src/renderer/public/js/phaseVocoderWorklet.js +341 -0
  106. package/src/renderer/public/js/soundtouch-worklet.js +1395 -0
  107. package/src/renderer/react-entry.jsx +44 -0
  108. package/src/renderer/styles/tailwind.css +106 -0
  109. package/src/renderer/utils/qrCodeGenerator.js +98 -0
  110. package/src/renderer/vite.config.js +31 -0
  111. package/src/shared/adapters/BridgeInterface.js +195 -0
  112. package/src/shared/components/EffectsPanel.jsx +177 -0
  113. package/src/shared/components/LibraryPanel.jsx +701 -0
  114. package/src/shared/components/LineDetailCanvas.jsx +167 -0
  115. package/src/shared/components/LyricLine.jsx +505 -0
  116. package/src/shared/components/LyricRejection.jsx +84 -0
  117. package/src/shared/components/LyricSuggestion.jsx +80 -0
  118. package/src/shared/components/LyricsEditorCanvas.jsx +271 -0
  119. package/src/shared/components/MixerPanel.jsx +94 -0
  120. package/src/shared/components/PlayerControls.jsx +206 -0
  121. package/src/shared/components/PortalSelect.jsx +239 -0
  122. package/src/shared/components/QueueList.jsx +365 -0
  123. package/src/shared/components/QuickSearch.jsx +126 -0
  124. package/src/shared/components/RequestsList.jsx +121 -0
  125. package/src/shared/components/SongEditor.jsx +1362 -0
  126. package/src/shared/components/SongInfoBar.jsx +81 -0
  127. package/src/shared/components/ThemeToggle.jsx +106 -0
  128. package/src/shared/components/Toast.jsx +30 -0
  129. package/src/shared/components/VisualizationSettings.jsx +243 -0
  130. package/src/shared/constants.js +95 -0
  131. package/src/shared/context/BridgeContext.jsx +32 -0
  132. package/src/shared/contexts/AudioContext.jsx +37 -0
  133. package/src/shared/contexts/PlayerContext.jsx +66 -0
  134. package/src/shared/contexts/SettingsContext.jsx +50 -0
  135. package/src/shared/defaults.js +158 -0
  136. package/src/shared/formatUtils.js +59 -0
  137. package/src/shared/formatUtils.test.js +207 -0
  138. package/src/shared/hooks/useAppState.js +97 -0
  139. package/src/shared/hooks/useAudioEngine.js +264 -0
  140. package/src/shared/hooks/usePlayer.js +89 -0
  141. package/src/shared/hooks/useSettingsPersistence.js +74 -0
  142. package/src/shared/hooks/useWebRTC.js +118 -0
  143. package/src/shared/ipcContracts.js +299 -0
  144. package/src/shared/package.json +3 -0
  145. package/src/shared/services/creatorService.js +373 -0
  146. package/src/shared/services/creatorService.test.js +413 -0
  147. package/src/shared/services/editorService.js +213 -0
  148. package/src/shared/services/editorService.test.js +219 -0
  149. package/src/shared/services/effectsService.js +271 -0
  150. package/src/shared/services/effectsService.test.js +418 -0
  151. package/src/shared/services/libraryService.js +438 -0
  152. package/src/shared/services/libraryService.test.js +474 -0
  153. package/src/shared/services/mixerService.js +172 -0
  154. package/src/shared/services/mixerService.test.js +399 -0
  155. package/src/shared/services/playerService.js +221 -0
  156. package/src/shared/services/playerService.test.js +357 -0
  157. package/src/shared/services/preferencesService.js +219 -0
  158. package/src/shared/services/queueService.js +226 -0
  159. package/src/shared/services/queueService.test.js +430 -0
  160. package/src/shared/services/requestsService.js +155 -0
  161. package/src/shared/services/requestsService.test.js +362 -0
  162. package/src/shared/services/serverSettingsService.js +151 -0
  163. package/src/shared/services/settingsService.js +257 -0
  164. package/src/shared/services/settingsService.test.js +295 -0
  165. package/src/shared/state/StateManager.js +263 -0
  166. package/src/shared/utils/audio.js +42 -0
  167. package/src/shared/utils/format.js +32 -0
  168. package/src/shared/utils/lyricsUtils.js +162 -0
  169. package/src/test/setup.js +40 -0
  170. package/src/utils/cdgLoader.js +180 -0
  171. package/src/utils/m4aLoader.js +333 -0
  172. package/src/web/App.jsx +578 -0
  173. package/src/web/adapters/WebBridge.js +428 -0
  174. package/src/web/components/PlayerSettingsPanel.jsx +231 -0
  175. package/src/web/components/SongSearch.jsx +180 -0
  176. package/src/web/dist/assets/index-0H-RnRrV.js +51 -0
  177. package/src/web/dist/assets/index-0H-RnRrV.js.map +1 -0
  178. package/src/web/dist/assets/index-DYW2zB0u.css +1 -0
  179. package/src/web/dist/index.html +15 -0
  180. package/src/web/index.html +14 -0
  181. package/src/web/main.jsx +10 -0
  182. package/src/web/package-lock.json +1765 -0
  183. package/src/web/pages/SongRequestPage.jsx +619 -0
  184. package/src/web/styles/tailwind.css +68 -0
  185. package/src/web/vite.config.js +27 -0
  186. package/static/fonts/material-icons.woff2 +0 -0
  187. package/static/images/butterchurn-screenshots/Aderrasi - Potion of Spirits.png +0 -0
  188. package/static/images/butterchurn-screenshots/Aderrasi - Songflower _Moss Posy_.png +0 -0
  189. package/static/images/butterchurn-screenshots/Aderrasi - Storm of the Eye _Thunder_ - mash0000 - quasi pseudo meta concentrics.png +0 -0
  190. package/static/images/butterchurn-screenshots/Aderrasi _ Geiss - Airhandler _Kali Mix_ - Canvas Mix.png +0 -0
  191. 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
  192. package/static/images/butterchurn-screenshots/Cope - The Neverending Explosion of Red Liquid Fire.png +0 -0
  193. proton lights __Krash_s beat code_ _Phat_remix02b.png +0 -0
  194. package/static/images/butterchurn-screenshots/Eo_S_ _ Phat - cubetrace - v2.png +0 -0
  195. package/static/images/butterchurn-screenshots/Eo_S_ _ Zylot - skylight _Stained Glass Majesty mix_.png +0 -0
  196. package/static/images/butterchurn-screenshots/Flexi - alien fish pond.png +0 -0
  197. package/static/images/butterchurn-screenshots/Flexi - area 51.png +0 -0
  198. package/static/images/butterchurn-screenshots/Flexi - infused with the spiral.png +0 -0
  199. package/static/images/butterchurn-screenshots/Flexi - mindblob _shiny mix_.png +0 -0
  200. package/static/images/butterchurn-screenshots/Flexi - mindblob mix.png +0 -0
  201. package/static/images/butterchurn-screenshots/Flexi - predator-prey-spirals.png +0 -0
  202. package/static/images/butterchurn-screenshots/Flexi - smashing fractals _acid etching mix_.png +0 -0
  203. package/static/images/butterchurn-screenshots/Flexi - truly soft piece of software - this is generic texturing _Jelly_ .png +0 -0
  204. package/static/images/butterchurn-screenshots/Flexi _ Martin - astral projection.png +0 -0
  205. package/static/images/butterchurn-screenshots/Flexi _ Martin - cascading decay swing.png +0 -0
  206. package/static/images/butterchurn-screenshots/Flexi _ amandio c - piercing 05 - Kopie _2_ - Kopie.png +0 -0
  207. package/static/images/butterchurn-screenshots/Flexi _ stahlregen - jelly showoff parade.png +0 -0
  208. package/static/images/butterchurn-screenshots/Flexi_ fishbrain_ Geiss _ Martin - tokamak witchery.png +0 -0
  209. package/static/images/butterchurn-screenshots/Flexi_ martin _ geiss - dedicated to the sherwin maxawow.png +0 -0
  210. package/static/images/butterchurn-screenshots/Fumbling_Foo _ Flexi_ Martin_ Orb_ Unchained - Star Nova v7b.png +0 -0
  211. package/static/images/butterchurn-screenshots/Geiss - Cauldron - painterly 2 _saturation remix_.png +0 -0
  212. package/static/images/butterchurn-screenshots/Geiss - Reaction Diffusion 2.png +0 -0
  213. package/static/images/butterchurn-screenshots/Geiss - Spiral Artifact.png +0 -0
  214. package/static/images/butterchurn-screenshots/Geiss - Thumb Drum.png +0 -0
  215. package/static/images/butterchurn-screenshots/Geiss _ Flexi _ Martin - disconnected.png +0 -0
  216. package/static/images/butterchurn-screenshots/Geiss_ Flexi _ Stahlregen - Thumbdrum Tokamak _crossfiring aftermath jelly mashup_.png +0 -0
  217. package/static/images/butterchurn-screenshots/Goody - The Wild Vort.png +0 -0
  218. package/static/images/butterchurn-screenshots/Idiot - Star Of Annon.png +0 -0
  219. package/static/images/butterchurn-screenshots/Krash _ Illusion - Spiral Movement.png +0 -0
  220. package/static/images/butterchurn-screenshots/Martin - QBikal - Surface Turbulence IIb.png +0 -0
  221. package/static/images/butterchurn-screenshots/Martin - acid wiring.png +0 -0
  222. package/static/images/butterchurn-screenshots/Martin - charisma.png +0 -0
  223. package/static/images/butterchurn-screenshots/Martin - liquid arrows.png +0 -0
  224. package/static/images/butterchurn-screenshots/Milk Artist At our Best - FED - SlowFast Ft AdamFX n Martin - HD CosmoFX.png +0 -0
  225. package/static/images/butterchurn-screenshots/ORB - Waaa.png +0 -0
  226. package/static/images/butterchurn-screenshots/Phat_fiShbRaiN_Eo_S_Mandala_Chasers_remix.png +0 -0
  227. package/static/images/butterchurn-screenshots/Rovastar - Oozing Resistance.png +0 -0
  228. package/static/images/butterchurn-screenshots/Rovastar _ Loadus _ Geiss - FractalDrop _Triple Mix_.png +0 -0
  229. package/static/images/butterchurn-screenshots/TonyMilkdrop - Leonardo Da Vinci_s Balloon _Flexi - merry-go-round _ techstyle_.png +0 -0
  230. package/static/images/butterchurn-screenshots/TonyMilkdrop - Magellan_s Nebula _Flexi - you enter first _ multiverse_.png +0 -0
  231. package/static/images/butterchurn-screenshots/Unchained - Rewop.png +0 -0
  232. package/static/images/butterchurn-screenshots/Unchained - Unified Drag 2.png +0 -0
  233. package/static/images/butterchurn-screenshots/Unchained _ Rovastar - Wormhole Pillars _Hall of Shadows mix_.png +0 -0
  234. package/static/images/butterchurn-screenshots/Zylot - Paint Spill _Music Reactive Paint Mix_.png +0 -0
  235. package/static/images/butterchurn-screenshots/Zylot - Star Ornament.png +0 -0
  236. package/static/images/butterchurn-screenshots/Zylot - True Visionary _Final Mix_.png +0 -0
  237. package/static/images/butterchurn-screenshots/_Aderrasi - Wanderer in Curved Space - mash0000 - faclempt kibitzing meshuggana schmaltz _Geiss color mix_.png +0 -0
  238. package/static/images/butterchurn-screenshots/_Geiss - Artifact 01.png +0 -0
  239. package/static/images/butterchurn-screenshots/_Geiss - Desert Rose 2.png +0 -0
  240. package/static/images/butterchurn-screenshots/_Geiss - untitled.png +0 -0
  241. package/static/images/butterchurn-screenshots/_Mig_049.png +0 -0
  242. package/static/images/butterchurn-screenshots/_Mig_085.png +0 -0
  243. package/static/images/butterchurn-screenshots/_Rovastar _ Geiss - Hurricane Nightmare _Posterize Mix_.png +0 -0
  244. package/static/images/butterchurn-screenshots/___ Royal - Mashup _197_.png +0 -0
  245. package/static/images/butterchurn-screenshots/___ Royal - Mashup _220_.png +0 -0
  246. package/static/images/butterchurn-screenshots/___ Royal - Mashup _431_.png +0 -0
  247. package/static/images/butterchurn-screenshots/cope _ martin - mother-of-pearl.png +0 -0
  248. package/static/images/butterchurn-screenshots/fiShbRaiN _ Flexi - witchcraft 2_0.png +0 -0
  249. package/static/images/butterchurn-screenshots/flexi - bouncing balls _double mindblob neon mix_.png +0 -0
  250. package/static/images/butterchurn-screenshots/flexi - mom_ why the sky looks different today.png +0 -0
  251. package/static/images/butterchurn-screenshots/flexi - patternton_ district of media_ capitol of the united abstractions of fractopia.png +0 -0
  252. package/static/images/butterchurn-screenshots/flexi - swing out on the spiral.png +0 -0
  253. package/static/images/butterchurn-screenshots/flexi - what is the matrix.png +0 -0
  254. package/static/images/butterchurn-screenshots/flexi _ amandio c - organic _random mashup_.png +0 -0
  255. package/static/images/butterchurn-screenshots/flexi _ amandio c - organic12-3d-2_milk.png +0 -0
  256. package/static/images/butterchurn-screenshots/flexi _ fishbrain - neon mindblob grafitti.png +0 -0
  257. package/static/images/butterchurn-screenshots/flexi _ geiss - pogo cubes vs_ tokamak vs_ game of life _stahls jelly 4_5 finish_.png +0 -0
  258. package/static/images/butterchurn-screenshots/high-altitude basket unraveling - singh grooves nitrogen argon nz_.png +0 -0
  259. package/static/images/butterchurn-screenshots/martin - The Bridge of Khazad-Dum.png +0 -0
  260. package/static/images/butterchurn-screenshots/martin - angel flight.png +0 -0
  261. package/static/images/butterchurn-screenshots/martin - another kind of groove.png +0 -0
  262. package/static/images/butterchurn-screenshots/martin - bombyx mori.png +0 -0
  263. package/static/images/butterchurn-screenshots/martin - castle in the air.png +0 -0
  264. package/static/images/butterchurn-screenshots/martin - chain breaker.png +0 -0
  265. package/static/images/butterchurn-screenshots/martin - disco mix 4.png +0 -0
  266. package/static/images/butterchurn-screenshots/martin - extreme heat.png +0 -0
  267. package/static/images/butterchurn-screenshots/martin - frosty caves 2.png +0 -0
  268. package/static/images/butterchurn-screenshots/martin - fruit machine.png +0 -0
  269. package/static/images/butterchurn-screenshots/martin - ghost city.png +0 -0
  270. package/static/images/butterchurn-screenshots/martin - glass corridor.png +0 -0
  271. package/static/images/butterchurn-screenshots/martin - infinity _2010 update_.png +0 -0
  272. package/static/images/butterchurn-screenshots/martin - mandelbox explorer - high speed demo version.png +0 -0
  273. package/static/images/butterchurn-screenshots/martin - mucus cervix.png +0 -0
  274. package/static/images/butterchurn-screenshots/martin - reflections on black tiles.png +0 -0
  275. package/static/images/butterchurn-screenshots/martin - stormy sea _2010 update_.png +0 -0
  276. package/static/images/butterchurn-screenshots/martin - witchcraft reloaded.png +0 -0
  277. package/static/images/butterchurn-screenshots/martin _ flexi - diamond cutter _prismaticvortex_com_ - camille - i wish i wish i wish i was constrained.png +0 -0
  278. package/static/images/butterchurn-screenshots/martin _shadow harlequins shape code_ - fata morgana.png +0 -0
  279. package/static/images/butterchurn-screenshots/martin_ flexi_ fishbrain _ sto - enterstate _random mashup_.png +0 -0
  280. package/static/images/butterchurn-screenshots/sawtooth grin roam.png +0 -0
  281. package/static/images/butterchurn-screenshots/shifter - dark tides bdrv mix 2.png +0 -0
  282. 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
  283. package/static/images/butterchurn-screenshots/suksma - heretical crosscut playpen.png +0 -0
  284. package/static/images/butterchurn-screenshots/suksma - uninitialized variabowl _hydroponic chronic_.png +0 -0
  285. package/static/images/butterchurn-screenshots/suksma - vector exp 1 - couldn_t not.png +0 -0
  286. package/static/images/butterchurn-screenshots/yin - 191 - Temporal singularities.png +0 -0
  287. package/static/images/logo-512.png +0 -0
  288. package/static/images/logo.png +0 -0
  289. package/static/loukai-logo.png +0 -0
  290. package/static/screenshot-generator.html +610 -0
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Conversion Service - Orchestrates the full karaoke creation pipeline
3
+ *
4
+ * Steps:
5
+ * 1. Convert input to WAV (if needed)
6
+ * 2. Run Demucs stem separation
7
+ * 3. Run Whisper transcription on vocals
8
+ * 4. Run CREPE pitch detection on vocals (optional)
9
+ * 5. Assemble into .stem.m4a file
10
+ */
11
+
12
+ import { join, dirname } from 'path';
13
+ import { existsSync, mkdirSync, rmSync, copyFileSync } from 'fs';
14
+ import { tmpdir } from 'os';
15
+ import { randomUUID } from 'crypto';
16
+ import { convertToWav, encodeToAAC, extractStemTrack } from './ffmpegService.js';
17
+ import { runDemucs, runWhisper, runCrepe } from './pythonRunner.js';
18
+ import { prepareWhisperContext } from './lrclibService.js';
19
+ import { buildStemM4a, injectLyricsIntoStemFile } from './stemBuilder.js';
20
+ import * as llmService from './llmService.js';
21
+ import { detectKey } from './keyDetection.js';
22
+
23
+ // Active conversion state
24
+ let conversionInProgress = false;
25
+ let conversionCancelled = false;
26
+ let currentProcess = null;
27
+
28
+ /**
29
+ * Check if conversion is in progress
30
+ * @returns {boolean}
31
+ */
32
+ export function isConversionInProgress() {
33
+ return conversionInProgress;
34
+ }
35
+
36
+ /**
37
+ * Cancel the current conversion
38
+ * @returns {boolean} True if cancellation was initiated
39
+ */
40
+ export function cancelConversion() {
41
+ if (!conversionInProgress) {
42
+ return false;
43
+ }
44
+
45
+ conversionCancelled = true;
46
+
47
+ // Kill current subprocess if any
48
+ if (currentProcess && typeof currentProcess.kill === 'function') {
49
+ try {
50
+ currentProcess.kill('SIGTERM');
51
+ } catch {
52
+ // Process may have already ended
53
+ }
54
+ }
55
+
56
+ // Reset flag immediately so user can start a new conversion
57
+ // The cancelled subprocess will clean up when it eventually errors
58
+ conversionInProgress = false;
59
+
60
+ return true;
61
+ }
62
+
63
+ /**
64
+ * Check if conversion was cancelled
65
+ * @throws {Error} If cancelled
66
+ */
67
+ function checkCancelled() {
68
+ if (conversionCancelled) {
69
+ throw new Error('Conversion cancelled');
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Run the full conversion pipeline
75
+ *
76
+ * @param {Object} options - Conversion options
77
+ * @param {string} options.inputPath - Path to input audio/video file
78
+ * @param {string} options.title - Song title
79
+ * @param {string} options.artist - Artist name
80
+ * @param {Object} options.tags - All original ID3 tags to preserve
81
+ * @param {number} options.numStems - Number of stems (2 or 4)
82
+ * @param {string} options.whisperModel - Whisper model to use
83
+ * @param {string} options.language - Language code
84
+ * @param {boolean} options.enableCrepe - Whether to run pitch detection
85
+ * @param {string} options.referenceLyrics - Reference lyrics for Whisper hints
86
+ * @param {string} options.outputDir - Output directory (defaults to input file directory)
87
+ * @param {boolean} options.lyricsOnlyMode - Skip stem separation (for existing stem files)
88
+ * @param {number} options.vocalsTrackIndex - Track index for vocals (for lyrics-only mode)
89
+ * @param {Function} onProgress - Progress callback (step, message, progress)
90
+ * @param {Function} onConsoleOutput - Console output callback (line)
91
+ * @param {Object} settingsManager - Settings manager for LLM settings
92
+ * @returns {Promise<Object>} Result with outputPath
93
+ */
94
+ export async function runConversion(
95
+ options,
96
+ onProgress = () => {},
97
+ onConsoleOutput = null,
98
+ settingsManager = null
99
+ ) {
100
+ const {
101
+ inputPath,
102
+ title,
103
+ artist,
104
+ tags = {},
105
+ numStems = 4,
106
+ whisperModel = 'large-v3-turbo',
107
+ language = 'en',
108
+ enableCrepe = true,
109
+ referenceLyrics = '',
110
+ outputDir = dirname(inputPath),
111
+ lyricsOnlyMode = false,
112
+ vocalsTrackIndex = 4, // Default: vocals is typically track 4 in NI Stems format (0=master, 1=drums, 2=bass, 3=other, 4=vocals)
113
+ } = options;
114
+
115
+ if (conversionInProgress) {
116
+ throw new Error('Conversion already in progress');
117
+ }
118
+
119
+ conversionInProgress = true;
120
+ conversionCancelled = false;
121
+ currentProcess = null;
122
+
123
+ // Create temp directory for intermediate files
124
+ const tempDir = join(tmpdir(), `kai-convert-${randomUUID()}`);
125
+ mkdirSync(tempDir, { recursive: true });
126
+
127
+ // Create stems temp directory
128
+ const stemsDir = join(tempDir, 'stems');
129
+ mkdirSync(stemsDir, { recursive: true });
130
+
131
+ try {
132
+ const safeFileName = (artist ? `${artist} - ${title}` : title).replace(/[<>:"/\\|?*]/g, '_');
133
+
134
+ const setCurrentProcess = (proc) => {
135
+ currentProcess = proc;
136
+ };
137
+
138
+ // Different step labels for lyrics-only mode vs full conversion
139
+ const STEPS = lyricsOnlyMode
140
+ ? {
141
+ extract: '1/4 Extract',
142
+ context: '2/4 Context',
143
+ whisper: '3/4 Lyrics',
144
+ crepe: '4/4 Pitch',
145
+ inject: '✓ Inject',
146
+ }
147
+ : {
148
+ wav: '1/7 Prepare',
149
+ demucs: '2/7 Stems',
150
+ context: '3/7 Context',
151
+ whisper: '4/7 Lyrics',
152
+ crepe: '5/7 Pitch',
153
+ encode: '6/7 Encode',
154
+ build: '7/7 Build',
155
+ };
156
+
157
+ let vocalsWavPath;
158
+ let stemPaths = null;
159
+ let demucsResult = null;
160
+
161
+ if (lyricsOnlyMode) {
162
+ // ========================================
163
+ // LYRICS-ONLY MODE: Skip stem separation
164
+ // ========================================
165
+ console.log('🎤 Lyrics-only mode: extracting vocals from existing stem file');
166
+
167
+ // Step 1: Extract vocals track to temp WAV (0-10%)
168
+ onProgress('extract', `[${STEPS.extract}] Extracting vocals track...`, 0);
169
+ checkCancelled();
170
+
171
+ vocalsWavPath = join(tempDir, 'vocals.wav');
172
+ await extractStemTrack(inputPath, vocalsWavPath, vocalsTrackIndex, { sampleRate: 44100 });
173
+
174
+ onProgress('extract', `[${STEPS.extract}] Vocals extracted`, 10);
175
+ checkCancelled();
176
+ } else {
177
+ // ========================================
178
+ // FULL CONVERSION MODE: Stem separation
179
+ // ========================================
180
+
181
+ // Step 1: Convert to WAV (0-5%)
182
+ onProgress('wav', `[${STEPS.wav}] Converting to WAV...`, 0);
183
+ checkCancelled();
184
+
185
+ const wavPath = join(tempDir, 'input.wav');
186
+ await convertToWav(inputPath, wavPath, { sampleRate: 44100 }, (progress) => {
187
+ onProgress(
188
+ 'wav',
189
+ `[${STEPS.wav}] Converting to WAV... ${Math.round(progress)}%`,
190
+ Math.floor(progress * 0.05)
191
+ );
192
+ });
193
+
194
+ checkCancelled();
195
+
196
+ // Step 2: Run Demucs (5-50%)
197
+ onProgress('demucs', `[${STEPS.demucs}] Loading Demucs...`, 5);
198
+ checkCancelled();
199
+
200
+ demucsResult = await runDemucs(
201
+ wavPath,
202
+ stemsDir,
203
+ { numStems },
204
+ (progress, message) => {
205
+ onProgress('demucs', `[${STEPS.demucs}] ${message}`, 5 + Math.floor(progress * 0.45));
206
+ },
207
+ onConsoleOutput,
208
+ setCurrentProcess
209
+ );
210
+
211
+ checkCancelled();
212
+
213
+ // Use stem paths returned by demucs_runner.py
214
+ stemPaths = demucsResult.stems;
215
+
216
+ if (!stemPaths || Object.keys(stemPaths).length === 0) {
217
+ throw new Error('Demucs did not return stem paths');
218
+ }
219
+
220
+ // Verify stem files exist
221
+ for (const [name, path] of Object.entries(stemPaths)) {
222
+ if (!existsSync(path)) {
223
+ throw new Error(`Stem file not found: ${name} at ${path}`);
224
+ }
225
+ }
226
+
227
+ vocalsWavPath = stemPaths.vocals;
228
+ }
229
+
230
+ // ========================================
231
+ // COMMON: Whisper + CREPE + Output
232
+ // ========================================
233
+
234
+ // Progress offsets differ between modes
235
+ const contextStart = lyricsOnlyMode ? 10 : 50;
236
+ const whisperStart = lyricsOnlyMode ? 15 : 52;
237
+ const whisperEnd = lyricsOnlyMode ? 70 : 80;
238
+ const crepeStart = lyricsOnlyMode ? 70 : 80;
239
+ const crepeEnd = lyricsOnlyMode ? 95 : 90;
240
+
241
+ // Step: Prepare Whisper context
242
+ onProgress('context', `[${STEPS.context}] Preparing vocabulary hints...`, contextStart);
243
+ checkCancelled();
244
+
245
+ let initialPrompt = '';
246
+ if (referenceLyrics) {
247
+ const context = await prepareWhisperContext(title, artist, referenceLyrics);
248
+ initialPrompt = context.initialPrompt || '';
249
+ onProgress(
250
+ 'context',
251
+ `[${STEPS.context}] Using ${initialPrompt.split(' ').length} vocabulary hints`,
252
+ contextStart + 1
253
+ );
254
+ }
255
+
256
+ // Step: Run Whisper
257
+ onProgress('whisper', `[${STEPS.whisper}] Loading Whisper...`, whisperStart);
258
+ checkCancelled();
259
+
260
+ if (initialPrompt) {
261
+ console.log(`🎤 Whisper prompt: ${initialPrompt}`);
262
+ }
263
+
264
+ let whisperResult = await runWhisper(
265
+ vocalsWavPath,
266
+ {
267
+ model: whisperModel,
268
+ language,
269
+ initialPrompt,
270
+ },
271
+ (progress, message) => {
272
+ const whisperProgressRange = whisperEnd - whisperStart;
273
+ onProgress(
274
+ 'whisper',
275
+ `[${STEPS.whisper}] ${message}`,
276
+ whisperStart + Math.floor(progress * (whisperProgressRange / 100))
277
+ );
278
+ },
279
+ onConsoleOutput,
280
+ setCurrentProcess
281
+ );
282
+
283
+ checkCancelled();
284
+
285
+ // LLM lyrics correction (optional)
286
+ let llmStats = null;
287
+ if (settingsManager && referenceLyrics) {
288
+ try {
289
+ const llmSettings = llmService.getLLMSettings(settingsManager);
290
+ // Local LLM (lmstudio) doesn't require API key
291
+ const hasValidConfig = llmSettings.provider === 'lmstudio' || llmSettings.apiKey;
292
+ if (llmSettings.enabled && hasValidConfig) {
293
+ onProgress('whisper', `[${STEPS.whisper}] 🤖 AI correction...`, whisperEnd - 2);
294
+ const llmResult = await llmService.correctLyrics(
295
+ whisperResult,
296
+ referenceLyrics,
297
+ llmSettings
298
+ );
299
+ whisperResult = llmResult.output;
300
+ llmStats = llmResult.stats;
301
+ }
302
+ } catch (error) {
303
+ console.warn('⚠️ LLM correction failed, using original Whisper output:', error.message);
304
+ // Continue with original Whisper output
305
+ llmStats = {
306
+ corrections_applied: 0,
307
+ suggestions_made: 0,
308
+ corrections_rejected: 0,
309
+ failed: true,
310
+ error: error.message,
311
+ };
312
+ }
313
+ }
314
+
315
+ checkCancelled();
316
+
317
+ // Run CREPE (optional)
318
+ let pitchData = null;
319
+ if (enableCrepe) {
320
+ onProgress('crepe', `[${STEPS.crepe}] Loading CREPE...`, crepeStart);
321
+ checkCancelled();
322
+
323
+ const crepeProgressRange = crepeEnd - crepeStart;
324
+ const crepeResult = await runCrepe(
325
+ vocalsWavPath,
326
+ null,
327
+ {},
328
+ (progress, message) => {
329
+ onProgress(
330
+ 'crepe',
331
+ `[${STEPS.crepe}] ${message}`,
332
+ crepeStart + Math.floor(progress * (crepeProgressRange / 100))
333
+ );
334
+ },
335
+ onConsoleOutput,
336
+ setCurrentProcess
337
+ );
338
+
339
+ pitchData = crepeResult;
340
+
341
+ // Detect musical key from pitch data
342
+ if (pitchData?.pitch_data) {
343
+ const keyResult = detectKey(pitchData);
344
+ if (keyResult.key !== 'unknown') {
345
+ console.log(
346
+ `🎵 Detected key: ${keyResult.key} (confidence: ${(keyResult.confidence * 100).toFixed(0)}%)`
347
+ );
348
+ pitchData.detected_key = keyResult;
349
+ }
350
+ }
351
+ }
352
+
353
+ checkCancelled();
354
+
355
+ // Build tags array for filtering
356
+ const karaTags = [];
357
+ if (llmStats && llmStats.corrections_applied > 0) {
358
+ karaTags.push('ai_corrected');
359
+ }
360
+
361
+ let outputPath;
362
+
363
+ if (lyricsOnlyMode) {
364
+ // ========================================
365
+ // LYRICS-ONLY: Inject kara atom into existing file
366
+ // ========================================
367
+ onProgress('inject', `[${STEPS.inject}] Adding lyrics to stem file...`, 95);
368
+ checkCancelled();
369
+
370
+ // Output to same directory with modified name, or optionally overwrite in place
371
+ outputPath = join(outputDir, `${safeFileName}.stem.m4a`);
372
+
373
+ // Copy original file to output location if different
374
+ if (inputPath !== outputPath) {
375
+ copyFileSync(inputPath, outputPath);
376
+ }
377
+
378
+ // Inject lyrics into the copied/original file
379
+ await injectLyricsIntoStemFile({
380
+ filePath: outputPath,
381
+ lyrics: whisperResult,
382
+ pitch: pitchData,
383
+ llmCorrections: llmStats,
384
+ tags: karaTags,
385
+ });
386
+
387
+ onProgress('complete', '✓ Lyrics added to stem file!', 100);
388
+ } else {
389
+ // ========================================
390
+ // FULL CONVERSION: Encode stems and build new file
391
+ // ========================================
392
+ const stemLabels = {
393
+ master: '🎵 Master',
394
+ vocals: '🎤 Vocals',
395
+ drums: '🥁 Drums',
396
+ bass: '🎸 Bass',
397
+ other: '🎹 Other',
398
+ no_vocals: '🎵 Instrumental',
399
+ };
400
+
401
+ onProgress('encode', `[${STEPS.encode}] Encoding stems to AAC...`, 90);
402
+ checkCancelled();
403
+
404
+ const aacPaths = {};
405
+ const wavPath = join(tempDir, 'input.wav');
406
+
407
+ // First encode the master (original mix) - required by NI Stems spec
408
+ const masterAacPath = join(tempDir, 'master.m4a');
409
+ onProgress('encode', `[${STEPS.encode}] Encoding ${stemLabels.master}...`, 90);
410
+ await encodeToAAC(wavPath, masterAacPath, { codec: 'aac', bitrate: '192k' });
411
+ aacPaths.master = masterAacPath;
412
+
413
+ checkCancelled();
414
+
415
+ // Then encode the individual stems in NI Stems order: drums, bass, other, vocals
416
+ const stemOrder = ['drums', 'bass', 'other', 'vocals'];
417
+ const encodeProgress = 4 / stemOrder.length;
418
+
419
+ for (let i = 0; i < stemOrder.length; i++) {
420
+ const stemName = stemOrder[i];
421
+ const stemPath = stemPaths[stemName];
422
+ if (!stemPath) continue; // Skip if stem doesn't exist
423
+
424
+ const aacPath = join(tempDir, `${stemName}.m4a`);
425
+ const label = stemLabels[stemName] || stemName;
426
+
427
+ onProgress(
428
+ 'encode',
429
+ `[${STEPS.encode}] Encoding ${label}...`,
430
+ 91 + Math.floor(i * encodeProgress)
431
+ );
432
+ await encodeToAAC(stemPath, aacPath, { codec: 'aac', bitrate: '192k' });
433
+ aacPaths[stemName] = aacPath;
434
+
435
+ checkCancelled();
436
+ }
437
+
438
+ // Build .stem.m4a (95-100%)
439
+ onProgress('build', `[${STEPS.build}] Packaging stem.m4a file...`, 95);
440
+ checkCancelled();
441
+
442
+ outputPath = join(outputDir, `${safeFileName}.stem.m4a`);
443
+
444
+ await buildStemM4a({
445
+ outputPath,
446
+ stems: aacPaths,
447
+ metadata: {
448
+ title,
449
+ artist,
450
+ duration: demucsResult?.duration || 0,
451
+ tags, // Preserve all original ID3 tags
452
+ },
453
+ lyrics: whisperResult,
454
+ pitch: pitchData,
455
+ llmCorrections: llmStats, // LLM corrections metadata
456
+ tags: karaTags, // Kara atom tags for filtering
457
+ });
458
+
459
+ onProgress('complete', '✓ Karaoke file created!', 100);
460
+ }
461
+
462
+ // Cleanup temp directory
463
+ try {
464
+ rmSync(tempDir, { recursive: true, force: true });
465
+ } catch {
466
+ console.warn('Failed to cleanup temp directory:', tempDir);
467
+ }
468
+
469
+ conversionInProgress = false;
470
+
471
+ return {
472
+ success: true,
473
+ outputPath,
474
+ duration: demucsResult?.duration || 0,
475
+ stems: lyricsOnlyMode ? [] : Object.keys(stemPaths || {}),
476
+ hasLyrics: Boolean(whisperResult?.words?.length),
477
+ hasPitch: Boolean(pitchData),
478
+ llmStats,
479
+ lyricsOnlyMode,
480
+ };
481
+ } catch (error) {
482
+ conversionInProgress = false;
483
+
484
+ // Cleanup on error
485
+ try {
486
+ rmSync(tempDir, { recursive: true, force: true });
487
+ } catch {
488
+ // Ignore cleanup errors
489
+ }
490
+
491
+ if (conversionCancelled) {
492
+ return { success: false, cancelled: true };
493
+ }
494
+
495
+ throw error;
496
+ }
497
+ }
498
+
499
+ export default {
500
+ runConversion,
501
+ cancelConversion,
502
+ isConversionInProgress,
503
+ };