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,413 @@
1
+ /**
2
+ * Creator Service Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
6
+
7
+ // Mock all external dependencies before importing the service
8
+ vi.mock('../../main/creator/systemChecker.js', () => ({
9
+ checkAllComponents: vi.fn(),
10
+ getCacheDir: vi.fn(() => '/mock/cache/dir'),
11
+ getPythonPath: vi.fn(() => '/mock/python'),
12
+ }));
13
+
14
+ vi.mock('../../main/creator/downloadManager.js', () => ({
15
+ installAllComponents: vi.fn(),
16
+ }));
17
+
18
+ vi.mock('../../main/creator/lrclibService.js', () => ({
19
+ searchLyrics: vi.fn(),
20
+ prepareWhisperContext: vi.fn(),
21
+ }));
22
+
23
+ vi.mock('../../main/creator/ffmpegService.js', () => ({
24
+ getAudioInfo: vi.fn(),
25
+ isVideoFile: vi.fn(),
26
+ }));
27
+
28
+ vi.mock('../../main/creator/conversionService.js', () => ({
29
+ runConversion: vi.fn(),
30
+ cancelConversion: vi.fn(),
31
+ isConversionInProgress: vi.fn(() => false),
32
+ }));
33
+
34
+ describe('creatorService', () => {
35
+ let creatorService;
36
+ let checkAllComponents;
37
+ let _getCacheDir;
38
+ let _getPythonPath;
39
+ let installAllComponents;
40
+ let searchLyrics;
41
+ let prepareWhisperContext;
42
+ let getAudioInfo;
43
+ let isVideoFile;
44
+ let runConversion;
45
+ let cancelConversion;
46
+ let isConversionInProgress;
47
+
48
+ beforeEach(async () => {
49
+ // Reset modules to clear module-level state (installationInProgress, etc.)
50
+ vi.resetModules();
51
+
52
+ // Re-import mocks and service fresh
53
+ const systemChecker = await import('../../main/creator/systemChecker.js');
54
+ checkAllComponents = systemChecker.checkAllComponents;
55
+ _getCacheDir = systemChecker.getCacheDir;
56
+ _getPythonPath = systemChecker.getPythonPath;
57
+
58
+ const downloadManager = await import('../../main/creator/downloadManager.js');
59
+ installAllComponents = downloadManager.installAllComponents;
60
+
61
+ const lrclibService = await import('../../main/creator/lrclibService.js');
62
+ searchLyrics = lrclibService.searchLyrics;
63
+ prepareWhisperContext = lrclibService.prepareWhisperContext;
64
+
65
+ const ffmpegService = await import('../../main/creator/ffmpegService.js');
66
+ getAudioInfo = ffmpegService.getAudioInfo;
67
+ isVideoFile = ffmpegService.isVideoFile;
68
+
69
+ const conversionService = await import('../../main/creator/conversionService.js');
70
+ runConversion = conversionService.runConversion;
71
+ cancelConversion = conversionService.cancelConversion;
72
+ isConversionInProgress = conversionService.isConversionInProgress;
73
+
74
+ // Import fresh service
75
+ creatorService = await import('./creatorService.js');
76
+
77
+ // Reset conversion state mock
78
+ isConversionInProgress.mockReturnValue(false);
79
+ });
80
+
81
+ describe('checkComponents', () => {
82
+ it('should return component status successfully', async () => {
83
+ checkAllComponents.mockResolvedValue({
84
+ ffmpeg: { installed: true },
85
+ python: { installed: true },
86
+ whisper: { installed: true },
87
+ });
88
+
89
+ const result = await creatorService.checkComponents();
90
+
91
+ expect(result.success).toBe(true);
92
+ expect(result.ffmpeg).toEqual({ installed: true });
93
+ expect(result.python).toEqual({ installed: true });
94
+ expect(result.whisper).toEqual({ installed: true });
95
+ });
96
+
97
+ it('should handle check errors', async () => {
98
+ checkAllComponents.mockRejectedValue(new Error('Check failed'));
99
+
100
+ const result = await creatorService.checkComponents();
101
+
102
+ expect(result.success).toBe(false);
103
+ expect(result.error).toBe('Check failed');
104
+ });
105
+ });
106
+
107
+ describe('getStatus', () => {
108
+ it('should return installation status', () => {
109
+ isConversionInProgress.mockReturnValue(false);
110
+
111
+ const result = creatorService.getStatus();
112
+
113
+ expect(result.installing).toBe(false);
114
+ expect(result.cancelled).toBe(false);
115
+ expect(result.converting).toBe(false);
116
+ expect(result.cacheDir).toBe('/mock/cache/dir');
117
+ expect(result.pythonPath).toBe('/mock/python');
118
+ });
119
+
120
+ it('should reflect conversion in progress', () => {
121
+ isConversionInProgress.mockReturnValue(true);
122
+
123
+ const result = creatorService.getStatus();
124
+
125
+ expect(result.converting).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe('installComponents', () => {
130
+ it('should install components successfully', async () => {
131
+ installAllComponents.mockImplementation((progressCallback) => {
132
+ progressCallback(50, 'Installing...');
133
+ progressCallback(100, 'Done');
134
+ return { success: true };
135
+ });
136
+
137
+ const onProgress = vi.fn();
138
+ const result = await creatorService.installComponents(onProgress);
139
+
140
+ expect(result.success).toBe(true);
141
+ expect(onProgress).toHaveBeenCalledWith({
142
+ step: 'starting',
143
+ message: 'Starting installation...',
144
+ progress: 0,
145
+ });
146
+ expect(onProgress).toHaveBeenCalledWith({
147
+ step: 'complete',
148
+ message: 'Installation complete',
149
+ progress: 100,
150
+ });
151
+ });
152
+
153
+ it('should handle installation errors', async () => {
154
+ installAllComponents.mockRejectedValue(new Error('Install failed'));
155
+
156
+ const onProgress = vi.fn();
157
+ const result = await creatorService.installComponents(onProgress);
158
+
159
+ expect(result.success).toBe(false);
160
+ expect(result.error).toBe('Install failed');
161
+ });
162
+
163
+ it('should prevent concurrent installations', async () => {
164
+ // Start first installation (won't resolve)
165
+ installAllComponents.mockImplementation(() => new Promise(() => {}));
166
+ creatorService.installComponents(vi.fn());
167
+
168
+ // Try to start second
169
+ const result = await creatorService.installComponents(vi.fn());
170
+
171
+ expect(result.success).toBe(false);
172
+ expect(result.error).toBe('Installation already in progress');
173
+ });
174
+ });
175
+
176
+ describe('cancelInstall', () => {
177
+ it('should return error when no installation in progress', () => {
178
+ const result = creatorService.cancelInstall();
179
+
180
+ expect(result.success).toBe(false);
181
+ expect(result.error).toBe('No installation in progress');
182
+ });
183
+ });
184
+
185
+ describe('findLyrics', () => {
186
+ it('should find lyrics successfully', async () => {
187
+ searchLyrics.mockResolvedValue({
188
+ syncedLyrics: '[00:01.00]Hello world',
189
+ plainLyrics: 'Hello world',
190
+ });
191
+
192
+ const result = await creatorService.findLyrics('Test Song', 'Test Artist');
193
+
194
+ expect(result.success).toBe(true);
195
+ expect(result.syncedLyrics).toBe('[00:01.00]Hello world');
196
+ expect(result.plainLyrics).toBe('Hello world');
197
+ expect(searchLyrics).toHaveBeenCalledWith('Test Song', 'Test Artist');
198
+ });
199
+
200
+ it('should return error when no lyrics found', async () => {
201
+ searchLyrics.mockResolvedValue(null);
202
+
203
+ const result = await creatorService.findLyrics('Unknown', 'Unknown');
204
+
205
+ expect(result.success).toBe(false);
206
+ expect(result.error).toBe('No lyrics found');
207
+ });
208
+
209
+ it('should handle search errors', async () => {
210
+ searchLyrics.mockRejectedValue(new Error('Search failed'));
211
+
212
+ const result = await creatorService.findLyrics('Test', 'Test');
213
+
214
+ expect(result.success).toBe(false);
215
+ expect(result.error).toBe('Search failed');
216
+ });
217
+ });
218
+
219
+ describe('getWhisperContext', () => {
220
+ it('should prepare whisper context successfully', async () => {
221
+ prepareWhisperContext.mockResolvedValue({
222
+ vocabulary: ['word1', 'word2'],
223
+ prompt: 'context prompt',
224
+ });
225
+
226
+ const result = await creatorService.getWhisperContext('Title', 'Artist', 'existing lyrics');
227
+
228
+ expect(result.success).toBe(true);
229
+ expect(result.vocabulary).toEqual(['word1', 'word2']);
230
+ expect(prepareWhisperContext).toHaveBeenCalledWith('Title', 'Artist', 'existing lyrics');
231
+ });
232
+
233
+ it('should handle context preparation errors', async () => {
234
+ prepareWhisperContext.mockRejectedValue(new Error('Context failed'));
235
+
236
+ const result = await creatorService.getWhisperContext('Title', 'Artist', '');
237
+
238
+ expect(result.success).toBe(false);
239
+ expect(result.error).toBe('Context failed');
240
+ });
241
+ });
242
+
243
+ describe('getFileInfo', () => {
244
+ it('should return file info with ID3 tags', async () => {
245
+ getAudioInfo.mockResolvedValue({
246
+ title: 'Song Title',
247
+ artist: 'Artist Name',
248
+ album: 'Album Name',
249
+ duration: 180,
250
+ sampleRate: 44100,
251
+ channels: 2,
252
+ codec: 'mp3',
253
+ tags: { year: '2023' },
254
+ });
255
+ isVideoFile.mockResolvedValue(false);
256
+ searchLyrics.mockResolvedValue({
257
+ syncedLyrics: '[00:01.00]Lyrics',
258
+ });
259
+
260
+ const result = await creatorService.getFileInfo('/path/to/song.mp3');
261
+
262
+ expect(result.success).toBe(true);
263
+ expect(result.file.title).toBe('Song Title');
264
+ expect(result.file.artist).toBe('Artist Name');
265
+ expect(result.file.album).toBe('Album Name');
266
+ expect(result.file.duration).toBe(180);
267
+ expect(result.file.hasId3Tags).toBe(true);
268
+ expect(result.file.isVideo).toBe(false);
269
+ expect(result.lyrics).toBeDefined();
270
+ });
271
+
272
+ it('should parse filename when no ID3 tags', async () => {
273
+ getAudioInfo.mockResolvedValue({
274
+ duration: 180,
275
+ sampleRate: 44100,
276
+ channels: 2,
277
+ codec: 'mp3',
278
+ });
279
+ isVideoFile.mockResolvedValue(false);
280
+ searchLyrics.mockResolvedValue(null);
281
+
282
+ const result = await creatorService.getFileInfo('/path/to/Artist Name - Song Title.mp3');
283
+
284
+ expect(result.success).toBe(true);
285
+ expect(result.file.title).toBe('Song Title');
286
+ expect(result.file.artist).toBe('Artist Name');
287
+ expect(result.file.hasId3Tags).toBe(false);
288
+ });
289
+
290
+ it('should detect video files', async () => {
291
+ getAudioInfo.mockResolvedValue({ duration: 180 });
292
+ isVideoFile.mockResolvedValue(true);
293
+
294
+ const result = await creatorService.getFileInfo('/path/to/video.mp4');
295
+
296
+ expect(result.file.isVideo).toBe(true);
297
+ });
298
+
299
+ it('should handle file info errors', async () => {
300
+ getAudioInfo.mockRejectedValue(new Error('File not found'));
301
+
302
+ const result = await creatorService.getFileInfo('/invalid/path.mp3');
303
+
304
+ expect(result.success).toBe(false);
305
+ expect(result.error).toBe('File not found');
306
+ });
307
+
308
+ it('should continue if lyrics lookup fails', async () => {
309
+ getAudioInfo.mockResolvedValue({
310
+ title: 'Song',
311
+ artist: 'Artist',
312
+ duration: 180,
313
+ });
314
+ isVideoFile.mockResolvedValue(false);
315
+ searchLyrics.mockRejectedValue(new Error('Network error'));
316
+
317
+ const result = await creatorService.getFileInfo('/path/to/song.mp3');
318
+
319
+ expect(result.success).toBe(true);
320
+ expect(result.lyrics).toBeUndefined();
321
+ });
322
+ });
323
+
324
+ describe('startConversion', () => {
325
+ it('should start conversion successfully', async () => {
326
+ runConversion.mockImplementation((options, progressCb) => {
327
+ progressCb('processing', 'Processing...', 50);
328
+ return { success: true, outputPath: '/output/file.m4a' };
329
+ });
330
+
331
+ const onProgress = vi.fn();
332
+ const result = await creatorService.startConversion({ inputPath: '/input.mp3' }, onProgress);
333
+
334
+ expect(result.success).toBe(true);
335
+ expect(result.outputPath).toBe('/output/file.m4a');
336
+ expect(onProgress).toHaveBeenCalledWith({
337
+ step: 'starting',
338
+ message: 'Starting conversion...',
339
+ progress: 0,
340
+ });
341
+ });
342
+
343
+ it('should prevent concurrent conversions', async () => {
344
+ isConversionInProgress.mockReturnValue(true);
345
+
346
+ const result = await creatorService.startConversion({}, vi.fn());
347
+
348
+ expect(result.success).toBe(false);
349
+ expect(result.error).toBe('Conversion already in progress');
350
+ });
351
+
352
+ it('should handle conversion errors', async () => {
353
+ runConversion.mockRejectedValue(new Error('Conversion failed'));
354
+
355
+ const result = await creatorService.startConversion({}, vi.fn());
356
+
357
+ expect(result.success).toBe(false);
358
+ expect(result.error).toBe('Conversion failed');
359
+ });
360
+
361
+ it('should pass console output callback', async () => {
362
+ const onConsoleOutput = vi.fn();
363
+ runConversion.mockResolvedValue({ success: true });
364
+
365
+ await creatorService.startConversion({}, vi.fn(), onConsoleOutput);
366
+
367
+ expect(runConversion).toHaveBeenCalledWith({}, expect.any(Function), onConsoleOutput, null);
368
+ });
369
+
370
+ it('should pass settings manager', async () => {
371
+ const settingsManager = { get: vi.fn() };
372
+ runConversion.mockResolvedValue({ success: true });
373
+
374
+ await creatorService.startConversion({}, vi.fn(), null, settingsManager);
375
+
376
+ expect(runConversion).toHaveBeenCalledWith({}, expect.any(Function), null, settingsManager);
377
+ });
378
+ });
379
+
380
+ describe('stopConversion', () => {
381
+ it('should stop conversion', () => {
382
+ cancelConversion.mockReturnValue(true);
383
+
384
+ const result = creatorService.stopConversion();
385
+
386
+ expect(result.success).toBe(true);
387
+ expect(cancelConversion).toHaveBeenCalled();
388
+ });
389
+
390
+ it('should return false when cancel fails', () => {
391
+ cancelConversion.mockReturnValue(false);
392
+
393
+ const result = creatorService.stopConversion();
394
+
395
+ expect(result.success).toBe(false);
396
+ });
397
+ });
398
+
399
+ describe('default export', () => {
400
+ it('should export all functions', () => {
401
+ expect(creatorService.default).toBeDefined();
402
+ expect(creatorService.default.checkComponents).toBeDefined();
403
+ expect(creatorService.default.getStatus).toBeDefined();
404
+ expect(creatorService.default.installComponents).toBeDefined();
405
+ expect(creatorService.default.cancelInstall).toBeDefined();
406
+ expect(creatorService.default.findLyrics).toBeDefined();
407
+ expect(creatorService.default.getWhisperContext).toBeDefined();
408
+ expect(creatorService.default.getFileInfo).toBeDefined();
409
+ expect(creatorService.default.startConversion).toBeDefined();
410
+ expect(creatorService.default.stopConversion).toBeDefined();
411
+ });
412
+ });
413
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Editor Service - Shared business logic for song editing
3
+ *
4
+ * Used by both IPC handlers (Electron) and REST endpoints (Web Server)
5
+ * to ensure consistent song editing behavior across all interfaces.
6
+ */
7
+
8
+ import M4ALoader from '../../utils/m4aLoader.js';
9
+ import { Atoms } from 'm4a-stems';
10
+
11
+ /**
12
+ * Load a song for editing
13
+ * @param {string} path - Path to the song file
14
+ * @returns {Promise<Object>} Song data ready for editing
15
+ */
16
+ export async function loadSong(path) {
17
+ if (!path) {
18
+ throw new Error('Path is required');
19
+ }
20
+
21
+ const lowerPath = path.toLowerCase();
22
+
23
+ // M4A/MP4 stems format is the only supported format for editing
24
+ if (lowerPath.endsWith('.m4a') || lowerPath.endsWith('.mp4')) {
25
+ const m4aData = await M4ALoader.load(path);
26
+ m4aData.originalFilePath = path;
27
+ return {
28
+ format: 'm4a-stems',
29
+ kaiData: m4aData, // Named kaiData for compatibility with existing editor components
30
+ };
31
+ } else {
32
+ // CDG and other formats are not supported for editing
33
+ throw new Error('Only M4A stems format is supported for editing');
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Save song edits
39
+ * @param {string} path - Path to the song file
40
+ * @param {Object} updates - Updates to apply
41
+ * @returns {Promise<Object>} Save result
42
+ */
43
+ export async function saveSong(path, updates) {
44
+ if (!path) {
45
+ throw new Error('Path is required');
46
+ }
47
+
48
+ const { format, metadata, lyrics } = updates;
49
+
50
+ if (format === 'm4a-stems') {
51
+ // Handle M4A format
52
+ return await saveM4ASong(path, { metadata, lyrics });
53
+ } else {
54
+ throw new Error(`Unsupported format: ${format}. Only m4a-stems format is supported.`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Save M4A song edits
60
+ * @param {string} path - Path to M4A file
61
+ * @param {Object} updates - Updates to apply
62
+ * @returns {Promise<Object>} Save result
63
+ */
64
+ async function saveM4ASong(path, updates) {
65
+ const { metadata, lyrics } = updates;
66
+
67
+ // Load existing M4A data
68
+ const m4aData = await M4ALoader.load(path);
69
+
70
+ // Merge metadata updates into the song data
71
+ const updatedMetadata = { ...m4aData.metadata };
72
+ if (metadata.title !== undefined) updatedMetadata.title = metadata.title;
73
+ if (metadata.artist !== undefined) updatedMetadata.artist = metadata.artist;
74
+ if (metadata.album !== undefined) updatedMetadata.album = metadata.album;
75
+ if (metadata.year !== undefined) updatedMetadata.year = metadata.year;
76
+ if (metadata.genre !== undefined) updatedMetadata.genre = metadata.genre;
77
+ if (metadata.key !== undefined) updatedMetadata.key = metadata.key;
78
+
79
+ // NOTE: Standard metadata (title, artist, album, year, genre) is now written
80
+ // using proper MP4 atoms via addStandardMetadata() below, not FFmpeg
81
+
82
+ // Use updated lyrics array
83
+ let updatedLyrics = m4aData.lyrics;
84
+ if (lyrics !== undefined && Array.isArray(lyrics)) {
85
+ updatedLyrics = lyrics;
86
+ }
87
+
88
+ // Prepare data to save
89
+ const dataToSave = {
90
+ metadata: updatedMetadata,
91
+ lyrics: updatedLyrics,
92
+ audio: m4aData.audio, // Preserve audio configuration
93
+ features: m4aData.features, // Preserve features
94
+ singers: m4aData.singers, // Preserve singers
95
+ meta: m4aData.meta, // Preserve meta
96
+ tags: m4aData.tags || [], // Preserve existing tags
97
+ };
98
+
99
+ // Add 'edited' tag if not already present
100
+ if (!dataToSave.tags.includes('edited')) {
101
+ dataToSave.tags = [...dataToSave.tags, 'edited'];
102
+ }
103
+
104
+ // Handle AI corrections metadata (rejections/suggestions) if present
105
+ if (metadata.rejections !== undefined || metadata.suggestions !== undefined) {
106
+ const updatedMeta = { ...(dataToSave.meta || {}) };
107
+
108
+ if (!updatedMeta.corrections) {
109
+ updatedMeta.corrections = {};
110
+ }
111
+
112
+ if (metadata.rejections !== undefined) {
113
+ updatedMeta.corrections.rejected = metadata.rejections.map((r) => ({
114
+ line: r.line_num,
115
+ start: r.start_time,
116
+ end: r.end_time,
117
+ old: r.old_text,
118
+ new: r.new_text,
119
+ reason: r.reason,
120
+ word_retention: r.retention_rate,
121
+ }));
122
+ }
123
+
124
+ if (metadata.suggestions !== undefined) {
125
+ updatedMeta.corrections.missing_lines_suggested = metadata.suggestions.map((s) => ({
126
+ suggested_text: s.suggested_text,
127
+ start: s.start_time,
128
+ end: s.end_time,
129
+ confidence: s.confidence,
130
+ reason: s.reason,
131
+ pitch_activity: s.pitch_activity,
132
+ }));
133
+ }
134
+
135
+ dataToSave.meta = updatedMeta;
136
+ }
137
+
138
+ // Prepare kara data structure for m4a-stems
139
+ // Note: Audio sources are read from the NI Stems 'stem' atom, not stored in kara
140
+ const karaData = {
141
+ // Timing information
142
+ timing: {
143
+ offset_sec: dataToSave.audio?.timing?.offsetSec || 0,
144
+ encoder_delay_samples: dataToSave.audio?.timing?.encoderDelaySamples || 0,
145
+ },
146
+
147
+ // Tags for filtering (e.g., 'edited', 'ai_corrected')
148
+ tags: dataToSave.tags || [],
149
+
150
+ // Lyrics (lines) - preserves word-level timing if present
151
+ lines: (dataToSave.lyrics || []).map((line) => ({
152
+ start: line.start || line.startTimeSec || 0,
153
+ end: line.end || line.endTimeSec || 0,
154
+ text: line.text || '',
155
+ ...(line.disabled && { disabled: true }),
156
+ ...(line.singer && { singer: line.singer }),
157
+ ...(line.words && { words: line.words }),
158
+ })),
159
+
160
+ // Optional: tempo/meter data
161
+ ...(dataToSave.features?.tempo && {
162
+ meter: dataToSave.features.tempo,
163
+ }),
164
+
165
+ // Optional: singers
166
+ ...(dataToSave.singers &&
167
+ dataToSave.singers.length > 0 && {
168
+ singers: dataToSave.singers,
169
+ }),
170
+
171
+ // Optional: corrections metadata
172
+ ...(dataToSave.meta?.corrections && {
173
+ meta: { corrections: dataToSave.meta.corrections },
174
+ }),
175
+ };
176
+
177
+ // Save using m4a-stems
178
+ console.log('💾 Saving M4A kara atom:', path);
179
+ console.log('📝 kara data prepared:', {
180
+ lyricsCount: karaData.lines?.length || 0,
181
+ tagsCount: karaData.tags?.length || 0,
182
+ });
183
+
184
+ await Atoms.writeKaraAtom(path, karaData);
185
+
186
+ // Write standard MP4 metadata atoms (title, artist, album, year, genre, BPM)
187
+ const standardMetadata = {
188
+ title: updatedMetadata.title,
189
+ artist: updatedMetadata.artist,
190
+ album: updatedMetadata.album,
191
+ year: updatedMetadata.year,
192
+ genre: updatedMetadata.genre,
193
+ tempo: updatedMetadata.tempo,
194
+ };
195
+ await Atoms.addStandardMetadata(path, standardMetadata);
196
+
197
+ // Write musical key if changed (separate atom for DJ software)
198
+ if (metadata.key !== undefined && updatedMetadata.key) {
199
+ console.log(`🎹 Writing musical key: ${updatedMetadata.key}`);
200
+ await Atoms.addMusicalKey(path, updatedMetadata.key);
201
+ }
202
+
203
+ // Restore any preserved atoms that we didn't explicitly handle
204
+ if (m4aData._preservedAtoms && Object.keys(m4aData._preservedAtoms).length > 0) {
205
+ console.log(`📦 Restoring ${Object.keys(m4aData._preservedAtoms).length} preserved atoms`);
206
+ // Note: These atoms are already in the file and we didn't delete them,
207
+ // so they should still be there. This is just for logging.
208
+ }
209
+
210
+ console.log('✅ M4A file saved successfully');
211
+
212
+ return { success: true };
213
+ }