// scripts/build-sidecar.mjs // faster-whisper 사이드카를 PyInstaller(onedir)로 묶어 sidecar-dist/sidecar/ 에 만든다. // // 사용: npm --prefix apps/desktop run sidecar:build // 출력: apps/desktop/sidecar-dist/sidecar/sidecar(.exe) ← electron-builder extraResources 대상 // // 주의: // - `--collect-all faster_whisper` 가 필수다. faster-whisper는 VAD용 // `assets/silero_vad_v6.onnx` 데이터 파일을 패키지 안에 두는데, 이걸 누락하면 // 번들된 앱에서 VAD 사용 시 런타임에 실패한다. // - 콘솔 모드를 유지한다(--noconsole 금지). 메인 프로세스가 stdout/stderr를 로그로 // 수집하는데, windowed 모드에서는 스트림이 사라져 진단이 불가능해진다. // 콘솔 창 깜빡임은 메인 프로세스 spawn의 windowsHide로 막는다. import { spawnSync } from 'node:child_process' import { existsSync, rmSync, statSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const desktopDir = path.resolve(scriptDir, '..') const sidecarDir = path.join(desktopDir, 'sidecar') const mainPy = path.join(sidecarDir, 'main.py') const outputDir = path.join(desktopDir, 'sidecar-dist') const isWindows = process.platform === 'win32' const exeSuffix = isWindows ? '.exe' : '' const venvPython = isWindows ? path.join(sidecarDir, '.venv', 'Scripts', 'python.exe') : path.join(sidecarDir, '.venv', 'bin', 'python3') const python = existsSync(venvPython) ? venvPython : isWindows ? 'python' : 'python3' if (!existsSync(mainPy)) { console.error(`사이드카 소스를 찾을 수 없습니다: ${mainPy}`) process.exit(1) } const probe = spawnSync( python, ['-c', 'import PyInstaller, faster_whisper; print("ok")'], { encoding: 'utf-8' }, ) if (probe.status !== 0) { console.error( [ `PyInstaller/faster-whisper를 사용할 수 없습니다 (python: ${python}).`, '먼저 사이드카 환경을 준비하세요:', ' npm --prefix apps/desktop run sidecar:setup', probe.stderr?.trim() || '', ].join('\n'), ) process.exit(1) } if (existsSync(outputDir)) { rmSync(outputDir, { recursive: true, force: true }) } const args = [ '-m', 'PyInstaller', '--name', 'sidecar', '--distpath', outputDir, '--workpath', path.join(desktopDir, 'build', 'sidecar-build'), '--specpath', path.join(desktopDir, 'build'), '--noconfirm', '--clean', // 패키지 데이터/바이너리 포함 (VAD onnx, ctranslate2 DLL 등) '--collect-all', 'faster_whisper', '--collect-all', 'ctranslate2', '--collect-all', 'tokenizers', '--collect-all', 'huggingface_hub', // uvicorn은 동적 임포트를 사용하므로 명시 '--hidden-import', 'uvicorn.logging', '--hidden-import', 'uvicorn.protocols.http', '--hidden-import', 'uvicorn.protocols.http.auto', '--hidden-import', 'uvicorn.protocols.http.h11_impl', '--hidden-import', 'uvicorn.protocols.websockets', '--hidden-import', 'uvicorn.protocols.websockets.auto', '--hidden-import', 'uvicorn.lifespan', '--hidden-import', 'uvicorn.lifespan.on', '--hidden-import', 'uvicorn.lifespan.off', mainPy, ] console.log('='.repeat(64)) console.log(`D3RO-VOICE 사이드카 빌드 (${process.platform} ${process.arch})`) console.log('='.repeat(64)) console.log(`\n$ ${python} ${args.join(' ')}\n`) const result = spawnSync(python, args, { stdio: 'inherit', cwd: sidecarDir }) if (result.status !== 0) { console.error(`\nPyInstaller 빌드 실패 (exit ${result.status ?? 'null'})`) process.exit(result.status ?? 1) } const exePath = path.join(outputDir, 'sidecar', `sidecar${exeSuffix}`) if (!existsSync(exePath)) { console.error(`\n빌드 산출물을 찾을 수 없습니다: ${exePath}`) process.exit(1) } const exeMb = statSync(exePath).size / (1024 * 1024) console.log(`\n빌드 성공: ${exePath} (${exeMb.toFixed(1)} MB)`) console.log('electron-builder가 sidecar-dist/sidecar/ 를 resources/sidecar/ 로 복사한다.')