diff --git a/apps/desktop/src/main/services/LocalLLMService.ts b/apps/desktop/src/main/services/LocalLLMService.ts index 39b3625..7e520a5 100644 --- a/apps/desktop/src/main/services/LocalLLMService.ts +++ b/apps/desktop/src/main/services/LocalLLMService.ts @@ -70,25 +70,47 @@ interface LocalLLMEvents { // 시스템 프롬프트 (설계서 Phase 4 참조) // ============================================================ +// 시스템 프롬프트. +// `/no_think`는 qwen3 계열 reasoning model의 thinking mode를 비활성화하는 토큰. +// 다른 모델에서는 무시되므로 호환성에 문제 없음. +const NO_THINK = '/no_think' + const SYSTEM_PROMPTS: Record = { - refine: `다음 음성 전사 텍스트를 자연스럽고 격식 있는 문어체로 다듬어주세요. + refine: `${NO_THINK} +다음 음성 전사 텍스트를 자연스럽고 격식 있는 문어체로 다듬어주세요. 원래 의미를 유지하면서 문법 오류를 수정하고, 불필요한 반복이나 필러를 제거하세요. 다듬어진 텍스트만 출력하세요. 설명이나 부가 문구를 붙이지 마세요.`, - translate: `다음 텍스트를 {{targetLanguage}}로 번역해주세요. + translate: `${NO_THINK} +다음 텍스트를 {{targetLanguage}}로 번역해주세요. 자연스럽고 정확한 번역만 출력하세요. 원문이나 설명을 붙이지 마세요.`, - summarize: `다음 텍스트의 핵심 내용을 3줄 이내로 요약해주세요. + summarize: `${NO_THINK} +다음 텍스트의 핵심 내용을 3줄 이내로 요약해주세요. 요약문만 출력하세요.`, - grammar: `다음 텍스트의 문법 오류만 수정해주세요. + grammar: `${NO_THINK} +다음 텍스트의 문법 오류만 수정해주세요. 원래 의미와 톤을 유지하면서 문법 오류만 수정하세요. 수정된 텍스트만 출력하세요.`, - expand: `다음 텍스트를 더 자세하고 풍부하게 확장해주세요. + expand: `${NO_THINK} +다음 텍스트를 더 자세하고 풍부하게 확장해주세요. 확장된 텍스트만 출력하세요.` } +/** + * Reasoning model(qwen3, deepseek-r1 등)이 응답에 포함하는 + * ... 블록을 제거한다. /no_think 토큰을 무시하는 + * 모델에서도 안전하게 동작하도록. + */ +function stripReasoningBlocks(text: string): string { + return text + .replace(/[\s\S]*?<\/think>\s*/gi, '') + .replace(/[\s\S]*?<\/thinking>\s*/gi, '') + .trim() +} + // ============================================================ // LocalLLMService // ============================================================ @@ -437,7 +459,17 @@ class LocalLLMService extends EventEmitter { } const result = await this.generate(text, { systemPrompt }) - return result.text.trim() + const cleaned = stripReasoningBlocks(result.text) + // reasoning 블록 제거 후 빈 응답이면 원본 텍스트 폴백 + // (모델이 thinking만 하고 출력은 안 한 경우 / 응답 파싱 실패 케이스) + if (cleaned.length === 0) { + logger.warn( + `LLM returned empty after reasoning strip — falling back to original transcript ` + + `(raw length=${result.text.length})` + ) + return text + } + return cleaned } cancelGeneration(): void {