feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
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
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
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
178
apps/desktop/src/main/services/stt/drivers/DeepgramDriver.ts
Normal file
178
apps/desktop/src/main/services/stt/drivers/DeepgramDriver.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
// 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}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue