refactor(desktop): session-scoped voice runtime, STT/LLM ports, caption ownership and meeting export fixes
This commit is contained in:
parent
0adedf5e7b
commit
83cf9133ac
39 changed files with 4787 additions and 1061 deletions
|
|
@ -14,7 +14,8 @@ import type {
|
|||
TestSTTConnectionResult,
|
||||
STTStatus,
|
||||
} from '@d3ro/core/types'
|
||||
import type { ISTTDriver } from './types'
|
||||
import type { ISTTDriver, STTEngine } from './types'
|
||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||
import { OpenAIDriver } from './drivers/OpenAIDriver'
|
||||
import { GroqDriver } from './drivers/GroqDriver'
|
||||
import { DeepgramDriver } from './drivers/DeepgramDriver'
|
||||
|
|
@ -26,6 +27,23 @@ import { normalizeLoopbackUrl } from '../../utils/loopback'
|
|||
|
||||
const logger = getLogger('STTManager')
|
||||
|
||||
/**
|
||||
* 클라우드 실패 → 로컬 폴백의 전체 상한(모델 로딩 포함).
|
||||
* 로컬 엔진의 각 단계에도 타임아웃이 있지만, 폴백 경로가 어떤 이유로든 멈추면
|
||||
* 받아쓰기 액션 큐 전체가 굳으므로 여기서 한 번 더 끊는다.
|
||||
*/
|
||||
export const LOCAL_FALLBACK_TIMEOUT_MS = 180_000
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
const timeout = new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(() => reject(new D3ROError(ErrorCode.STTTranscriptionTimeout, message)), timeoutMs)
|
||||
})
|
||||
return Promise.race([promise, timeout]).finally(() => {
|
||||
if (timer) clearTimeout(timer)
|
||||
})
|
||||
}
|
||||
|
||||
export const STT_PROVIDERS_META: STTProviderInfo[] = [
|
||||
{
|
||||
id: 'local',
|
||||
|
|
@ -116,7 +134,7 @@ export const STT_PROVIDERS_META: STTProviderInfo[] = [
|
|||
},
|
||||
]
|
||||
|
||||
export class STTManager extends EventEmitter {
|
||||
export class STTManager extends EventEmitter implements STTEngine {
|
||||
private _drivers = new Map<STTProviderType, ISTTDriver>()
|
||||
|
||||
constructor() {
|
||||
|
|
@ -210,7 +228,7 @@ export class STTManager extends EventEmitter {
|
|||
async transcribe(audioBuffer: Buffer, options?: TranscribeOptions): Promise<TranscriptionResult> {
|
||||
const provider = this.getActiveProvider()
|
||||
|
||||
// 1. Local STT
|
||||
// 1. Local STT — LocalSTTService.transcribe가 준비되지 않은 엔진을 스스로 초기화한다
|
||||
if (provider === 'local') {
|
||||
return getLocalSTTService().transcribe(audioBuffer, options)
|
||||
}
|
||||
|
|
@ -219,7 +237,7 @@ export class STTManager extends EventEmitter {
|
|||
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)
|
||||
return this._transcribeLocalFallback(audioBuffer, options)
|
||||
}
|
||||
|
||||
const config = this.getProviderConfig(provider)
|
||||
|
|
@ -236,7 +254,7 @@ export class STTManager extends EventEmitter {
|
|||
logger.info(`Auto-fallback: executing Local Whisper transcription...`)
|
||||
this.emit('fallback-to-local', { provider, reason: errorMsg })
|
||||
try {
|
||||
return await getLocalSTTService().transcribe(audioBuffer, options)
|
||||
return await this._transcribeLocalFallback(audioBuffer, options)
|
||||
} catch (localError) {
|
||||
logger.error(`Local STT fallback also failed: ${localError}`)
|
||||
throw error // 원래 에러 유지
|
||||
|
|
@ -247,6 +265,45 @@ export class STTManager extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 클라우드 공급자에서 로컬 엔진으로 넘어가는 경로.
|
||||
* 클라우드 공급자를 쓰는 동안에는 아무도 로컬 모델을 올리지 않는다 — LocalSTTService.transcribe가
|
||||
* 설정된 모델을 스스로 올린 뒤 전사하고(실패하면 reject), 여기서는 전체에 상한 시간을 두어
|
||||
* 받아쓰기 액션 큐가 굳지 않게 한다.
|
||||
*/
|
||||
private _transcribeLocalFallback(
|
||||
audioBuffer: Buffer,
|
||||
options?: TranscribeOptions,
|
||||
): Promise<TranscriptionResult> {
|
||||
return withTimeout(
|
||||
getLocalSTTService().transcribe(audioBuffer, options),
|
||||
LOCAL_FALLBACK_TIMEOUT_MS,
|
||||
`Local STT fallback timed out after ${LOCAL_FALLBACK_TIMEOUT_MS}ms`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* STTEngine 포트: 현재 공급자로 전사할 준비를 한다.
|
||||
* 로컬이면 설정된 모델을 올리고(이미 올라가 있으면 즉시 반환), 클라우드면 할 일이 없다.
|
||||
*/
|
||||
async ensureReady(): Promise<void> {
|
||||
if (this.getActiveProvider() !== 'local') return
|
||||
await getLocalSTTService().initialize(configGet('sttModelId'))
|
||||
}
|
||||
|
||||
/** STTEngine 포트: 로컬 공급자에서 선택한 모델이 설치되지 않았으면 그 id를 돌려준다. */
|
||||
getMissingModel(): string | null {
|
||||
if (this.getActiveProvider() !== 'local') return null
|
||||
const modelId = configGet('sttModelId')
|
||||
const model = getLocalSTTService().getModels().find((m) => m.id === modelId)
|
||||
return model && !model.downloaded ? modelId : null
|
||||
}
|
||||
|
||||
/** STTEngine 포트: 실시간 미리보기는 로컬 Whisper에서만 지원한다. */
|
||||
supportsPartial(): boolean {
|
||||
return this.getActiveProvider() === 'local'
|
||||
}
|
||||
|
||||
/**
|
||||
* 녹음 중 실시간 미리보기 전사(최종 삽입과 무관).
|
||||
* 로컬 Whisper에서만 지원한다 — 클라우드 공급자는 요청 비용/지연이 커서 사용하지 않는다.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue