loukai-app 0.3.1 ā 0.4.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/package.json +2 -2
- package/src/main/audioEngine.js +11 -10
- package/src/main/creator/conversionService.js +4 -3
- package/src/main/creator/downloadManager.js +70 -67
- package/src/main/creator/ffmpegService.js +3 -2
- package/src/main/creator/installLogger.js +5 -7
- package/src/main/creator/llmService.js +13 -7
- package/src/main/creator/lrclibService.js +5 -6
- package/src/main/creator/stemBuilder.js +20 -19
- package/src/main/handlers/audioHandlers.js +2 -1
- package/src/main/handlers/canvasHandlers.js +4 -3
- package/src/main/handlers/creatorHandlers.js +2 -1
- package/src/main/handlers/editorHandlers.js +8 -7
- package/src/main/handlers/index.js +21 -19
- package/src/main/handlers/queueHandlers.js +3 -2
- package/src/main/handlers/settingsHandlers.js +2 -1
- package/src/main/handlers/webServerHandlers.js +3 -2
- package/src/main/logger.js +12 -0
- package/src/main/main.js +71 -66
- package/src/main/statePersistence.js +13 -12
- package/src/main/utils/pathValidator.js +21 -17
- package/src/main/webServer.js +100 -93
- package/src/shared/services/serverSettingsService.js +0 -1
- package/src/shared/services/settingsService.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loukai-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Loukai Karaoke - Free and open source karaoke system for playing and creating stem-based karaoke files",
|
|
6
6
|
"main": "src/main/main.js",
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
]
|
|
232
232
|
},
|
|
233
233
|
"bin": {
|
|
234
|
-
"loukai": "bin/loukai.js"
|
|
234
|
+
"loukai-app": "bin/loukai.js"
|
|
235
235
|
},
|
|
236
236
|
"files": [
|
|
237
237
|
"src/",
|
package/src/main/audioEngine.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
+
import { log } from './logger.js';
|
|
2
3
|
|
|
3
4
|
class AudioEngine extends EventEmitter {
|
|
4
5
|
constructor() {
|
|
@@ -42,7 +43,7 @@ class AudioEngine extends EventEmitter {
|
|
|
42
43
|
try {
|
|
43
44
|
this.initialized = true;
|
|
44
45
|
this.scanDevices();
|
|
45
|
-
|
|
46
|
+
log('Audio engine initialized');
|
|
46
47
|
} catch (error) {
|
|
47
48
|
console.error('Failed to initialize audio engine:', error);
|
|
48
49
|
throw error;
|
|
@@ -77,7 +78,7 @@ class AudioEngine extends EventEmitter {
|
|
|
77
78
|
this.devices.IEM = 'default-output';
|
|
78
79
|
this.devices.input = 'default-input';
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
log(`Audio engine initialized with ${this.availableDevices.length} fallback devices`);
|
|
81
82
|
} catch (error) {
|
|
82
83
|
console.error('Failed to scan audio devices:', error);
|
|
83
84
|
this.availableDevices = [];
|
|
@@ -130,7 +131,7 @@ class AudioEngine extends EventEmitter {
|
|
|
130
131
|
},
|
|
131
132
|
};
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
log(`${busType} output stream created`);
|
|
134
135
|
} catch (error) {
|
|
135
136
|
console.error(`Failed to create ${busType} output stream:`, error);
|
|
136
137
|
}
|
|
@@ -154,7 +155,7 @@ class AudioEngine extends EventEmitter {
|
|
|
154
155
|
},
|
|
155
156
|
};
|
|
156
157
|
|
|
157
|
-
//
|
|
158
|
+
// log('Input stream created');
|
|
158
159
|
} catch (error) {
|
|
159
160
|
console.error('Failed to create input stream:', error);
|
|
160
161
|
}
|
|
@@ -260,7 +261,7 @@ class AudioEngine extends EventEmitter {
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
this.emit('mixChanged', this.getMixerState());
|
|
263
|
-
|
|
264
|
+
log(`Loaded song with ${this.stems.size} stems`);
|
|
264
265
|
} catch (error) {
|
|
265
266
|
console.error('Failed to load song:', error);
|
|
266
267
|
throw error;
|
|
@@ -268,7 +269,7 @@ class AudioEngine extends EventEmitter {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
play() {
|
|
271
|
-
|
|
272
|
+
log('šµ MAIN AudioEngine.play() called');
|
|
272
273
|
if (!this.initialized || !this.songData) return false;
|
|
273
274
|
|
|
274
275
|
try {
|
|
@@ -279,7 +280,7 @@ class AudioEngine extends EventEmitter {
|
|
|
279
280
|
this.playStartPosition = this.currentPosition;
|
|
280
281
|
this.startPositionTimer();
|
|
281
282
|
|
|
282
|
-
|
|
283
|
+
log('šµ MAIN AudioEngine.play() - started timer, isPlaying =', this.isPlaying);
|
|
283
284
|
|
|
284
285
|
if (this.audioStreams.PA) this.audioStreams.PA.start();
|
|
285
286
|
if (this.audioStreams.IEM) this.audioStreams.IEM.start();
|
|
@@ -437,13 +438,13 @@ class AudioEngine extends EventEmitter {
|
|
|
437
438
|
}
|
|
438
439
|
|
|
439
440
|
setAutotuneEnabled(enabled) {
|
|
440
|
-
|
|
441
|
+
log('Auto-tune enabled set to:', enabled);
|
|
441
442
|
// TODO: Implement actual auto-tune processing
|
|
442
443
|
return true;
|
|
443
444
|
}
|
|
444
445
|
|
|
445
446
|
setAutotuneSettings(settings) {
|
|
446
|
-
|
|
447
|
+
log('Auto-tune settings updated:', settings);
|
|
447
448
|
// TODO: Implement actual settings application
|
|
448
449
|
return true;
|
|
449
450
|
}
|
|
@@ -468,7 +469,7 @@ class AudioEngine extends EventEmitter {
|
|
|
468
469
|
}
|
|
469
470
|
|
|
470
471
|
this.initialized = false;
|
|
471
|
-
|
|
472
|
+
log('Audio engine stopped');
|
|
472
473
|
} catch (error) {
|
|
473
474
|
console.error('Error stopping audio engine:', error);
|
|
474
475
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from '../logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* Conversion Service - Orchestrates the full karaoke creation pipeline
|
|
3
4
|
*
|
|
@@ -162,7 +163,7 @@ export async function runConversion(
|
|
|
162
163
|
// ========================================
|
|
163
164
|
// LYRICS-ONLY MODE: Skip stem separation
|
|
164
165
|
// ========================================
|
|
165
|
-
|
|
166
|
+
log('š¤ Lyrics-only mode: extracting vocals from existing stem file');
|
|
166
167
|
|
|
167
168
|
// Step 1: Extract vocals track to temp WAV (0-10%)
|
|
168
169
|
onProgress('extract', `[${STEPS.extract}] Extracting vocals track...`, 0);
|
|
@@ -258,7 +259,7 @@ export async function runConversion(
|
|
|
258
259
|
checkCancelled();
|
|
259
260
|
|
|
260
261
|
if (initialPrompt) {
|
|
261
|
-
|
|
262
|
+
log(`š¤ Whisper prompt: ${initialPrompt}`);
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
let whisperResult = await runWhisper(
|
|
@@ -342,7 +343,7 @@ export async function runConversion(
|
|
|
342
343
|
if (pitchData?.pitch_data) {
|
|
343
344
|
const keyResult = detectKey(pitchData);
|
|
344
345
|
if (keyResult.key !== 'unknown') {
|
|
345
|
-
|
|
346
|
+
log(
|
|
346
347
|
`šµ Detected key: ${keyResult.key} (confidence: ${(keyResult.confidence * 100).toFixed(0)}%)`
|
|
347
348
|
);
|
|
348
349
|
pitchData.detected_key = keyResult;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from '../logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* Download Manager - Handles downloading and installing AI components for Creator
|
|
3
4
|
*
|
|
@@ -106,8 +107,8 @@ function getPythonBuildUrl() {
|
|
|
106
107
|
// SECURITY FIX (#29): Add max redirects parameter to prevent infinite loops
|
|
107
108
|
function downloadFile(url, destPath, onProgress = null, maxRedirects = 5) {
|
|
108
109
|
return new Promise((resolve, reject) => {
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
log(`š„ Downloading: ${url}`);
|
|
111
|
+
log(` Destination: ${destPath}`);
|
|
111
112
|
|
|
112
113
|
let protocol;
|
|
113
114
|
try {
|
|
@@ -125,7 +126,7 @@ function downloadFile(url, destPath, onProgress = null, maxRedirects = 5) {
|
|
|
125
126
|
if (!existsSync(dir)) {
|
|
126
127
|
try {
|
|
127
128
|
mkdirSync(dir, { recursive: true });
|
|
128
|
-
|
|
129
|
+
log(`ā
Created directory: ${dir}`);
|
|
129
130
|
} catch (error) {
|
|
130
131
|
console.error(`ā Failed to create directory: ${dir}`);
|
|
131
132
|
console.error('Error:', error);
|
|
@@ -136,13 +137,13 @@ function downloadFile(url, destPath, onProgress = null, maxRedirects = 5) {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const request = protocol.get(url, (response) => {
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
log(`š” Response status: ${response.statusCode} ${response.statusMessage}`);
|
|
141
|
+
log(` Headers:`, JSON.stringify(response.headers, null, 2));
|
|
141
142
|
|
|
142
143
|
// Handle redirects
|
|
143
144
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
144
145
|
const location = response.headers.location;
|
|
145
|
-
|
|
146
|
+
log(`š Redirect to: ${location}`);
|
|
146
147
|
|
|
147
148
|
// SECURITY FIX (#29): Check redirect limit to prevent infinite loops/SSRF
|
|
148
149
|
if (maxRedirects <= 0) {
|
|
@@ -153,8 +154,10 @@ function downloadFile(url, destPath, onProgress = null, maxRedirects = 5) {
|
|
|
153
154
|
try {
|
|
154
155
|
// Handle relative redirects by resolving against original URL
|
|
155
156
|
const redirectUrl = location.startsWith('http') ? location : new URL(location, url).href;
|
|
156
|
-
|
|
157
|
-
downloadFile(redirectUrl, destPath, onProgress, maxRedirects - 1)
|
|
157
|
+
log(`š Resolved redirect URL: ${redirectUrl} (${maxRedirects - 1} redirects remaining)`);
|
|
158
|
+
downloadFile(redirectUrl, destPath, onProgress, maxRedirects - 1)
|
|
159
|
+
.then(resolve)
|
|
160
|
+
.catch(reject);
|
|
158
161
|
} catch (error) {
|
|
159
162
|
console.error(`ā Failed to resolve redirect URL`);
|
|
160
163
|
console.error('Original URL:', url);
|
|
@@ -176,7 +179,7 @@ function downloadFile(url, destPath, onProgress = null, maxRedirects = 5) {
|
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
const totalBytes = parseInt(response.headers['content-length'] || '0', 10);
|
|
179
|
-
|
|
182
|
+
log(`š¦ Content length: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
180
183
|
let downloadedBytes = 0;
|
|
181
184
|
let lastLoggedPercent = -1;
|
|
182
185
|
|
|
@@ -191,7 +194,7 @@ function downloadFile(url, destPath, onProgress = null, maxRedirects = 5) {
|
|
|
191
194
|
|
|
192
195
|
// Log progress every 10%
|
|
193
196
|
if (percent >= lastLoggedPercent + 10 || percent === 100) {
|
|
194
|
-
|
|
197
|
+
log(
|
|
195
198
|
` Progress: ${percent}% (${(downloadedBytes / 1024 / 1024).toFixed(2)} / ${(totalBytes / 1024 / 1024).toFixed(2)} MB)`
|
|
196
199
|
);
|
|
197
200
|
lastLoggedPercent = percent;
|
|
@@ -209,8 +212,8 @@ function downloadFile(url, destPath, onProgress = null, maxRedirects = 5) {
|
|
|
209
212
|
|
|
210
213
|
fileStream.on('finish', () => {
|
|
211
214
|
fileStream.close();
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
log(`ā
Download complete: ${destPath}`);
|
|
216
|
+
log(` Size: ${(downloadedBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
214
217
|
resolve();
|
|
215
218
|
});
|
|
216
219
|
|
|
@@ -391,67 +394,67 @@ function detectGPU() {
|
|
|
391
394
|
* Download and install Python
|
|
392
395
|
*/
|
|
393
396
|
export async function downloadPython(onProgress = null) {
|
|
394
|
-
|
|
397
|
+
log('š Starting Python installation...');
|
|
395
398
|
const cacheDir = getCacheDir();
|
|
396
399
|
const pythonDir = join(cacheDir, 'python');
|
|
397
|
-
|
|
398
|
-
|
|
400
|
+
log(` Cache dir: ${cacheDir}`);
|
|
401
|
+
log(` Python dir: ${pythonDir}`);
|
|
399
402
|
|
|
400
403
|
// Check if already installed
|
|
401
404
|
const pythonPath = getPythonPath();
|
|
402
|
-
|
|
405
|
+
log(` Checking for existing Python: ${pythonPath}`);
|
|
403
406
|
if (existsSync(pythonPath)) {
|
|
404
|
-
|
|
407
|
+
log('ā
Python already installed');
|
|
405
408
|
if (onProgress) onProgress('complete', 'Python already installed');
|
|
406
409
|
return { success: true, path: pythonPath };
|
|
407
410
|
}
|
|
408
411
|
|
|
409
412
|
try {
|
|
410
413
|
const url = getPythonBuildUrl();
|
|
411
|
-
|
|
414
|
+
log(`š Python download URL: ${url}`);
|
|
412
415
|
const tarPath = join(cacheDir, 'python.tar.gz');
|
|
413
416
|
|
|
414
417
|
// Download
|
|
415
|
-
|
|
418
|
+
log('š„ Starting Python download...');
|
|
416
419
|
if (onProgress) onProgress('downloading', 'Downloading Python...');
|
|
417
420
|
await downloadFile(url, tarPath, (percent) => {
|
|
418
421
|
if (onProgress) onProgress('downloading', `Downloading Python... ${percent}%`);
|
|
419
422
|
});
|
|
420
423
|
|
|
421
424
|
// Extract
|
|
422
|
-
|
|
425
|
+
log('š¦ Extracting Python...');
|
|
423
426
|
if (onProgress) onProgress('extracting', 'Extracting Python...');
|
|
424
427
|
|
|
425
428
|
// Create python directory
|
|
426
429
|
if (!existsSync(pythonDir)) {
|
|
427
|
-
|
|
430
|
+
log(` Creating Python directory: ${pythonDir}`);
|
|
428
431
|
mkdirSync(pythonDir, { recursive: true });
|
|
429
432
|
}
|
|
430
433
|
|
|
431
434
|
// Use tar to extract (available on all platforms)
|
|
432
|
-
|
|
435
|
+
log(' Loading tar module...');
|
|
433
436
|
const tar = await import('tar');
|
|
434
|
-
|
|
437
|
+
log(' Extracting tarball...');
|
|
435
438
|
await tar.extract({
|
|
436
439
|
file: tarPath,
|
|
437
440
|
cwd: pythonDir,
|
|
438
441
|
strip: 1,
|
|
439
442
|
});
|
|
440
|
-
|
|
443
|
+
log('ā
Extraction complete');
|
|
441
444
|
|
|
442
445
|
// Remove quarantine on macOS
|
|
443
446
|
if (process.platform === 'darwin') {
|
|
444
|
-
|
|
447
|
+
log('š Removing macOS quarantine attributes...');
|
|
445
448
|
try {
|
|
446
449
|
execSync(`xattr -cr "${pythonDir}"`, { stdio: 'ignore' });
|
|
447
|
-
|
|
450
|
+
log('ā
Quarantine removed');
|
|
448
451
|
} catch (error) {
|
|
449
452
|
console.warn('ā ļø Failed to remove quarantine (non-fatal):', error.message);
|
|
450
453
|
}
|
|
451
454
|
}
|
|
452
455
|
|
|
453
456
|
// Clean up tarball
|
|
454
|
-
|
|
457
|
+
log('š§¹ Cleaning up tarball...');
|
|
455
458
|
rmSync(tarPath, { force: true });
|
|
456
459
|
|
|
457
460
|
// Upgrade pip and setuptools, fix common conflicts
|
|
@@ -465,7 +468,7 @@ export async function downloadPython(onProgress = null) {
|
|
|
465
468
|
// Non-fatal - coverage may not be installed
|
|
466
469
|
}
|
|
467
470
|
|
|
468
|
-
|
|
471
|
+
log('ā
Python installation complete');
|
|
469
472
|
if (onProgress) onProgress('complete', 'Python installed successfully');
|
|
470
473
|
return { success: true, path: pythonPath };
|
|
471
474
|
} catch (error) {
|
|
@@ -807,14 +810,14 @@ except Exception as e:
|
|
|
807
810
|
* Download FFmpeg binary
|
|
808
811
|
*/
|
|
809
812
|
export async function downloadFFmpeg(onProgress = null) {
|
|
810
|
-
|
|
813
|
+
log('š¬ Starting FFmpeg installation...');
|
|
811
814
|
const cacheDir = getCacheDir();
|
|
812
815
|
const binDir = join(cacheDir, 'bin');
|
|
813
|
-
|
|
816
|
+
log(` Binary dir: ${binDir}`);
|
|
814
817
|
|
|
815
818
|
if (!existsSync(binDir)) {
|
|
816
819
|
mkdirSync(binDir, { recursive: true });
|
|
817
|
-
|
|
820
|
+
log(` Created binary directory`);
|
|
818
821
|
}
|
|
819
822
|
|
|
820
823
|
const plat = process.platform;
|
|
@@ -822,13 +825,13 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
822
825
|
const ffprobeName = plat === 'win32' ? 'ffprobe.exe' : 'ffprobe';
|
|
823
826
|
const ffmpegPath = join(binDir, ffmpegName);
|
|
824
827
|
const ffprobePath = join(binDir, ffprobeName);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
+
log(` Platform: ${plat}`);
|
|
829
|
+
log(` FFmpeg path: ${ffmpegPath}`);
|
|
830
|
+
log(` FFprobe path: ${ffprobePath}`);
|
|
828
831
|
|
|
829
832
|
// Check if both already exist
|
|
830
833
|
if (existsSync(ffmpegPath) && existsSync(ffprobePath)) {
|
|
831
|
-
|
|
834
|
+
log('ā
FFmpeg and FFprobe already installed');
|
|
832
835
|
if (onProgress) onProgress('complete', 'FFmpeg already downloaded');
|
|
833
836
|
return { success: true, ffmpegPath, ffprobePath };
|
|
834
837
|
}
|
|
@@ -849,16 +852,16 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
849
852
|
// John Van Sickle builds include both ffmpeg and ffprobe
|
|
850
853
|
url = 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz';
|
|
851
854
|
}
|
|
852
|
-
|
|
855
|
+
log(`š FFmpeg download URL: ${url}`);
|
|
853
856
|
if (ffprobeUrl) {
|
|
854
|
-
|
|
857
|
+
log(`š FFprobe download URL: ${ffprobeUrl}`);
|
|
855
858
|
}
|
|
856
859
|
|
|
857
860
|
const archivePath = join(binDir, plat === 'linux' ? 'ffmpeg.tar.xz' : 'ffmpeg.zip');
|
|
858
|
-
|
|
861
|
+
log(` Archive path: ${archivePath}`);
|
|
859
862
|
|
|
860
863
|
// Download ffmpeg
|
|
861
|
-
|
|
864
|
+
log('š„ Starting FFmpeg download...');
|
|
862
865
|
if (onProgress) onProgress('downloading', 'Downloading FFmpeg...');
|
|
863
866
|
await downloadFile(url, archivePath, (percent) => {
|
|
864
867
|
if (onProgress) onProgress('downloading', `Downloading FFmpeg... ${percent}%`);
|
|
@@ -868,7 +871,7 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
868
871
|
let ffprobeArchivePath = null;
|
|
869
872
|
if (ffprobeUrl) {
|
|
870
873
|
ffprobeArchivePath = join(binDir, 'ffprobe.zip');
|
|
871
|
-
|
|
874
|
+
log('š„ Starting FFprobe download...');
|
|
872
875
|
if (onProgress) onProgress('downloading', 'Downloading FFprobe...');
|
|
873
876
|
await downloadFile(ffprobeUrl, ffprobeArchivePath, (percent) => {
|
|
874
877
|
if (onProgress) onProgress('downloading', `Downloading FFprobe... ${percent}%`);
|
|
@@ -876,7 +879,7 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
876
879
|
}
|
|
877
880
|
|
|
878
881
|
// Extract
|
|
879
|
-
|
|
882
|
+
log('š¦ Extracting FFmpeg...');
|
|
880
883
|
if (onProgress) onProgress('extracting', 'Extracting FFmpeg...');
|
|
881
884
|
|
|
882
885
|
// Extract and find ffmpeg binary
|
|
@@ -885,15 +888,15 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
885
888
|
const tempDir = mkdtempSync(join(tmpdir(), 'ffmpeg-'));
|
|
886
889
|
|
|
887
890
|
try {
|
|
888
|
-
|
|
891
|
+
log(` Extracting to temp dir: ${tempDir}`);
|
|
889
892
|
if (plat === 'linux') {
|
|
890
|
-
|
|
893
|
+
log(' Using tar to extract...');
|
|
891
894
|
execSync(`tar -xf "${archivePath}" -C "${tempDir}"`);
|
|
892
895
|
} else {
|
|
893
|
-
|
|
896
|
+
log(' Using yauzl to extract...');
|
|
894
897
|
await extractZip(archivePath, tempDir);
|
|
895
898
|
}
|
|
896
|
-
|
|
899
|
+
log(' Extraction complete, searching for binary...');
|
|
897
900
|
|
|
898
901
|
// Find binary recursively by name
|
|
899
902
|
const findBinary = (dir, name) => {
|
|
@@ -923,13 +926,13 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
923
926
|
|
|
924
927
|
if (ffmpegFound) {
|
|
925
928
|
const ffmpegDest = join(binDir, ffmpegName);
|
|
926
|
-
|
|
927
|
-
|
|
929
|
+
log(` Found ffmpeg: ${ffmpegFound}`);
|
|
930
|
+
log(` Copying to: ${ffmpegDest}`);
|
|
928
931
|
copyFileSync(ffmpegFound, ffmpegDest);
|
|
929
932
|
if (plat !== 'win32') {
|
|
930
933
|
chmodSync(ffmpegDest, 0o755);
|
|
931
934
|
}
|
|
932
|
-
|
|
935
|
+
log('ā
ffmpeg binary installed');
|
|
933
936
|
} else {
|
|
934
937
|
console.error('ā ffmpeg binary not found in archive');
|
|
935
938
|
console.error(` Searched in: ${tempDir}`);
|
|
@@ -937,26 +940,26 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
937
940
|
}
|
|
938
941
|
|
|
939
942
|
if (ffprobeFound) {
|
|
940
|
-
|
|
941
|
-
|
|
943
|
+
log(` Found ffprobe: ${ffprobeFound}`);
|
|
944
|
+
log(` Copying to: ${ffprobePath}`);
|
|
942
945
|
copyFileSync(ffprobeFound, ffprobePath);
|
|
943
946
|
if (plat !== 'win32') {
|
|
944
947
|
chmodSync(ffprobePath, 0o755);
|
|
945
948
|
}
|
|
946
|
-
|
|
949
|
+
log('ā
ffprobe binary installed');
|
|
947
950
|
} else if (ffprobeArchivePath) {
|
|
948
951
|
// macOS: Extract ffprobe from separate archive
|
|
949
|
-
|
|
952
|
+
log('š¦ Extracting FFprobe from separate archive...');
|
|
950
953
|
const ffprobeTempDir = mkdtempSync(join(tmpdir(), 'ffprobe-'));
|
|
951
954
|
try {
|
|
952
955
|
await extractZip(ffprobeArchivePath, ffprobeTempDir);
|
|
953
956
|
const ffprobeExtracted = findBinary(ffprobeTempDir, ffprobeName);
|
|
954
957
|
if (ffprobeExtracted) {
|
|
955
|
-
|
|
956
|
-
|
|
958
|
+
log(` Found ffprobe: ${ffprobeExtracted}`);
|
|
959
|
+
log(` Copying to: ${ffprobePath}`);
|
|
957
960
|
copyFileSync(ffprobeExtracted, ffprobePath);
|
|
958
961
|
chmodSync(ffprobePath, 0o755);
|
|
959
|
-
|
|
962
|
+
log('ā
ffprobe binary installed');
|
|
960
963
|
} else {
|
|
961
964
|
console.warn('ā ļø ffprobe not found in separate archive');
|
|
962
965
|
}
|
|
@@ -971,11 +974,11 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
971
974
|
}
|
|
972
975
|
|
|
973
976
|
// Clean up
|
|
974
|
-
|
|
977
|
+
log('š§¹ Cleaning up temporary files...');
|
|
975
978
|
rmSync(tempDir, { recursive: true, force: true });
|
|
976
979
|
rmSync(archivePath, { force: true });
|
|
977
980
|
|
|
978
|
-
|
|
981
|
+
log('ā
FFmpeg installation complete');
|
|
979
982
|
if (onProgress) onProgress('complete', 'FFmpeg installed');
|
|
980
983
|
return { success: true, ffmpegPath, ffprobePath };
|
|
981
984
|
} catch (extractError) {
|
|
@@ -997,9 +1000,9 @@ export async function downloadFFmpeg(onProgress = null) {
|
|
|
997
1000
|
* Install all components in order
|
|
998
1001
|
*/
|
|
999
1002
|
export async function installAllComponents(onProgress = null) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
+
log('š Starting installation of all components...');
|
|
1004
|
+
log(` Platform: ${process.platform}`);
|
|
1005
|
+
log(` Architecture: ${process.arch}`);
|
|
1003
1006
|
|
|
1004
1007
|
const results = {};
|
|
1005
1008
|
|
|
@@ -1036,9 +1039,9 @@ export async function installAllComponents(onProgress = null) {
|
|
|
1036
1039
|
},
|
|
1037
1040
|
];
|
|
1038
1041
|
|
|
1039
|
-
|
|
1042
|
+
log(`š Installation plan: ${steps.length} components`);
|
|
1040
1043
|
steps.forEach((s, i) => {
|
|
1041
|
-
|
|
1044
|
+
log(` ${i + 1}. ${s.label} (${s.size})`);
|
|
1042
1045
|
});
|
|
1043
1046
|
|
|
1044
1047
|
let completedWeight = 0;
|
|
@@ -1050,7 +1053,7 @@ export async function installAllComponents(onProgress = null) {
|
|
|
1050
1053
|
const totalSteps = steps.length;
|
|
1051
1054
|
const action = step.action || 'Installing';
|
|
1052
1055
|
|
|
1053
|
-
|
|
1056
|
+
log(`\nš¦ [${stepNumber}/${totalSteps}] ${action} ${step.label}...`);
|
|
1054
1057
|
|
|
1055
1058
|
if (onProgress) {
|
|
1056
1059
|
const percent = Math.floor((completedWeight / totalWeight) * 100);
|
|
@@ -1094,7 +1097,7 @@ export async function installAllComponents(onProgress = null) {
|
|
|
1094
1097
|
return { success: false, failed: step.name, error: result.error, results };
|
|
1095
1098
|
}
|
|
1096
1099
|
|
|
1097
|
-
|
|
1100
|
+
log(`ā
[${stepNumber}/${totalSteps}] ${step.label} installed successfully`);
|
|
1098
1101
|
|
|
1099
1102
|
completedWeight += step.weight;
|
|
1100
1103
|
|
|
@@ -1104,10 +1107,10 @@ export async function installAllComponents(onProgress = null) {
|
|
|
1104
1107
|
}
|
|
1105
1108
|
}
|
|
1106
1109
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1110
|
+
log('\nš All components installed successfully!');
|
|
1111
|
+
log('Installation results:');
|
|
1109
1112
|
Object.entries(results).forEach(([name, result]) => {
|
|
1110
|
-
|
|
1113
|
+
log(` ${result.success ? 'ā
' : 'ā'} ${name}`);
|
|
1111
1114
|
});
|
|
1112
1115
|
|
|
1113
1116
|
if (onProgress) onProgress(100, 'All components installed successfully!');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from '../logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* FFmpeg Service - Audio conversion and processing
|
|
3
4
|
*
|
|
@@ -451,7 +452,7 @@ export function extractStemTrack(inputPath, outputPath, trackIndex, options = {}
|
|
|
451
452
|
outputPath,
|
|
452
453
|
];
|
|
453
454
|
|
|
454
|
-
|
|
455
|
+
log(`š¤ Extracting audio track ${trackIndex} to WAV...`);
|
|
455
456
|
const proc = spawn(ffmpeg, args, { timeout: 300000 }); // 5 min timeout
|
|
456
457
|
|
|
457
458
|
let stderr = '';
|
|
@@ -465,7 +466,7 @@ export function extractStemTrack(inputPath, outputPath, trackIndex, options = {}
|
|
|
465
466
|
reject(new Error(`Track extraction failed: ${stderr.slice(-500)}`));
|
|
466
467
|
return;
|
|
467
468
|
}
|
|
468
|
-
|
|
469
|
+
log(`ā
Track ${trackIndex} extracted successfully`);
|
|
469
470
|
resolve();
|
|
470
471
|
});
|
|
471
472
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Installation Logger - Intercepts
|
|
2
|
+
* Installation Logger - Intercepts log and sends to UI
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
let logCallback = null;
|
|
@@ -19,13 +19,11 @@ export function clearLogCallback() {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Log a message (sends to
|
|
22
|
+
* Log a message (sends to callback for UI display)
|
|
23
|
+
* Note: In production mode, console output is suppressed by the main logger
|
|
23
24
|
*/
|
|
24
25
|
export function log(...args) {
|
|
25
|
-
//
|
|
26
|
-
console.log(...args);
|
|
27
|
-
|
|
28
|
-
// Also send to UI if callback is set
|
|
26
|
+
// Send to UI if callback is set
|
|
29
27
|
if (logCallback) {
|
|
30
28
|
const message = args
|
|
31
29
|
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
|
|
@@ -38,7 +36,7 @@ export function log(...args) {
|
|
|
38
36
|
* Log an error (sends to console AND callback)
|
|
39
37
|
*/
|
|
40
38
|
export function error(...args) {
|
|
41
|
-
// Always log to console
|
|
39
|
+
// Always log errors to console
|
|
42
40
|
console.error(...args);
|
|
43
41
|
|
|
44
42
|
// Also send to UI if callback is set
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from '../logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* LLM Service for lyrics correction
|
|
3
4
|
* Supports OpenAI, Anthropic Claude, Google Gemini, and local LM Studio
|
|
@@ -49,7 +50,7 @@ function getLLMProvider(settings) {
|
|
|
49
50
|
*/
|
|
50
51
|
export async function correctLyrics(whisperOutput, referenceLyrics, settings) {
|
|
51
52
|
if (!settings.enabled) {
|
|
52
|
-
|
|
53
|
+
log('š¤ LLM correction disabled, using Whisper output as-is');
|
|
53
54
|
return {
|
|
54
55
|
output: whisperOutput,
|
|
55
56
|
stats: null,
|
|
@@ -57,14 +58,14 @@ export async function correctLyrics(whisperOutput, referenceLyrics, settings) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
if (!referenceLyrics || !referenceLyrics.trim()) {
|
|
60
|
-
|
|
61
|
+
log('š¤ No reference lyrics provided, skipping LLM correction');
|
|
61
62
|
return {
|
|
62
63
|
output: whisperOutput,
|
|
63
64
|
stats: null,
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
log(`š¤ Starting LLM lyrics correction with ${settings.provider}...`);
|
|
68
69
|
|
|
69
70
|
try {
|
|
70
71
|
const provider = getLLMProvider(settings);
|
|
@@ -77,9 +78,9 @@ export async function correctLyrics(whisperOutput, referenceLyrics, settings) {
|
|
|
77
78
|
missingLines,
|
|
78
79
|
} = parseCorrection(llmResponse, whisperOutput);
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
log(`ā
LLM correction complete (${corrections.length} lines changed)`);
|
|
81
82
|
if (missingLines.length > 0) {
|
|
82
|
-
|
|
83
|
+
log(`š LLM suggested ${missingLines.length} missing lines`);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
return {
|
|
@@ -101,7 +102,7 @@ export async function correctLyrics(whisperOutput, referenceLyrics, settings) {
|
|
|
101
102
|
};
|
|
102
103
|
} catch (error) {
|
|
103
104
|
console.error('ā LLM correction failed:', error.message);
|
|
104
|
-
|
|
105
|
+
log('ā ļø Falling back to original Whisper output');
|
|
105
106
|
return {
|
|
106
107
|
output: whisperOutput,
|
|
107
108
|
stats: {
|
|
@@ -304,7 +305,12 @@ export function getLLMSettings(settingsManager) {
|
|
|
304
305
|
const apiKey = llmConfig.apiKey || LLM_DEFAULTS.apiKey;
|
|
305
306
|
|
|
306
307
|
// SECURITY FIX (#25): Mask API key - only show last 4 chars to renderer
|
|
307
|
-
const maskedApiKey =
|
|
308
|
+
const maskedApiKey =
|
|
309
|
+
apiKey && apiKey.length > 8
|
|
310
|
+
? `${'ā¢'.repeat(apiKey.length - 4)}${apiKey.slice(-4)}`
|
|
311
|
+
: apiKey
|
|
312
|
+
? 'ā¢ā¢ā¢ā¢ā¢ā¢ā¢ā¢'
|
|
313
|
+
: '';
|
|
308
314
|
|
|
309
315
|
return {
|
|
310
316
|
enabled: llmConfig.enabled ?? LLM_DEFAULTS.enabled,
|