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:
parent
e0a1864e60
commit
fd46ac7b14
13 changed files with 280 additions and 16 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@
|
|||
<div id="root">
|
||||
<div id="container" class="recording-tip">
|
||||
<!-- recording 상태 -->
|
||||
<div id="recording-view" class="view">
|
||||
<div id="wave-bars" class="wave-bars"></div>
|
||||
<span id="duration-text" class="duration">0:00</span>
|
||||
<div id="recording-view" class="view view-recording">
|
||||
<div class="recording-row">
|
||||
<div id="wave-bars" class="wave-bars"></div>
|
||||
<span id="duration-text" class="duration">0:00</span>
|
||||
</div>
|
||||
<!-- 실시간 부분 전사 미리보기 -->
|
||||
<div id="partial-text" class="partial-text hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- thinking 상태 -->
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
var durationText = document.getElementById('duration-text')
|
||||
var progressBar = document.getElementById('progress-bar')
|
||||
var errorText = document.getElementById('error-text')
|
||||
var partialText = document.getElementById('partial-text')
|
||||
|
||||
// ── 상태 ─────────────────────────────────────────────
|
||||
var bars = []
|
||||
|
|
@ -110,6 +111,10 @@
|
|||
durationText.textContent = '0:00'
|
||||
currentHeights.fill(MIN_HEIGHT)
|
||||
|
||||
// 이전 세션의 부분 전사 리셋
|
||||
partialText.textContent = ''
|
||||
partialText.classList.add('hidden')
|
||||
|
||||
animInterval = setInterval(updateBars, UPDATE_INTERVAL)
|
||||
durationInterval = setInterval(updateDuration, 1000)
|
||||
}
|
||||
|
|
@ -166,6 +171,17 @@
|
|||
audioLevel = data.level || 0
|
||||
})
|
||||
|
||||
// 실시간 부분 전사 — 녹음 중 말하는 내용 미리보기
|
||||
window.popupAPI.on('voice:partialTranscript', function (data) {
|
||||
if (currentState !== 'recording') return
|
||||
var text = (data && data.text) || ''
|
||||
if (text.length === 0) return
|
||||
partialText.textContent = text
|
||||
partialText.classList.remove('hidden')
|
||||
// 항상 끝부분(최근 발화)이 보이도록 스크롤
|
||||
partialText.scrollTop = partialText.scrollHeight
|
||||
})
|
||||
|
||||
// 상태 변경 (showRecordingTip + updateRecordingTipState 양쪽에서 사용)
|
||||
window.popupAPI.on('window:tipStateChanged', function (data) {
|
||||
var state = data.state
|
||||
|
|
|
|||
|
|
@ -133,3 +133,37 @@ body {
|
|||
.view.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── 실시간 부분 전사 (recording 상태 하단) ─────────────── */
|
||||
.view-recording {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
width: 288px;
|
||||
}
|
||||
|
||||
.recording-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.partial-text {
|
||||
color: var(--d3-text-primary);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
text-align: left;
|
||||
max-height: 35px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
border-top: 1px solid var(--d3-border-default);
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.partial-text.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue