feat(V2-1a): Monorepo 구조 전환 — apps/desktop으로 V1 이동

- npm workspaces 루트 (apps/*, packages/*) 세팅
- V1 전체를 apps/desktop/으로 git mv (src, resources, tests, sidecar,
  scripts, electron.vite.config.ts, electron-builder.yml, vitest.config.ts,
  tsconfig.node.json, tsconfig.web.json)
- apps/desktop/package.json 신규 (name=@d3ro/desktop)
- productName: 'd3ro-voice' 명시 — app.getName()을 고정하여 userData 경로
  %APPDATA%\d3ro-voice\ 그대로 유지 (기존 DB/설정 연속성 보장)
- 루트 package.json을 workspace 루트로 재구성, 공통 devDep만 유지
  (typescript, eslint, prettier)
- turbo.json, tsconfig.base.json 추가 (Turborepo 자체 설치는 별도 sub-phase)
- memory/project_status.md 생성 (규칙 13)

검증:
- npm run typecheck 통과
- npm run build 통과 (electron-vite main+preload+renderer)
- npm run dev 실제 실행 → DB/핫키/Ollama 자동 실행 모두 정상
This commit is contained in:
yunchan8804 2026-04-08 14:04:41 +09:00
parent 3a160b9032
commit 45a580878a
178 changed files with 214 additions and 0 deletions

View file

@ -0,0 +1,147 @@
/**
* D3RO-VOICE 효과음 WAV 파일 생성 스크립트
*
* 생성 파일:
* resources/sounds/recording-start.wav 상승 (440880Hz, 150ms)
* resources/sounds/recording-stop.wav 하강 (880440Hz, 150ms)
* resources/sounds/error.wav 저음 비프 2 (220Hz, 200ms×2)
*
* WAV 포맷: 16kHz, mono, 16bit PCM
*/
const fs = require('fs');
const path = require('path');
const SAMPLE_RATE = 16000;
const BIT_DEPTH = 16;
const NUM_CHANNELS = 1;
const BYTES_PER_SAMPLE = BIT_DEPTH / 8;
/**
* 사인파 샘플 생성 (주파수 선형 스윕 지원)
* @param {number} durationMs - 길이 (ms)
* @param {number} freqStart - 시작 주파수 (Hz)
* @param {number} freqEnd - 주파수 (Hz)
* @param {number} volume - 볼륨 (0.0~1.0)
* @returns {Int16Array}
*/
function generateTone(durationMs, freqStart, freqEnd, volume = 0.6) {
const numSamples = Math.floor((SAMPLE_RATE * durationMs) / 1000);
const samples = new Int16Array(numSamples);
const maxVal = 32767 * volume;
// 페이드 인/아웃 길이 (클릭 방지)
const fadeSamples = Math.min(Math.floor(numSamples * 0.05), 80);
let phase = 0;
for (let i = 0; i < numSamples; i++) {
const t = i / numSamples;
const freq = freqStart + (freqEnd - freqStart) * t;
// 페이드 인/아웃 엔벨로프
let envelope = 1.0;
if (i < fadeSamples) {
envelope = i / fadeSamples;
} else if (i > numSamples - fadeSamples) {
envelope = (numSamples - i) / fadeSamples;
}
samples[i] = Math.round(Math.sin(phase) * maxVal * envelope);
phase += (2 * Math.PI * freq) / SAMPLE_RATE;
}
return samples;
}
/**
* 무음 생성
* @param {number} durationMs
* @returns {Int16Array}
*/
function generateSilence(durationMs) {
const numSamples = Math.floor((SAMPLE_RATE * durationMs) / 1000);
return new Int16Array(numSamples);
}
/**
* 여러 샘플 배열을 이어붙임
* @param {Int16Array[]} arrays
* @returns {Int16Array}
*/
function concatenate(arrays) {
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
const result = new Int16Array(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
/**
* PCM 데이터를 WAV 파일 버퍼로 변환
* @param {Int16Array} samples
* @returns {Buffer}
*/
function createWavBuffer(samples) {
const dataSize = samples.length * BYTES_PER_SAMPLE;
const headerSize = 44;
const buffer = Buffer.alloc(headerSize + dataSize);
// RIFF header
buffer.write('RIFF', 0);
buffer.writeUInt32LE(headerSize - 8 + dataSize, 4);
buffer.write('WAVE', 8);
// fmt chunk
buffer.write('fmt ', 12);
buffer.writeUInt32LE(16, 16); // chunk size
buffer.writeUInt16LE(1, 20); // PCM format
buffer.writeUInt16LE(NUM_CHANNELS, 22);
buffer.writeUInt32LE(SAMPLE_RATE, 24);
buffer.writeUInt32LE(SAMPLE_RATE * NUM_CHANNELS * BYTES_PER_SAMPLE, 28); // byte rate
buffer.writeUInt16LE(NUM_CHANNELS * BYTES_PER_SAMPLE, 32); // block align
buffer.writeUInt16LE(BIT_DEPTH, 34);
// data chunk
buffer.write('data', 36);
buffer.writeUInt32LE(dataSize, 40);
// PCM data (Int16 little-endian)
const pcmBuffer = Buffer.from(samples.buffer, samples.byteOffset, samples.byteLength);
pcmBuffer.copy(buffer, headerSize);
return buffer;
}
// --- 효과음 생성 ---
const outputDir = path.resolve(__dirname, '..', 'resources', 'sounds');
fs.mkdirSync(outputDir, { recursive: true });
// 1. recording-start.wav: 상승 톤 440→880Hz, 150ms
const startSamples = generateTone(150, 440, 880, 0.5);
const startWav = createWavBuffer(startSamples);
const startPath = path.join(outputDir, 'recording-start.wav');
fs.writeFileSync(startPath, startWav);
console.log(`Created: ${startPath} (${startWav.length} bytes)`);
// 2. recording-stop.wav: 하강 톤 880→440Hz, 150ms
const stopSamples = generateTone(150, 880, 440, 0.5);
const stopWav = createWavBuffer(stopSamples);
const stopPath = path.join(outputDir, 'recording-stop.wav');
fs.writeFileSync(stopPath, stopWav);
console.log(`Created: ${stopPath} (${stopWav.length} bytes)`);
// 3. error.wav: 220Hz 비프 200ms × 2회, 중간 100ms 무음
const beep1 = generateTone(200, 220, 220, 0.5);
const gap = generateSilence(100);
const beep2 = generateTone(200, 220, 220, 0.5);
const errorSamples = concatenate([beep1, gap, beep2]);
const errorWav = createWavBuffer(errorSamples);
const errorPath = path.join(outputDir, 'error.wav');
fs.writeFileSync(errorPath, errorWav);
console.log(`Created: ${errorPath} (${errorWav.length} bytes)`);
console.log('\nDone! All 3 sound files generated.');