d3ro-voice/apps/desktop/src/main/services/stt/drivers/DeepgramDriver.ts
Yun Chan 708e20f747
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
2026-08-20 11:12:05 +09:00

178 lines
5.7 KiB
TypeScript

// 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<TranscriptionResult> {
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}`,
}
}
}
}