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

This commit is contained in:
Yun Chan 2026-08-20 11:12:05 +09:00
parent 5cd1de6859
commit 708e20f747
406 changed files with 42464 additions and 6199 deletions

View file

@ -0,0 +1,282 @@
// apps/desktop/src/main/services/stt/STTManager.ts
// 통합 STT 매니저: Local Whisper 및 클라우드 AI 서비스 6종 디스패처 및 자동 폴백
import { EventEmitter } from 'events'
import { getLogger } from '../LoggerService'
import { configGet, configSet } from '../ConfigService'
import { getLocalSTTService } from '../LocalSTTService'
import type { TranscriptionResult, TranscribeOptions } from '../LocalSTTService'
import type {
STTProviderType,
STTProviderInfo,
STTProviderConfig,
TestSTTConnectionParams,
TestSTTConnectionResult,
STTStatus,
} from '@d3ro/core/types'
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
import type { ISTTDriver } from './types'
import { OpenAIDriver } from './drivers/OpenAIDriver'
import { GroqDriver } from './drivers/GroqDriver'
import { DeepgramDriver } from './drivers/DeepgramDriver'
import { AssemblyAIDriver } from './drivers/AssemblyAIDriver'
import { GoogleDriver } from './drivers/GoogleDriver'
import { CustomDriver } from './drivers/CustomDriver'
import { D3ROCloudDriver } from './drivers/D3ROCloudDriver'
const logger = getLogger('STTManager')
export const STT_PROVIDERS_META: STTProviderInfo[] = [
{
id: 'local',
name: 'Local Whisper',
description: '100% 완전 오프라인, 무료, 프라이버시 보호',
badge: 'Offline · Free',
requiresApiKey: false,
defaultModel: 'large-v3-turbo',
models: ['tiny', 'base', 'small', 'medium', 'large-v3', 'large-v3-turbo'],
isCloud: false,
},
{
id: 'd3ro-cloud',
name: 'D3RO Cloud STT (Managed)',
description: 'D3RO 클라우드 매니지드 전사 서비스 (키 설정 불필요)',
badge: 'Cloud · Zero Config',
requiresApiKey: false,
defaultModel: 'default',
defaultBaseUrl: 'http://localhost:5000',
models: ['default', 'whisper-large-v3-turbo', 'nova-3', 'gemini-2.0-flash'],
isCloud: true,
},
{
id: 'openai',
name: 'OpenAI Whisper',
description: '공식 OpenAI 고품질 다국어 음성 인식',
badge: 'Standard · High Accuracy',
requiresApiKey: true,
defaultModel: 'whisper-1',
defaultBaseUrl: 'https://api.openai.com/v1',
models: ['whisper-1', 'gpt-4o-audio-preview'],
isCloud: true,
},
{
id: 'groq',
name: 'Groq Whisper LPU',
description: 'LPU 가속 기반 초저지연(~200ms) 초고속 인식',
badge: 'Ultra Fast (~200ms)',
requiresApiKey: true,
defaultModel: 'whisper-large-v3-turbo',
defaultBaseUrl: 'https://api.groq.com/openai/v1',
models: ['whisper-large-v3-turbo', 'whisper-large-v3', 'distil-whisper-large-v3-en'],
isCloud: true,
},
{
id: 'deepgram',
name: 'Deepgram Nova-3',
description: '업계 최고 인식률, 스마트 문장 부호 및 포맷팅',
badge: 'Industry Benchmark',
requiresApiKey: true,
defaultModel: 'nova-3',
defaultBaseUrl: 'https://api.deepgram.com',
models: ['nova-3', 'nova-2', 'nova-2-general', 'nova-2-meeting'],
isCloud: true,
},
{
id: 'assemblyai',
name: 'AssemblyAI Universal-2',
description: '고도화된 음향 모델 및 문맥 인식 STT',
badge: 'Advanced Acoustic',
requiresApiKey: true,
defaultModel: 'best',
defaultBaseUrl: 'https://api.assemblyai.com/v2',
models: ['best', 'nano'],
isCloud: true,
},
{
id: 'google',
name: 'Google Gemini 2.0 Flash / Cloud STT',
description: 'Gemini 2.0 Flash 기반 한국어/다국어 인식',
badge: 'Multilingual · Asian Languages',
requiresApiKey: true,
defaultModel: 'gemini-2.0-flash',
defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
models: ['gemini-2.0-flash', 'gemini-1.5-flash'],
isCloud: true,
},
{
id: 'custom',
name: 'Custom (OpenAI 호환)',
description: 'vLLM, 사내 프라이빗 서버, Together 등 커스텀 엔드포인트',
badge: 'Self-Hosted / Proxy',
requiresApiKey: false,
defaultModel: 'whisper-1',
defaultBaseUrl: 'http://localhost:8000/v1',
models: ['whisper-1', 'custom'],
isCloud: true,
},
]
export class STTManager extends EventEmitter {
private _drivers = new Map<STTProviderType, ISTTDriver>()
constructor() {
super()
this._registerDrivers()
}
private _registerDrivers(): void {
this._drivers.set('d3ro-cloud', new D3ROCloudDriver())
this._drivers.set('openai', new OpenAIDriver())
this._drivers.set('groq', new GroqDriver())
this._drivers.set('deepgram', new DeepgramDriver())
this._drivers.set('assemblyai', new AssemblyAIDriver())
this._drivers.set('google', new GoogleDriver())
this._drivers.set('custom', new CustomDriver())
}
getProviders(): STTProviderInfo[] {
return STT_PROVIDERS_META
}
getActiveProvider(): STTProviderType {
const provider = configGet('sttProvider') as STTProviderType | undefined
return provider || 'local'
}
setProvider(provider: STTProviderType): void {
logger.info(`STT provider changed to: ${provider}`)
configSet('sttProvider', provider)
this.emit('provider-changed', { provider })
}
getProviderConfig(provider: STTProviderType): STTProviderConfig {
const configs = (configGet('sttProviderConfigs') || {}) as Record<STTProviderType, STTProviderConfig>
const specificConfig = configs[provider] || {}
const meta = STT_PROVIDERS_META.find((p) => p.id === provider)
return {
apiKey: specificConfig.apiKey ?? '',
baseUrl: specificConfig.baseUrl ?? meta?.defaultBaseUrl ?? '',
modelId: specificConfig.modelId ?? meta?.defaultModel ?? '',
temperature: specificConfig.temperature ?? 0,
}
}
setProviderConfig(provider: STTProviderType, config: STTProviderConfig): void {
const configs = { ...((configGet('sttProviderConfigs') || {}) as Record<STTProviderType, STTProviderConfig>) }
configs[provider] = {
...configs[provider],
...config,
}
configSet('sttProviderConfigs', configs)
this.emit('config-changed', { provider, config: configs[provider] })
logger.info(`STT provider config updated for: ${provider}`)
}
async testConnection(params: TestSTTConnectionParams): Promise<TestSTTConnectionResult> {
const { provider, apiKey, baseUrl, modelId } = params
if (provider === 'local') {
const localStt = getLocalSTTService()
const status = localStt.getStatus()
return {
success: true,
latencyMs: 0,
message: `로컬 Whisper 준비됨 (상태: ${status.engineState}, 모델: ${status.activeModel || '미선택'})`,
}
}
const driver = this._drivers.get(provider)
if (!driver) {
return {
success: false,
latencyMs: 0,
message: `지원되지 않는 STT 공급자입니다: ${provider}`,
}
}
const currentConfig = this.getProviderConfig(provider)
const effectiveConfig: STTProviderConfig = {
...currentConfig,
apiKey: apiKey !== undefined ? apiKey : currentConfig.apiKey,
baseUrl: baseUrl !== undefined ? baseUrl : currentConfig.baseUrl,
modelId: modelId !== undefined ? modelId : currentConfig.modelId,
}
return driver.testConnection(effectiveConfig)
}
async transcribe(audioBuffer: Buffer, options?: TranscribeOptions): Promise<TranscriptionResult> {
const provider = this.getActiveProvider()
// 1. Local STT
if (provider === 'local') {
return getLocalSTTService().transcribe(audioBuffer, options)
}
// 2. Cloud STT Driver
const driver = this._drivers.get(provider)
if (!driver) {
logger.warn(`Driver not found for provider ${provider}, falling back to local`)
return getLocalSTTService().transcribe(audioBuffer, options)
}
const config = this.getProviderConfig(provider)
const fallbackToLocal = configGet('sttFallbackToLocal') ?? true
try {
logger.info(`Transcribing with cloud STT provider: ${provider} (model: ${config.modelId})`)
return await driver.transcribe(audioBuffer, options, config)
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
logger.warn(`Cloud STT (${provider}) failed: ${errorMsg}`)
if (fallbackToLocal) {
logger.info(`Auto-fallback: executing Local Whisper transcription...`)
this.emit('fallback-to-local', { provider, reason: errorMsg })
try {
return await getLocalSTTService().transcribe(audioBuffer, options)
} catch (localError) {
logger.error(`Local STT fallback also failed: ${localError}`)
throw error // 원래 에러 유지
}
}
throw error
}
}
getStatus(): STTStatus {
const provider = this.getActiveProvider()
if (provider === 'local') {
return {
...getLocalSTTService().getStatus(),
activeProvider: 'local',
}
}
const config = this.getProviderConfig(provider)
const meta = STT_PROVIDERS_META.find((p) => p.id === provider)
return {
engineState: 'ready' as import('@d3ro/core/types').STTEngineState,
activeModel: config.modelId || meta?.defaultModel || null,
engineVersion: meta?.name || null,
gpuAccelerated: false,
activeProvider: provider,
}
}
}
let _instance: STTManager | null = null
export function getSTTManager(): STTManager {
if (!_instance) {
_instance = new STTManager()
}
return _instance
}
export function resetSTTManagerForTests(): void {
if (_instance) _instance.removeAllListeners()
_instance = null
}