fix(voice): stop mic-test ref stealing, unblock action queue, keep tail audio, and move context/LLM routing behind ports and pure policies
This commit is contained in:
parent
2cd462333f
commit
2f94d24c99
13 changed files with 1672 additions and 353 deletions
89
packages/core/src/voice-llm-route.ts
Normal file
89
packages/core/src/voice-llm-route.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// packages/core/src/voice-llm-route.ts
|
||||
// 받아쓰기 세션의 LLM 후처리 경로 결정 — 순수 함수만 둔다 (IO·설정 저장소 의존 없음).
|
||||
//
|
||||
// 예전엔 같은 결정을 VoiceModeService._transcribe(스킵 여부)와 _processWithLLM(무엇을 실행할지)이
|
||||
// 서로 다른 규칙으로 두 번 내렸다. 두 사본이 어긋나 '체인' 기본 액션에서 음성 단축키가 지목한
|
||||
// 지시문이 조용히 무시됐다. 이제 경로는 여기서 한 번만 정하고, 서비스는 결과에 따라 IO 만 실행한다.
|
||||
//
|
||||
// 우선순위: 음성 단축키(override) > 체인 > 지시문(custom) > 일반 액션 > none
|
||||
// - 지시문 경로는 기본 액션이 'custom' 이거나 override 가 있을 때만 탄다.
|
||||
// (활성 지시문이 있어도 refine/translate 등 일반 액션을 덮어쓰지 않는다)
|
||||
// - 체인을 골랐는데 활성 체인이 없으면 일반 액션 'chain' 으로 LLM 에 넘긴다(기존 동작 유지).
|
||||
// - 로컬 백엔드인데 로컬 LLM 을 쓸 수 없으면 원문 삽입으로 떨어지고, 그 이유를 밝힌다
|
||||
// (이 경우에만 호출자가 경고를 띄운다).
|
||||
|
||||
import type { LLMAction, LLMActionSelection } from './types'
|
||||
|
||||
export type VoiceLlmBackend = 'local' | 'online'
|
||||
|
||||
export interface VoiceLlmRouteInput {
|
||||
/** 설정의 기본 후처리 액션 */
|
||||
defaultAction: LLMActionSelection
|
||||
/** 음성 단축키가 지목한 지시문 id (없으면 null) */
|
||||
overrideInstructionId: string | null
|
||||
/** 활성 체인 id */
|
||||
activeChainId: string | null
|
||||
/** 활성 지시문 id ('custom' 기본 액션에서만 쓴다) */
|
||||
activeInstructionId: string | null
|
||||
/** 로컬 LLM 사용 가능 여부 */
|
||||
localLlmAvailable: boolean
|
||||
/** LLM 백엔드 — 알 수 없으면 undefined (가용성 검사를 건너뛴다) */
|
||||
backend: VoiceLlmBackend | undefined
|
||||
}
|
||||
|
||||
export type VoiceLlmRoute =
|
||||
/** LLM 없이 원문을 삽입한다 */
|
||||
| { kind: 'insert-raw'; reason: 'none' | 'llm-unavailable' }
|
||||
/** 활성 체인을 실행한다 */
|
||||
| { kind: 'chain'; chainId: string }
|
||||
/** 지시문('custom')으로 처리한다. instructionId 가 null 이면 지시문 없이 custom 처리 */
|
||||
| { kind: 'instruction'; instructionId: string | null; source: 'override' | 'active' }
|
||||
/** 일반 액션으로 처리한다 */
|
||||
| { kind: 'action'; action: Exclude<LLMAction, 'custom'> }
|
||||
|
||||
function nonEmpty(value: string | null | undefined): string | null {
|
||||
return typeof value === 'string' && value.length > 0 ? value : null
|
||||
}
|
||||
|
||||
/** LLM 이 필요한 경로를 고른다 (가용성 검사 전). */
|
||||
function resolveDesiredRoute(input: VoiceLlmRouteInput): VoiceLlmRoute {
|
||||
const override = nonEmpty(input.overrideInstructionId)
|
||||
if (override) {
|
||||
// 명시적 음성 명령은 기본 설정(none·chain 포함)이 무효화하지 않는다
|
||||
return { kind: 'instruction', instructionId: override, source: 'override' }
|
||||
}
|
||||
|
||||
switch (input.defaultAction) {
|
||||
case 'none':
|
||||
return { kind: 'insert-raw', reason: 'none' }
|
||||
case 'chain': {
|
||||
const chainId = nonEmpty(input.activeChainId)
|
||||
return chainId ? { kind: 'chain', chainId } : { kind: 'action', action: 'chain' }
|
||||
}
|
||||
case 'custom':
|
||||
return {
|
||||
kind: 'instruction',
|
||||
instructionId: nonEmpty(input.activeInstructionId),
|
||||
source: 'active',
|
||||
}
|
||||
default:
|
||||
return { kind: 'action', action: input.defaultAction }
|
||||
}
|
||||
}
|
||||
|
||||
/** 이 경로가 LLM 호출을 필요로 하는가 */
|
||||
export function routeUsesLlm(route: VoiceLlmRoute): boolean {
|
||||
return route.kind !== 'insert-raw'
|
||||
}
|
||||
|
||||
/**
|
||||
* 받아쓰기 한 건의 LLM 후처리 경로를 결정한다.
|
||||
*/
|
||||
export function resolveVoiceLlmRoute(input: VoiceLlmRouteInput): VoiceLlmRoute {
|
||||
const desired = resolveDesiredRoute(input)
|
||||
if (!routeUsesLlm(desired)) return desired
|
||||
if (input.backend === 'local' && !input.localLlmAvailable) {
|
||||
return { kind: 'insert-raw', reason: 'llm-unavailable' }
|
||||
}
|
||||
return desired
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue