// apps/desktop/src/main/services/stt/drivers/DeepgramDriver.ts // Deepgram Nova-3 / Nova-2 최고 정확도 STT API 드라이버 import { D3ROError, ErrorCode } from '@d3ro/core/errors' import type { ISTTDriver } from '../types' import type { TranscriptionResult, TranscribeOptions } from '../../LocalSTTService' import type { STTProviderConfig } from '@d3ro/core/types' import { pcmToWav, createProbeWav } from '../audio-utils' import { getLogger } from '../../LoggerService' const logger = getLogger('DeepgramDriver') export class DeepgramDriver implements ISTTDriver { readonly id = 'deepgram' as const readonly name = 'Deepgram (Nova-3 최고 정확도)' async transcribe( audioBuffer: Buffer, options?: TranscribeOptions, config?: STTProviderConfig ): Promise { const apiKey = config?.apiKey?.trim() if (!apiKey) { throw new D3ROError(ErrorCode.STTTranscriptionFailed, 'Deepgram API Key가 설정되지 않았습니다.') } const baseUrl = (config?.baseUrl?.trim() || 'https://api.deepgram.com').replace(/\/+$/, '') const model = config?.modelId?.trim() || 'nova-3' const searchParams = new URLSearchParams({ model, smart_format: 'true', punctuate: 'true', }) if (options?.language && options.language !== 'auto') { searchParams.set('language', options.language) } else { searchParams.set('detect_language', 'true') } if (options?.initialPrompt) { searchParams.set('keywords', options.initialPrompt.slice(0, 200)) } const endpoint = `${baseUrl}/v1/listen?${searchParams.toString()}` const startTime = Date.now() const wavBuffer = pcmToWav(audioBuffer, 16000, 1, 16) try { const response = await fetch(endpoint, { method: 'POST', headers: { Authorization: `Token ${apiKey}`, 'Content-Type': 'audio/wav', }, body: wavBuffer, signal: AbortSignal.timeout(25000), }) if (!response.ok) { const errorBody = await response.text() let errorMessage = `Deepgram API 오류 (${response.status})` try { const parsed = JSON.parse(errorBody) if (parsed.err_msg || parsed.message) { errorMessage = parsed.err_msg || parsed.message } } catch { errorMessage = errorBody || errorMessage } if (response.status === 401 || response.status === 403) { throw new D3ROError(ErrorCode.STTTranscriptionFailed, `인증 실패: Deepgram API Key를 확인하세요 (${errorMessage})`) } throw new D3ROError(ErrorCode.STTTranscriptionFailed, errorMessage) } const data = (await response.json()) as { metadata?: { duration?: number } results?: { channels?: Array<{ alternatives?: Array<{ transcript?: string confidence?: number words?: Array<{ word: string; start: number; end: number; confidence: number }> detected_language?: string }> }> } } const alt = data.results?.channels?.[0]?.alternatives?.[0] const rawText = alt?.transcript?.trim() ?? '' if (!rawText) { throw new D3ROError(ErrorCode.STTNoAudioData, '인식된 음성이 없습니다.') } const processingTime = Date.now() - startTime return { text: rawText, language: alt?.detected_language || options?.language || 'ko', duration: data.metadata?.duration || Math.round(audioBuffer.length / 2 / 16000), processingTime, segments: (alt?.words || []).map((w) => ({ text: w.word, start: w.start, end: w.end, confidence: w.confidence || alt?.confidence || 0.98, })), } } catch (err) { if (err instanceof D3ROError) throw err const msg = err instanceof Error ? err.message : String(err) logger.error('Deepgram transcribe error:', msg) throw new D3ROError(ErrorCode.STTTranscriptionFailed, `Deepgram STT 전사 실패: ${msg}`) } } async testConnection(config: STTProviderConfig): Promise<{ success: boolean; latencyMs: number; message: string }> { const apiKey = config.apiKey?.trim() if (!apiKey) { return { success: false, latencyMs: 0, message: 'Deepgram API Key를 입력해주세요.' } } const baseUrl = (config.baseUrl?.trim() || 'https://api.deepgram.com').replace(/\/+$/, '') const model = config.modelId?.trim() || 'nova-3' const endpoint = `${baseUrl}/v1/listen?model=${model}&punctuate=true` const probe = createProbeWav(300, 16000) const startTime = Date.now() try { const response = await fetch(endpoint, { method: 'POST', headers: { Authorization: `Token ${apiKey}`, 'Content-Type': 'audio/wav', }, body: probe, signal: AbortSignal.timeout(10000), }) const latencyMs = Date.now() - startTime if (response.ok) { return { success: true, latencyMs, message: `연결 성공 (지연 시간: ${latencyMs}ms, 모델: ${model})`, } } const text = await response.text() let msg = `HTTP ${response.status}` try { const json = JSON.parse(text) if (json.err_msg || json.message) msg = json.err_msg || json.message } catch { msg = text || msg } return { success: false, latencyMs, message: `연결 실패: ${msg}`, } } catch (err) { const latencyMs = Date.now() - startTime const msg = err instanceof Error ? err.message : String(err) return { success: false, latencyMs, message: `연결 실패: ${msg}`, } } } }