From f281809d053926759cde67ba1add9e39974977ce Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sun, 5 Apr 2026 11:20:10 +0900 Subject: [PATCH] =?UTF-8?q?Phase=209:=20About=20=ED=83=AD=20+=20=EB=85=B9?= =?UTF-8?q?=EC=9D=8C=20=EC=98=A4=EB=94=94=EC=98=A4=20WAV=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Settings: 정보(About) 탭 추가 (버전, 기술 스택, 음성 엔진 정보) - VoiceModeService: 전사 완료 시 PCM→WAV 파일 저장 ({userData}/recordings/) - WAV 헤더 생성 (16kHz, 16bit, mono) - HistoryService와 연동 가능 (audioLocalPath) - docs/phases/phase-9.md 설계 문서 작성 --- docs/phases/phase-9.md | 33 +++++++++++++++ src/main/services/VoiceModeService.ts | 49 +++++++++++++++++++++++ src/renderer/components/SettingsModal.tsx | 36 +++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 docs/phases/phase-9.md diff --git a/docs/phases/phase-9.md b/docs/phases/phase-9.md new file mode 100644 index 0000000..b4db47f --- /dev/null +++ b/docs/phases/phase-9.md @@ -0,0 +1,33 @@ +# Phase 9: 품질 보강 + 사용자 경험 개선 + +## 목표 +Phase 8에서 핵심 파이프라인(핫키→녹음→전사→삽입)이 동작하게 되었으므로, +사용자 경험을 개선하는 보조 기능들을 추가한다. + +## 태스크 + +### 9.1 Settings About 탭 +- 앱 버전, Electron 버전 표시 +- 기술 스택 정보 (Whisper, Ollama, SoX) +- 프로젝트 링크 + +### 9.2 마이크 테스트 UI +- Settings Audio 탭에 "테스트" 버튼 추가 +- 버튼 누르면 3초간 마이크 캡처 → 레벨 바 표시 +- AudioCaptureService의 audio-level 이벤트 활용 + +### 9.3 녹음 오디오 파일 저장 +- VoiceModeService에서 세션 완료 시 오디오 버퍼를 WAV 파일로 저장 +- 저장 경로: `{userData}/recordings/{sessionId}.wav` +- HistoryService create 시 audioLocalPath 기록 +- History UI에서 재생 버튼 (향후) + +### 9.4 RecordingTip thinking 상태 개선 +- 전사 중 "처리 중..." 텍스트 표시 +- 프로그레스 바 점근 수렴 애니메이션 + +## 완료 조건 +- Settings About 탭에 버전 표시 +- 마이크 테스트 버튼 동작 +- 전사 완료 시 WAV 파일 저장 +- typecheck + test 통과 diff --git a/src/main/services/VoiceModeService.ts b/src/main/services/VoiceModeService.ts index 781ff3e..dafaa33 100644 --- a/src/main/services/VoiceModeService.ts +++ b/src/main/services/VoiceModeService.ts @@ -5,6 +5,9 @@ import { EventEmitter } from 'events' import { randomUUID } from 'crypto' +import { writeFile, mkdir } from 'fs/promises' +import { join } from 'path' +import { app } from 'electron' import { getLogger } from './LoggerService' import { getAudioCaptureService } from './AudioCaptureService' import { getLocalSTTService } from './LocalSTTService' @@ -97,6 +100,9 @@ class VoiceModeService extends EventEmitter { private _audioBuffer: Buffer[] = [] private _audioBufferBytes = 0 + // 녹음 오디오 저장용 + private _lastAudioBuffer: Buffer | null = null + // 에러 가드 private _errorEmitted = false @@ -404,6 +410,7 @@ class VoiceModeService extends EventEmitter { this._setRecognitionState(RecognitionState.RECOGNIZING) const merged = Buffer.concat(this._audioBuffer) + this._lastAudioBuffer = merged // WAV 저장용 복사본 this._audioBuffer = [] this._audioBufferBytes = 0 @@ -497,6 +504,48 @@ class VoiceModeService extends EventEmitter { showResultPopup(finalText, 10000) } } + + // 녹음 오디오 WAV 파일 저장 (비동기, 실패해도 무시) + this._saveAudioFile(session.id) + } + + /** PCM 버퍼를 WAV 파일로 저장 */ + private async _saveAudioFile(sessionId: string): Promise { + if (!this._lastAudioBuffer || this._lastAudioBuffer.length === 0) return + + try { + const recordingsDir = join(app.getPath('userData'), 'recordings') + await mkdir(recordingsDir, { recursive: true }) + + const wavPath = join(recordingsDir, `${sessionId}.wav`) + const pcmData = this._lastAudioBuffer + + // WAV 헤더 생성 (16kHz, 16bit, mono) + const header = Buffer.alloc(44) + const dataSize = pcmData.length + const fileSize = dataSize + 36 + + header.write('RIFF', 0) + header.writeUInt32LE(fileSize, 4) + header.write('WAVE', 8) + header.write('fmt ', 12) + header.writeUInt32LE(16, 16) // fmt chunk size + header.writeUInt16LE(1, 20) // PCM format + header.writeUInt16LE(1, 22) // mono + header.writeUInt32LE(16000, 24) // sample rate + header.writeUInt32LE(32000, 28) // byte rate (16000 * 2) + header.writeUInt16LE(2, 32) // block align + header.writeUInt16LE(16, 34) // bits per sample + header.write('data', 36) + header.writeUInt32LE(dataSize, 40) + + await writeFile(wavPath, Buffer.concat([header, pcmData])) + logger.info(`Audio saved: ${wavPath} (${Math.round(dataSize / 1024)}KB)`) + } catch (error) { + logger.warn(`Audio save failed: ${error instanceof Error ? error.message : String(error)}`) + } finally { + this._lastAudioBuffer = null + } } private _cancelSession(reason: 'user' | 'timeout' | 'too-short'): void { diff --git a/src/renderer/components/SettingsModal.tsx b/src/renderer/components/SettingsModal.tsx index c3d4cd5..fb977e8 100644 --- a/src/renderer/components/SettingsModal.tsx +++ b/src/renderer/components/SettingsModal.tsx @@ -283,6 +283,7 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac + {/* ── 일반 탭 ─────────────────────────────── */} @@ -518,6 +519,41 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac + + {/* ── 정보 탭 ──────────────────────────────── */} + + + + D3RO-VOICE + + + + 버전 + v1.0.0 + + + + 기술 스택 + + Electron + React 19 + MUI 7 + TypeScript + + + + + 음성 엔진 + + STT: faster-whisper (로컬) / LLM: Ollama (로컬) + + + + + + + Speakly 리버스엔지니어링 노하우 기반 로컬 AI 음성 어시스턴트. + 클라우드 의존성 없이 완전 로컬로 동작합니다. + + +