Phase 4 구현: Ollama LLM 연동 (다듬기, 번역, 스트리밍)

- LocalLLMService: Ollama REST API, 스트리밍 NDJSON 파싱, 5초 가용성 폴링
- 시스템 프롬프트: refine/translate/summarize/grammar/expand/custom 6개 액션
- VoiceModeService: 전사→LLM 후처리→텍스트 삽입, LLM 실패 시 원본 폴백
- LLM IPC 핸들러: status/models/process/cancel/serverUrl 8개
- StatusBar: Ollama 연결 상태 + 활성 모델 표시
- Preload: llm API 섹션 추가
- Bootstrap: llm-polling 초기화 단계 추가
This commit is contained in:
Yun Chan 2026-04-05 02:18:26 +09:00
parent 517210af2f
commit 4a5cf6c819
10 changed files with 663 additions and 9 deletions

View file

@ -13,6 +13,7 @@ import { getHotkeyService } from './HotkeyService'
import type { HotkeyConfig } from './HotkeyService'
import { configGet } from './ConfigService'
import { getTextInsertService } from './TextInsertService'
import { getLocalLLMService } from './LocalLLMService'
import { D3ROError, ErrorCode } from '@shared/errors'
import { TIMING } from '@shared/constants'
import { RecognitionState, AudioState } from '@shared/types'
@ -407,8 +408,14 @@ class VoiceModeService extends EventEmitter {
this.emit('transcription-update', { text: result.text, isFinal: true })
// Phase 2: LLM 후처리 없이 바로 완료
this._completeSession(result.text)
// LLM 후처리
const llmAction = configGet('defaultLLMAction')
if (llmAction !== 'refine' || !getLocalLLMService().isAvailable()) {
// LLM 미가용이거나 기본 액션이면 원본 텍스트로 완료
this._completeSession(result.text)
} else {
await this._processWithLLM(result.text)
}
} catch (error) {
if (this._isInTerminalState()) return
this._handleError(
@ -420,6 +427,35 @@ class VoiceModeService extends EventEmitter {
}
}
// ── LLM 후처리 ─────────────────────────────────────────
private async _processWithLLM(transcribedText: string): Promise<void> {
if (this._isInTerminalState()) return
// RECOGNIZING 상태 유지 (UI에서 thinking으로 표시됨)
try {
const llm = getLocalLLMService()
const action = configGet('defaultLLMAction')
logger.info(`Processing with LLM (action: ${action})`)
const processedText = await llm.processText(transcribedText, action)
if (this._isInTerminalState()) return
if (this._session) {
this._session.processedText = processedText
}
this._completeSession(processedText)
} catch (error) {
if (this._isInTerminalState()) return
logger.warn(`LLM processing failed, using original text: ${error instanceof Error ? error.message : String(error)}`)
// LLM 실패 시 원본 텍스트로 폴백
this._completeSession(transcribedText)
}
}
// ── 세션 완료/취소 ─────────────────────────────────────
private async _completeSession(finalText: string): Promise<void> {