fix(voice): 받아쓰기 파이프라인 4버그 수정 + 실시간 부분 전사

- press가 STT 초기화를 await하며 action queue 점유 → release 수십 초 지연·
  유령 세션 반복 버그 수정 (initSTT fire-and-forget)
- 프리플라이트: STT 모델 미설치 시 즉시 에러 + 메인 UI 경고 + 온보딩 오픈
- 사이드카: 기동 중 프로세스 사망 시 30초 대기 없이 즉시 실패,
  restartCount 리셋, error 리스너 부재 미처리 예외 방지
- 실시간 부분 전사: 1.5s 간격 interim → RecordingTip에 말하는 내용 미리보기
- Ollama 미가용 후처리 스킵 시 warning 배너, voice:error 브로드캐스트 신설
This commit is contained in:
Yun Chan 2026-07-21 20:56:40 +09:00
parent e0a1864e60
commit fd46ac7b14
13 changed files with 280 additions and 16 deletions

View file

@ -70,6 +70,8 @@ export function AppLayout(): React.ReactElement {
const [currentTier, setCurrentTier] = useState<LicenseTier>('free')
// Phase 3.2: Premium LLM fallback 배너 (상단 중앙, 8초, warning filled)
const [fallbackMsg, setFallbackMsg] = useState<string | null>(null)
// 음성 세션 에러/경고 배너 (모델 미설치, 엔진 실패, LLM 스킵 등)
const [voiceAlert, setVoiceAlert] = useState<{ message: string; severity: 'error' | 'warning' } | null>(null)
// 첫 실행 감지 — 로컬 모드 entry point에서 온보딩 자동 표시
useEffect(() => {
@ -101,6 +103,22 @@ export function AppLayout(): React.ReactElement {
const handleOpenSettings = () => setSettingsOpen(true)
window.addEventListener('d3ro:open-settings', handleOpenSettings)
// 음성 세션 에러/경고 — 단축키 녹음 실패를 메인 UI에서도 명확히 알림.
// 모델 미설치(101)는 온보딩 모달을 함께 연다.
const unsubVoiceError = window.electronAPI.voice.onError((e) => {
const severity = e.severity ?? 'error'
let message = e.message
if (e.errorCode === 101) {
message = t('voice.error.modelMissing')
setOnboardingOpen(true)
} else if (e.errorCode === 103 || (e.errorCode >= 130 && e.errorCode <= 132)) {
message = t('voice.error.engine')
} else if (severity === 'warning' && e.errorCode === 300) {
message = t('voice.warning.llmSkipped')
}
setVoiceAlert({ message, severity })
})
// Phase 3.2: Premium LLM fallback/upgrade 이벤트 구독
const unsubFallback = window.electronAPI.llm.premium.onFallback((e) => {
setFallbackMsg(e.reason)
@ -114,10 +132,11 @@ export function AppLayout(): React.ReactElement {
unsubUpgrade()
unsubFallback()
unsubUpgradeReq()
unsubVoiceError()
window.removeEventListener('d3ro:open-license-modal', handleOpenLicenseModal)
window.removeEventListener('d3ro:open-settings', handleOpenSettings)
}
}, [])
}, [t])
return (
<Box sx={{ display: 'flex', height: '100vh', flexDirection: 'column', bgcolor: d3roPalette.bg.app }}>
@ -300,6 +319,23 @@ export function AppLayout(): React.ReactElement {
{fallbackMsg}
</Alert>
</Snackbar>
{/* 음성 세션 에러/경고 배너 — 모델 미설치·엔진 실패·LLM 스킵 알림 */}
<Snackbar
open={voiceAlert !== null}
autoHideDuration={6000}
onClose={() => setVoiceAlert(null)}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert
severity={voiceAlert?.severity ?? 'error'}
variant="filled"
onClose={() => setVoiceAlert(null)}
sx={{ width: '100%' }}
>
{voiceAlert?.message ?? ''}
</Alert>
</Snackbar>
</Box>
)
}