From 18d5fef594644fe99aa8dac567910e854ee76e63 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sun, 5 Apr 2026 10:23:54 +0900 Subject: [PATCH] =?UTF-8?q?VoiceModeService=20=E2=86=94=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EC=97=B0=EB=8F=99=20=EA=B5=AC=ED=98=84=20(Speakly?= =?UTF-8?q?=20RE=20=ED=8C=A8=ED=84=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - startSession: RecordingTip 표시 (recording 상태) - 오디오 레벨: sendAudioLevelToTip → 웨이브 바 애니메이션 - stopSession: RecordingTip → thinking 상태 전환 - completeSession: RecordingTip 숨김 + 삽입 실패 시 ResultPopup 표시 - cancelSession: RecordingTip 즉시 숨김 - handleError: RecordingTip에 에러 표시 → 3초 후 자동 숨김 → idle 복귀 - CLAUDE.md: 핵심 병목 + Speakly 파이프라인 패턴 문서화 --- CLAUDE.md | 41 +++++++++++++++++++++++++-- src/main/services/VoiceModeService.ts | 34 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3c3e0ce..d0ccc65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,10 +78,47 @@ npm run typecheck # tsc --noEmit ## 현재 상태 Phase: 8 진행 중 (Phase 1~7.5 + 3.5 완료, Phase 8 UI+기능 보강 진행) -마지막 작업: Phase 8 — SSOT 리팩토링 + HotkeyRecordModal + Dashboard 재작성 -다음 작업: **녹음→전사 파이프라인 E2E 테스트** (핫키 변경 후 실제 사이클 확인) +마지막 작업: SoX `-t waveaudio` 수정 → 마이크 캡처 동작 확인, VAD 에러 방어 +다음 작업: **VoiceModeService ↔ 팝업 연동** (RE 패턴 기반 — 핵심 병목) 차단 이슈: @nut-tree-fork/nut-js 포크 사용 +### ⚠️ 핵심 병목: VoiceModeService ↔ 팝업 연동 미구현 +Speakly RE 재조사로 발견된 치명적 갭: +1. **VoiceModeService가 RecordingTip/ResultPopup과 전혀 연동 안 됨** + - 녹음 시작 시 팝업 표시 안 함, 에러 시 팝업 숨김 안 함 + - Speakly: VoiceModeService가 이벤트 발행 → main/index.js에서 RecordingTipWindow 메서드 호출 +2. **RecordingTipUIState 상태 머신 없음** + - Speakly: OPENING→RECORDING→THINKING→COMPLETED/ERROR/CANCELLED + - D3RO: 상태 없이 수동 show/hide만 +3. **session-aware 윈도우 관리 없음** + - Speakly: showForSession(sessionId) / hideForSession(sessionId) — race condition 방지 +4. **에러 후 복구 안 됨** + - 에러 시 RecordingTip이 "실패" 상태로 남아있고, 핫키가 안 먹힘 + - Speakly: 에러 표시 → 3초 후 자동 숨김 → idle 복귀 + +### Speakly 음성 파이프라인 패턴 (RE 결과) +``` +[핫키 press] → Action Queue에 enqueue → processActionQueue() + → startRecording(): + 1. 시스템 오디오 뮤트 (500ms 후) + 2. emit('recording-tip:show-opening') → RecordingTip 표시 + 3. MicNativeService.start() → 오디오 스트리밍 + 4. emit('recording-tip:mic-ready') → 웨이브 바 활성 + 5. RecognitionSession 생성 → STT 연결 + → 녹음 중: 오디오 레벨 → sendAudioLevel() → 웨이브 바 + +[핫키 release] → stopAndProcess(): + 1. MicNativeService.stop() + 2. accidentalPress 체크 (< 700ms → 취소) + 3. 언뮤트 + 종료 효과음 + 4. RecordingTip → thinking 상태 + 5. session.commitAndWait() → 전사 결과 대기 + 6. 전사 완료 → 텍스트 삽입 시도 + 7. 삽입 성공 → RecordingTip 숨김 + 8. 삽입 실패 → ResultPopup 표시 + 9. 에러 → RecordingTip에 에러 표시 → 3초 후 숨김 +``` + ### DS 컴포넌트 현황 (src/renderer/components/ds/) - CrtDisplay.tsx: WebGL CRT 셰이더 (파형+스캔라인+비네팅+글리치) ✅ - InstrumentPanel.tsx: 메탈 섀시 컨테이너 (각인 텍스트, 노이즈) ✅ diff --git a/src/main/services/VoiceModeService.ts b/src/main/services/VoiceModeService.ts index 03a9783..41d01da 100644 --- a/src/main/services/VoiceModeService.ts +++ b/src/main/services/VoiceModeService.ts @@ -18,6 +18,13 @@ import { D3ROError, ErrorCode } from '@shared/errors' import { TIMING } from '@shared/constants' import { RecognitionState, AudioState } from '@shared/types' import type { VoiceMode, VoiceState } from '@shared/types' +import { + showRecordingTip, + hideRecordingTip, + updateRecordingTipState, + sendAudioLevelToTip, + showResultPopup, +} from '../windows/WindowManager' const logger = getLogger('VoiceModeService') @@ -187,6 +194,9 @@ class VoiceModeService extends EventEmitter { this.emit('session-started', { session: this._session }) logger.info(`Session started: ${this._session.id} (mode: ${mode})`) + // Speakly 패턴: 녹음 시작 시 즉시 RecordingTip 표시 + showRecordingTip('recording') + // 이중 조건 플러시: STT 초기화 + 오디오 캡처를 병렬 시작 const sttPromise = this._initSTT() const audioPromise = this._startAudio() @@ -212,6 +222,9 @@ class VoiceModeService extends EventEmitter { // 오디오 캡처 중지 await this._stopAudio() + // Speakly 패턴: 녹음 종료 후 thinking 상태로 전환 + updateRecordingTipState('thinking') + // 버퍼가 있으면 STT에 전달 if (this._audioBuffer.length > 0 && this._sttReady) { await this._transcribe() @@ -321,6 +334,8 @@ class VoiceModeService extends EventEmitter { this._audioLevelHandler = (payload) => { this.emit('audio-level', { level: payload.level }) + // Speakly 패턴: 오디오 레벨을 RecordingTip 웨이브 바에 전달 + sendAudioLevelToTip(payload.level) } audio.on('audio-data', this._audioDataHandler) @@ -467,16 +482,26 @@ class VoiceModeService extends EventEmitter { const session = { ...this._session } logger.info(`Session completed: "${finalText.substring(0, 50)}${finalText.length > 50 ? '...' : ''}"`) + // Speakly 패턴: RecordingTip 숨김 + hideRecordingTip() + // 텍스트 삽입 (autoInsert 설정 확인) + let insertSuccess = false if (configGet('autoInsert') && finalText.length > 0) { try { const insertMethod = configGet('insertMethod') await getTextInsertService().insertText(finalText, insertMethod) + insertSuccess = true } catch (error) { logger.warn(`Text insert failed: ${error instanceof Error ? error.message : String(error)}`) } } + // Speakly 패턴: 삽입 실패 시 ResultPopup에 텍스트 표시 + if (!insertSuccess && finalText.length > 0) { + showResultPopup(finalText, 10000) + } + this.emit('session-completed', { session, finalText }) // IDLE로 복귀 @@ -492,6 +517,9 @@ class VoiceModeService extends EventEmitter { const session = { ...this._session } logger.info(`Session cancelled: ${reason}`) + // Speakly 패턴: RecordingTip 즉시 숨김 + hideRecordingTip() + // 오디오 정리 this._stopAudio() @@ -528,6 +556,12 @@ class VoiceModeService extends EventEmitter { this._stopAudio() + // Speakly 패턴: RecordingTip에 에러 표시 → 3초 후 자동 숨김 + updateRecordingTipState('error', { errorMessage: error.message }) + setTimeout(() => { + hideRecordingTip() + }, 3000) + this.emit('error', { error, session: this._session ? { ...this._session } : null }) this._resetToIdle() }