File size: 6,225 Bytes
ac4560f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""
Document chunking with hierarchical approach.
Creates overlapping chunks while preserving context.
"""

import json
from pathlib import Path
from typing import List, Dict, Tuple
import PyPDF2
import tiktoken
import logging

logger = logging.getLogger(__name__)


class ChunkProcessor:
    """Process PDFs into hierarchical chunks for embedding."""
    
    def __init__(self, chunk_size: int = 1000, overlap: int = 200):
        """Initialize chunk processor.
        
        Args:
            chunk_size: Target size for chunks in tokens
            overlap: Number of tokens to overlap between chunks
        """
        self.chunk_size = chunk_size
        self.overlap = overlap
        self.encoding = tiktoken.encoding_for_model("gpt-4")
        
    def count_tokens(self, text: str) -> int:
        """Count tokens in text."""
        return len(self.encoding.encode(text))
    
    def extract_text_from_pdf(self, pdf_path: Path) -> List[Tuple[str, int]]:
        """Extract text from PDF with page numbers."""
        text_pages = []
        
        try:
            with open(pdf_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                
                for page_num, page in enumerate(pdf_reader.pages, 1):
                    text = page.extract_text()
                    if text.strip():
                        text_pages.append((text, page_num))
                        
        except Exception as e:
            logger.error(f"Error reading PDF {pdf_path}: {e}")
            
        return text_pages
    
    def chunk_text(self, text: str, source: str, page: int) -> List[Dict]:
        """Split text into overlapping chunks."""
        chunks = []
        
        # Split into sentences (simple approach)
        sentences = text.replace('\n', ' ').split('. ')
        sentences = [s.strip() + '.' for s in sentences if s.strip()]
        
        current_chunk = []
        current_tokens = 0
        chunk_index = 0
        
        for sentence in sentences:
            sentence_tokens = self.count_tokens(sentence)
            
            # If adding this sentence would exceed chunk size
            if current_tokens + sentence_tokens > self.chunk_size and current_chunk:
                # Create chunk
                chunk_text = ' '.join(current_chunk)
                chunks.append({
                    "text": chunk_text,
                    "metadata": {
                        "source": source,
                        "page": page,
                        "chunk_index": chunk_index,
                        "token_count": current_tokens
                    }
                })
                
                # Start new chunk with overlap
                overlap_tokens = 0
                overlap_sentences = []
                
                # Add sentences from end of current chunk for overlap
                for sent in reversed(current_chunk):
                    sent_tokens = self.count_tokens(sent)
                    if overlap_tokens + sent_tokens <= self.overlap:
                        overlap_sentences.insert(0, sent)
                        overlap_tokens += sent_tokens
                    else:
                        break
                
                current_chunk = overlap_sentences
                current_tokens = overlap_tokens
                chunk_index += 1
            
            current_chunk.append(sentence)
            current_tokens += sentence_tokens
        
        # Add final chunk
        if current_chunk:
            chunk_text = ' '.join(current_chunk)
            chunks.append({
                "text": chunk_text,
                "metadata": {
                    "source": source,
                    "page": page,
                    "chunk_index": chunk_index,
                    "token_count": current_tokens
                }
            })
        
        return chunks
    
    def chunk_pdf(self, pdf_path: Path) -> List[Dict]:
        """Process entire PDF into chunks."""
        all_chunks = []
        text_pages = self.extract_text_from_pdf(pdf_path)
        
        for text, page_num in text_pages:
            chunks = self.chunk_text(text, pdf_path.name, page_num)
            all_chunks.extend(chunks)
        
        logger.info(f"Created {len(all_chunks)} chunks from {pdf_path.name}")
        return all_chunks
    
    def process_directory(self, pdf_dir: Path) -> Dict[str, List[Dict]]:
        """Process all PDFs in directory, organized by version."""
        version_chunks = {}
        
        for pdf_path in pdf_dir.glob("*.pdf"):
            # Extract version from filename (e.g., harmony_1_8_guide.pdf)
            filename = pdf_path.stem.lower()
            
            # Determine version
            if "harmony" in filename:
                if "1_8" in filename or "1.8" in filename:
                    version = "harmony_1_8"
                elif "1_6" in filename or "1.6" in filename:
                    version = "harmony_1_6"
                elif "1_5" in filename or "1.5" in filename:
                    version = "harmony_1_5"
                elif "1_2" in filename or "1.2" in filename:
                    version = "harmony_1_2"
                else:
                    version = "harmony_general"
            elif "chorus" in filename:
                version = "chorus_1_1"
            else:
                version = "general_faq"
            
            # Process PDF
            chunks = self.chunk_pdf(pdf_path)
            
            # Add to version collection
            if version not in version_chunks:
                version_chunks[version] = []
            version_chunks[version].extend(chunks)
        
        return version_chunks
    
    def save_chunks(self, chunks: List[Dict], output_path: Path):
        """Save chunks to JSON file."""
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        with open(output_path, 'w') as f:
            json.dump({
                "chunks": chunks,
                "chunk_size": self.chunk_size,
                "overlap": self.overlap
            }, f, indent=2)
        
        logger.info(f"Saved {len(chunks)} chunks to {output_path}")