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
178 lines
5.7 KiB
TypeScript
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}`,
|
|
}
|
|
}
|
|
}
|
|
}
|