feat(conversation): OpenAI gpt-realtime-2.1 라이브 음성 대화 Premium 백엔드

- realtime-token Edge Function: 티어 검증 + realtime_session 쿼터 + ephemeral key 발급
- useRealtimeConversation 훅: WebRTC 직결 (마이크 → OpenAI, 오디오 자동 재생)
- VoiceConversationPage: conversationBackend 분기 + 연결 실패 시 로컬 fallback
- AppConfig.conversationBackend + SettingsModal 음성 대화 엔진 선택
- ErrorCode 799 ConversationRealtimeTokenFailed, i18n ko/en
This commit is contained in:
Yun Chan 2026-07-21 12:10:13 +09:00
parent 983c60cda2
commit 8f7d300b89
15 changed files with 762 additions and 40 deletions

View file

@ -5,8 +5,13 @@ import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@d3ro/core/errors'
import { getVoiceConversationService } from '../services/VoiceConversationService'
import { getCloudSyncService } from '../services/CloudSyncService'
import { getLogger } from '../services/LoggerService'
import type { ConversationSendParams } from '@d3ro/core/types'
import type {
ConversationSendParams,
RealtimeTokenParams,
RealtimeTokenResult,
} from '@d3ro/core/types'
const logger = getLogger('voice-conversation-handlers')
@ -84,6 +89,47 @@ export function registerVoiceConversationHandlers(): void {
}
})
// OpenAI Realtime ephemeral token — 렌더러가 WebRTC 직결에 사용
ipcMain.handle(
IPC_CHANNELS.VOICE_CONVERSATION.GET_REALTIME_TOKEN,
async (_event, params: RealtimeTokenParams) => {
try {
const { data, error } = await getCloudSyncService().invokeFunction(
'realtime-token',
{ ...params },
)
if (error) {
return ipcError(ErrorCode.ConversationRealtimeTokenFailed, error.message)
}
const raw = data as {
value?: string
expires_at?: number
model?: string
tier?: string
error?: string
message?: string
}
if (!raw.value) {
return ipcError(
ErrorCode.ConversationRealtimeTokenFailed,
raw.error ?? raw.message ?? 'No token in response',
)
}
const result: RealtimeTokenResult = {
value: raw.value,
expiresAt: raw.expires_at ?? 0,
model: raw.model ?? 'unknown',
tier: (raw.tier as RealtimeTokenResult['tier']) ?? 'free',
}
return ipcSuccess(result)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Realtime token request failed:', msg)
return ipcError(ErrorCode.ConversationRealtimeTokenFailed, msg)
}
},
)
// finishListening — 렌더러에서 녹음 종료 버튼 클릭 시
ipcMain.handle('voiceConversation:finishListening', async () => {
try {

View file

@ -31,6 +31,8 @@ const CONFIG_DEFAULTS: AppConfig = {
// Phase 3.2: 기본값은 'local' — 누구나 로그인 없이 로컬 Ollama로 쓸 수 있는
// 엔트리 전략. 사용자가 Settings에서 'premium'으로 전환 시 로그인 + 구독 필요.
llmBackend: 'local' as const,
// 라이브 음성 대화 백엔드 — 'realtime'은 로그인+구독 필요 (OpenAI Realtime WebRTC)
conversationBackend: 'local' as const,
defaultLLMAction: 'refine',
dictationShortcut: {
keyCode: 0xa5, // Right Alt