UNPKG

aiwf

Version:

AI Workflow Framework for Claude Code with multi-language support (Korean/English)

492 lines (423 loc) 15.6 kB
import { TokenCounter } from './token-counter.js'; /** * 텍스트 요약 알고리즘 프로토타입 */ export class TextSummarizer { constructor() { this.tokenCounter = new TokenCounter(); } /** * 텍스트를 요약합니다 * @param {string} text - 요약할 텍스트 * @param {Object} options - 요약 옵션 * @returns {Object} 요약 결과 */ summarize(text, options = {}) { const { targetRatio = 0.3, // 목표 압축률 (30%) strategy = 'extractive', // 'extractive' 또는 'abstractive' preserveStructure = true, // 구조 유지 여부 minSentenceLength = 20, // 최소 문장 길이 maxSentenceLength = 200 // 최대 문장 길이 } = options; const summary = strategy === 'extractive' ? this.extractiveSummarization(text, targetRatio, options) : this.abstractiveSummarization(text, targetRatio, options); return { original: text, summary: summary.content, originalTokens: this.tokenCounter.countTokens(text), summaryTokens: this.tokenCounter.countTokens(summary.content), compressionRatio: summary.compressionRatio, keyPoints: summary.keyPoints, metadata: summary.metadata }; } /** * 추출적 요약 (중요한 문장들을 선택) * @param {string} text - 요약할 텍스트 * @param {number} targetRatio - 목표 압축률 * @param {Object} options - 옵션 * @returns {Object} 요약 결과 */ extractiveSummarization(text, targetRatio, options) { // 1. 텍스트를 문장으로 분할 const sentences = this.splitIntoSentences(text); // 2. 각 문장의 중요도 계산 const scoredSentences = this.scoreSentences(sentences); // 3. 중요도에 따라 정렬 const sortedSentences = scoredSentences.sort((a, b) => b.score - a.score); // 4. 목표 비율에 맞춰 상위 문장 선택 const targetSentenceCount = Math.ceil(sentences.length * targetRatio); const selectedSentences = sortedSentences.slice(0, targetSentenceCount); // 5. 원본 순서대로 재배열 const finalSentences = selectedSentences.sort((a, b) => a.index - b.index); // 6. 요약 텍스트 생성 const summaryText = finalSentences.map(s => s.text).join(' '); return { content: summaryText, compressionRatio: this.calculateCompressionRatio(text, summaryText), keyPoints: this.extractKeyPoints(finalSentences), metadata: { strategy: 'extractive', originalSentences: sentences.length, selectedSentences: finalSentences.length, avgSentenceScore: finalSentences.reduce((sum, s) => sum + s.score, 0) / finalSentences.length } }; } /** * 추상적 요약 (내용을 재구성) * @param {string} text - 요약할 텍스트 * @param {number} targetRatio - 목표 압축률 * @param {Object} options - 옵션 * @returns {Object} 요약 결과 */ abstractiveSummarization(text, targetRatio, options) { // 1. 주요 개념 추출 const concepts = this.extractConcepts(text); // 2. 핵심 정보 식별 const keyInfo = this.identifyKeyInformation(text); // 3. 구조 분석 const structure = this.analyzeStructure(text); // 4. 요약 생성 const summaryText = this.generateAbstractSummary(concepts, keyInfo, structure, targetRatio); return { content: summaryText, compressionRatio: this.calculateCompressionRatio(text, summaryText), keyPoints: concepts.slice(0, 5), // 상위 5개 개념 metadata: { strategy: 'abstractive', conceptCount: concepts.length, keyInfoCount: keyInfo.length, structureElements: structure.length } }; } /** * 텍스트를 문장으로 분할합니다 * @param {string} text - 분할할 텍스트 * @returns {Array<string>} 문장 배열 */ splitIntoSentences(text) { // 마크다운 구조 유지하면서 문장 분할 const sentences = []; const lines = text.split('\n'); for (const line of lines) { if (line.trim() === '') continue; // 헤더, 목록 항목은 그대로 유지 if (line.match(/^#{1,6}\s+/) || line.match(/^\s*[-*+]\s+/) || line.match(/^\s*\d+\.\s+/)) { sentences.push(line.trim()); } else { // 일반 텍스트는 문장으로 분할 const lineSentences = line.split(/[.!?]+/).filter(s => s.trim().length > 0); sentences.push(...lineSentences.map(s => s.trim() + '.')); } } return sentences.filter(s => s.length > 10); // 너무 짧은 문장 제거 } /** * 문장들의 중요도를 계산합니다 * @param {Array<string>} sentences - 문장 배열 * @returns {Array<Object>} 점수가 매겨진 문장 배열 */ scoreSentences(sentences) { const scoredSentences = []; // 키워드 빈도 계산 const wordFreq = this.calculateWordFrequency(sentences.join(' ')); // 중요 키워드 정의 const importantKeywords = [ 'goal', 'objective', 'requirement', 'critical', 'important', 'key', 'main', 'primary', 'essential', 'must', 'should', 'need', 'implement', 'develop', 'create', 'build', 'design', 'task', 'subtask', 'acceptance', 'criteria', '목표', '목적', '요구사항', '중요', '핵심', '주요', '필수', '구현', '개발', '생성', '구축', '설계', '태스크', '하위', '승인', '기준' ]; sentences.forEach((sentence, index) => { let score = 0; // 1. 문장 길이 점수 (너무 짧거나 긴 문장 페널티) const length = sentence.length; if (length >= 30 && length <= 150) { score += 1; } else if (length < 30) { score -= 0.5; } // 2. 중요 키워드 점수 const lowerSentence = sentence.toLowerCase(); importantKeywords.forEach(keyword => { if (lowerSentence.includes(keyword.toLowerCase())) { score += 2; } }); // 3. 단어 빈도 점수 const words = sentence.toLowerCase().split(/\s+/); words.forEach(word => { if (wordFreq[word] && wordFreq[word] > 1) { score += Math.log(wordFreq[word]); } }); // 4. 위치 점수 (시작과 끝 부분 문장에 높은 점수) if (index < sentences.length * 0.3) { score += 1; // 시작 부분 } if (index > sentences.length * 0.7) { score += 0.5; // 끝 부분 } // 5. 구조적 중요도 (헤더, 목록 항목) if (sentence.match(/^#{1,6}\s+/) || sentence.match(/^\s*[-*+]\s+/)) { score += 3; } // 6. 숫자 포함 점수 (통계, 메트릭 등) if (sentence.match(/\d+/)) { score += 0.5; } scoredSentences.push({ text: sentence, score, index, length }); }); return scoredSentences; } /** * 단어 빈도를 계산합니다 * @param {string} text - 분석할 텍스트 * @returns {Object} 단어 빈도 객체 */ calculateWordFrequency(text) { const words = text.toLowerCase().split(/\s+/); const frequency = {}; // 불용어 제거 const stopWords = new Set([ 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'among', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'may', 'might', 'must', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'its', 'our', 'their', '이', '그', '저', '이것', '그것', '저것', '이거', '그거', '저거', '여기', '거기', '저기', '이곳', '그곳', '저곳', '는', '은', '을', '를', '이', '가', '와', '과', '에', '에서', '으로', '로', '의', '도', '만', '부터', '까지', '처럼', '같이', '보다', '한테', '에게', '께', '한', '하나', '두', '세', '네', '다섯', '여섯', '일곱', '여덟', '아홉', '열' ]); words.forEach(word => { const cleanWord = word.replace(/[^\w가-힣]/g, ''); if (cleanWord.length > 2 && !stopWords.has(cleanWord)) { frequency[cleanWord] = (frequency[cleanWord] || 0) + 1; } }); return frequency; } /** * 핵심 포인트를 추출합니다 * @param {Array<Object>} sentences - 선택된 문장들 * @returns {Array<string>} 핵심 포인트 배열 */ extractKeyPoints(sentences) { const keyPoints = []; sentences.forEach(sentence => { const text = sentence.text; // 목표, 요구사항, 기준 등 핵심 정보 추출 if (text.includes('goal') || text.includes('objective') || text.includes('목표')) { keyPoints.push(`목표: ${text.replace(/^[#\s*-]+/, '').trim()}`); } else if (text.includes('requirement') || text.includes('요구사항')) { keyPoints.push(`요구사항: ${text.replace(/^[#\s*-]+/, '').trim()}`); } else if (text.includes('criteria') || text.includes('기준')) { keyPoints.push(`기준: ${text.replace(/^[#\s*-]+/, '').trim()}`); } else if (text.includes('task') || text.includes('태스크')) { keyPoints.push(`태스크: ${text.replace(/^[#\s*-]+/, '').trim()}`); } }); return keyPoints.slice(0, 5); // 상위 5개만 반환 } /** * 주요 개념을 추출합니다 * @param {string} text - 분석할 텍스트 * @returns {Array<Object>} 개념 배열 */ extractConcepts(text) { const concepts = []; const lines = text.split('\n'); for (const line of lines) { // 헤더에서 개념 추출 const headerMatch = line.match(/^#{1,6}\s+(.+)$/); if (headerMatch) { concepts.push({ text: headerMatch[1], type: 'header', importance: 6 - line.indexOf('#') }); } // 목록 항목에서 개념 추출 const listMatch = line.match(/^\s*[-*+]\s+(.+)$/); if (listMatch) { concepts.push({ text: listMatch[1], type: 'list_item', importance: 2 }); } } return concepts.sort((a, b) => b.importance - a.importance); } /** * 핵심 정보를 식별합니다 * @param {string} text - 분석할 텍스트 * @returns {Array<Object>} 핵심 정보 배열 */ identifyKeyInformation(text) { const keyInfo = []; const sentences = this.splitIntoSentences(text); const keyPatterns = [ /goal|objective|목표|목적/i, /requirement|요구사항|조건/i, /criteria|기준|조건/i, /task|태스크|작업/i, /deadline|마감일|기한/i, /important|중요|핵심/i, /critical|중대|치명적/i, /must|필수|반드시/i ]; sentences.forEach(sentence => { keyPatterns.forEach(pattern => { if (pattern.test(sentence)) { keyInfo.push({ text: sentence, pattern: pattern.source, importance: this.calculateImportanceScore(sentence) }); } }); }); return keyInfo.sort((a, b) => b.importance - a.importance); } /** * 구조를 분석합니다 * @param {string} text - 분석할 텍스트 * @returns {Array<Object>} 구조 요소 배열 */ analyzeStructure(text) { const structure = []; const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // 헤더 구조 const headerMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headerMatch) { structure.push({ type: 'header', level: headerMatch[1].length, text: headerMatch[2], line: i }); } // 목록 구조 const listMatch = line.match(/^\s*([-*+]|\d+\.)\s+(.+)$/); if (listMatch) { structure.push({ type: 'list', marker: listMatch[1], text: listMatch[2], line: i }); } // 코드 블록 if (line.match(/^```/)) { structure.push({ type: 'code_block', line: i }); } } return structure; } /** * 추상적 요약을 생성합니다 * @param {Array<Object>} concepts - 개념 배열 * @param {Array<Object>} keyInfo - 핵심 정보 배열 * @param {Array<Object>} structure - 구조 배열 * @param {number} targetRatio - 목표 비율 * @returns {string} 요약 텍스트 */ generateAbstractSummary(concepts, keyInfo, structure, targetRatio) { const summary = []; // 1. 주요 개념 요약 const topConcepts = concepts.slice(0, Math.ceil(concepts.length * targetRatio)); summary.push('## 주요 개념'); topConcepts.forEach(concept => { summary.push(`- ${concept.text}`); }); // 2. 핵심 정보 요약 const topKeyInfo = keyInfo.slice(0, Math.ceil(keyInfo.length * targetRatio)); if (topKeyInfo.length > 0) { summary.push('\n## 핵심 정보'); topKeyInfo.forEach(info => { summary.push(`- ${info.text}`); }); } // 3. 구조적 요약 const headers = structure.filter(s => s.type === 'header' && s.level <= 3); if (headers.length > 0) { summary.push('\n## 구조'); headers.forEach(header => { summary.push(`${'#'.repeat(header.level + 1)} ${header.text}`); }); } return summary.join('\n'); } /** * 중요도 점수를 계산합니다 * @param {string} text - 분석할 텍스트 * @returns {number} 중요도 점수 */ calculateImportanceScore(text) { let score = 0; const lowerText = text.toLowerCase(); // 키워드 기반 점수 const keywordWeights = { 'critical': 5, 'important': 4, 'key': 3, 'main': 3, 'primary': 3, 'essential': 4, 'must': 4, 'should': 3, 'need': 2, 'goal': 4, 'objective': 4, 'requirement': 4, 'criteria': 3, 'task': 2, '중요': 4, '핵심': 4, '주요': 3, '필수': 4, '목표': 4, '목적': 4, '요구사항': 4, '기준': 3, '태스크': 2, '반드시': 4 }; Object.entries(keywordWeights).forEach(([keyword, weight]) => { if (lowerText.includes(keyword)) { score += weight; } }); // 길이 기반 점수 if (text.length > 50 && text.length < 200) { score += 1; } // 구두점 기반 점수 if (text.includes('!') || text.includes('?')) { score += 1; } return score; } /** * 압축률을 계산합니다 * @param {string} original - 원본 텍스트 * @param {string} compressed - 압축된 텍스트 * @returns {number} 압축률 (백분율) */ calculateCompressionRatio(original, compressed) { const originalTokens = this.tokenCounter.countTokens(original); const compressedTokens = this.tokenCounter.countTokens(compressed); if (originalTokens === 0) return 0; return ((originalTokens - compressedTokens) / originalTokens) * 100; } /** * 리소스를 정리합니다 */ cleanup() { if (this.tokenCounter) { this.tokenCounter.cleanup(); } } } export default TextSummarizer;