d3ro-voice/apps/desktop/src/main/voice-text-routing.ts

62 lines
2.2 KiB
TypeScript

// 받아쓰기 결과 라우팅 포트.
// VoiceMode 세션이 끝나면 전사 결과를 여기로 넘긴다. 등록된 소비자(sink)가 결과를 가져가면
// (예: 진행 중인 받아쓰기 템플릿 세션의 현재 필드) 그 세션은 "소비됨"으로 기록된다.
// 일반 삽입 경로는 isVoiceSessionClaimed(sessionId)로 소비 여부를 확인하고 삽입을 건너뛸 수 있다.
// 이 모듈은 소비자 구현을 모른다 — 소비자가 스스로 등록한다(의존성 역전).
import { getLogger } from './services/LoggerService'
const logger = getLogger('voice-text-routing')
export interface VoiceTextOffer {
sessionId: string
text: string
}
/** 결과를 가져가면 true. 가져가지 않으면 false — 다음 소비자에게 넘어간다. */
export type VoiceTextSink = (offer: VoiceTextOffer) => boolean
/** 소비 기록 보존 개수 — 최근 세션만 기억하면 된다. */
const MAX_CLAIMED_SESSIONS = 32
const sinks = new Set<VoiceTextSink>()
const claimedSessions: string[] = []
export function registerVoiceTextSink(sink: VoiceTextSink): () => void {
sinks.add(sink)
return () => {
sinks.delete(sink)
}
}
/**
* 완료된 받아쓰기 결과를 소비자에게 제안한다. 같은 세션을 두 번 넘겨도 한 번만 소비된다.
* @returns 소비자가 가져갔으면 true
*/
export function routeCompletedVoiceText(sessionId: string, text: string): boolean {
if (claimedSessions.includes(sessionId)) return true
for (const sink of [...sinks]) {
let claimed = false
try {
claimed = sink({ sessionId, text })
} catch (err) {
logger.warn(`Voice text sink failed: ${err instanceof Error ? err.message : String(err)}`)
}
if (claimed) {
claimedSessions.push(sessionId)
if (claimedSessions.length > MAX_CLAIMED_SESSIONS) claimedSessions.shift()
return true
}
}
return false
}
/** 이 세션의 결과를 소비자가 가져갔는지 — 일반 삽입을 건너뛸지 판단할 때 쓴다. */
export function isVoiceSessionClaimed(sessionId: string): boolean {
return claimedSessions.includes(sessionId)
}
export function resetVoiceTextRoutingForTests(): void {
sinks.clear()
claimedSessions.length = 0
}