Spaces:
Runtime error
Runtime error
File size: 14,075 Bytes
8126b08 4928428 8126b08 4928428 8126b08 86434e0 8126b08 86434e0 8126b08 86434e0 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 4928428 8126b08 | 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 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 | import re
from typing import List, Dict, Any
def calculate_group_height(pdf, group: Dict[str, Any]) -> int:
"""Calculate the estimated height needed for a complete group"""
subheading_type = group['subheading_type']
content_lines = group['content'].split('\n') if group['content'] else []
# Base height for subheading
if subheading_type == 'h1':
subheading_height = 25 # 20pt font + spacing
elif subheading_type == 'h2':
subheading_height = 20 # 16pt font + underline + spacing
elif subheading_type == 'h3':
subheading_height = 16 # 14pt font + spacing
elif subheading_type == 'h4':
subheading_height = 14 # 12pt font + spacing
elif subheading_type == 'bold':
subheading_height = 20 # 16pt font + spacing
elif subheading_type == 'numbered':
subheading_height = 20 # 16pt font + spacing
else:
subheading_height = 16
# Calculate content height more accurately
content_height = 0
for line in content_lines:
if line.strip():
# More accurate line height calculation
words = line.split()
if len(words) > 0:
# Estimate characters per line (roughly 80-90 chars per line)
chars_per_line = 85
estimated_lines = max(1, len(line) // chars_per_line + 1)
line_height = estimated_lines * 8 # 8pt per line
content_height += line_height
else:
content_height += 8 # Empty line
else:
content_height += 3 # Empty line spacing
# Add spacing between groups
group_spacing = 8
total_height = subheading_height + content_height + group_spacing
# print(f"DEBUG: Group '{group.get('subheading_clean', 'Unknown')[:30]}...' height calculation:")
# print(f" Subheading height: {subheading_height}")
# print(f" Content height: {content_height}")
# print(f" Group spacing: {group_spacing}")
# print(f" Total height: {total_height}")
return total_height
def should_break_page_for_group(pdf, group: Dict[str, Any], current_y: int, page_height: int = 280) -> bool:
"""Determine if we should break to a new page for this group"""
estimated_height = calculate_group_height(pdf, group)
# print(f"DEBUG: should_break_page_for_group called")
# print(f"DEBUG: Subheading: '{group.get('subheading_clean', 'Unknown')[:30]}...'")
# print(f"DEBUG: Current Y: {current_y}")
# print(f"DEBUG: Estimated height: {estimated_height}")
# print(f"DEBUG: Page height: {page_height}")
# print(f"DEBUG: Available space: {page_height - current_y}")
# print(f"DEBUG: Will fit? {current_y + estimated_height <= page_height}")
# CRITICAL FIX: Only break if the group absolutely won't fit
# This prevents splitting subheadings from their content
# If the group won't fit on current page, break
if current_y + estimated_height > page_height:
# print(f"DEBUG: -> BREAKING: Group won't fit ({current_y} + {estimated_height} > {page_height})")
return True
# If we're very close to the end of the page (less than 50mm remaining) and group is significant, break
if current_y > page_height - 50 and estimated_height > 30:
# print(f"DEBUG: -> BREAKING: Very close to end of page ({current_y} > {page_height - 50}) and group significant ({estimated_height} > 30)")
return True
# print(f"DEBUG: -> NO BREAK: Group will fit on current page")
return False
def clean_markdown_from_line(line: str) -> str:
"""Clean markdown formatting from a single line"""
if not line:
return ""
cleaned = line
# Remove bold, italic formatting
cleaned = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned) # Remove bold
cleaned = re.sub(r'\*(.*?)\*', r'\1', cleaned) # Remove italic
cleaned = re.sub(r'`(.*?)`', r'\1', cleaned) # Remove code
cleaned = re.sub(r'~~(.*?)~~', r'\1', cleaned) # Remove strikethrough
# Remove emphasis markers
cleaned = re.sub(r'_{1,2}(.*?)_{1,2}', r'\1', cleaned) # Remove underscores
cleaned = re.sub(r'\*{1,2}(.*?)\*{1,2}', r'\1', cleaned) # Remove asterisks
# Remove links (keep text)
cleaned = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', cleaned) # Remove links, keep text
# Remove any remaining HTML-like tags
cleaned = re.sub(r'<[^>]+>', '', cleaned)
# CRITICAL FIX: Replace multiple spaces with single space to prevent double spaces
cleaned = re.sub(r' +', ' ', cleaned) # Replace multiple spaces with single space
return cleaned.strip()
def parse_content_into_groups(content: str) -> List[Dict[str, Any]]:
"""Parse markdown content into subheading-content groups"""
if not content:
return []
# print(f"DEBUG: parse_content_into_groups called with content length: {len(content)}")
lines = content.split('\n')
groups = []
current_group = None
for i, line in enumerate(lines):
line = line.strip()
if not line:
if current_group:
current_group['content_lines'].append('') # Preserve empty lines
continue
# Check if this is a heading (H1, H2, H3, H4, or numbered)
is_heading = (
line.startswith('# ') or # H1
(line.startswith('## ') and not line.startswith('### ')) or # H2
(line.startswith('### ') and not line.startswith('#### ')) or # H3
(line.startswith('#### ') and not line.startswith('##### ')) or # H4
re.match(r'^\d+\.\d+\s+', line) # Numbered subheadings like "3.6 SWOT Analysis"
)
# Check if this is bold text
is_bold_text = (
(line.startswith('**') and line.endswith('**') and len(line) > 4) or # Bold headings
(re.match(r'^\*\*.*?\*\*:', line) and len(line) > 4) # Bold text before colon as heading
)
# Check if this is a bold list item (e.g., "- **Item**")
is_bold_list_item = re.match(r'^[-*]\s*\*\*(.*?)\*\*$', line)
# Special handling for bold text following numbered subheadings
should_create_new_group = False
if is_heading:
# Always create new group for regular headings
should_create_new_group = True
# print(f"DEBUG: Line {i+1} '{line[:50]}...' - Regular heading, creating new group")
elif is_bold_text:
# Check if we should treat this bold text as content for a numbered subheading
if groups and groups[-1].get('subheading_type') == 'h3':
# Check if the previous group's subheading looks like a numbered subheading
previous_subheading = groups[-1].get('subheading', '')
if re.match(r'^\d+\.\d+\s+', previous_subheading):
# This is content for a numbered subheading, add to previous group
groups[-1]['content_lines'].append(line)
# print(f"DEBUG: Line {i+1} '{line[:50]}...' - Adding to previous numbered subheading '{previous_subheading[:30]}...'")
continue # Skip creating new group
else:
# Not a numbered subheading, treat as new heading
should_create_new_group = True
# print(f"DEBUG: Line {i+1} '{line[:50]}...' - Treating as NEW HEADING (not numbered subheading)")
else:
# No previous group or not a numbered subheading, treat as new heading
should_create_new_group = True
# print(f"DEBUG: Line {i+1} '{line[:50]}...' - Treating as NEW HEADING (no numbered subheading)")
elif is_bold_list_item:
# If it's a bold list item, treat it as a new heading
should_create_new_group = True
# print(f"DEBUG: Line {i+1} '{line[:50]}...' - Bold list item, creating new group")
if should_create_new_group:
# Save previous group if it exists
if current_group:
current_group['content'] = '\n'.join(current_group['content_lines'])
groups.append(current_group)
# print(f"DEBUG: Saved group '{current_group.get('subheading_clean', 'Unknown')[:30]}...' with {len(current_group['content_lines'])} content lines")
# Clean markdown symbols BEFORE determining type and storing
clean_text = line
heading_type = 'text'
if line.startswith('# '):
clean_text = line[2:].strip() # Remove "# "
heading_type = 'h1'
elif line.startswith('## '):
clean_text = line[3:].strip() # Remove "## "
heading_type = 'h2'
elif line.startswith('### '):
clean_text = line[4:].strip() # Remove "### "
heading_type = 'h3'
elif line.startswith('#### '):
clean_text = line[5:].strip() # Remove "#### "
heading_type = 'h4'
elif re.match(r'^\d+\.\d+\s+', line):
clean_text = line.strip() # Keep numbered headings as-is
heading_type = 'numbered'
elif is_bold_text:
# Remove ** markers from bold text
clean_text = clean_markdown_from_line(line.strip())
heading_type = 'bold'
elif is_bold_list_item:
# Extract the bold text from list item and treat as H3 subheading
# Remove the list marker and ** markers
clean_text = re.sub(r'^[-*]\s*\*\*(.*?)\*\*', r'\1', line.strip())
heading_type = 'h3' # Treat as H3 subheading
# print(f"DEBUG: Creating new group '{clean_text[:30]}...' (type: {heading_type})")
# Start new group with cleaned text
current_group = {
'subheading': line, # Keep original for reference
'content_lines': [],
'content': '',
'subheading_clean': clean_text, # Pre-cleaned text
'subheading_type': heading_type
}
else:
# Add to current group's content - clean the line
if current_group:
cleaned_line = clean_markdown_from_line(line)
current_group['content_lines'].append(cleaned_line)
if i < 5: # Log first few content lines
# print(f"DEBUG: Added content line to group '{current_group.get('subheading_clean', 'Unknown')[:30]}...': '{cleaned_line[:50]}...'")
pass
else:
# Content before any subheading - create a default group
if not groups or groups[-1].get('subheading') != '## Introduction':
cleaned_line = clean_markdown_from_line(line)
current_group = {
'subheading': '## Introduction',
'content_lines': [cleaned_line],
'content': '',
'subheading_clean': 'Introduction',
'subheading_type': 'h2'
}
# Don't forget the last group
if current_group:
current_group['content'] = '\n'.join(current_group['content_lines'])
groups.append(current_group)
# print(f"DEBUG: Saved final group '{current_group.get('subheading_clean', 'Unknown')[:30]}...' with {len(current_group['content_lines'])} content lines")
# Add metadata to each group
for i, group in enumerate(groups):
group['group_id'] = f"group_{i}"
group['content_length'] = len(group['content'])
group['word_count'] = len(group['content'].split()) if group['content'] else 0
group['has_charts'] = 'CHART' in group['content'].upper() if group['content'] else False
group['line_count'] = len([line for line in group['content'].split('\n') if line.strip()])
# print(f"DEBUG: Created {len(groups)} groups total")
return groups
def extract_key_phrases(content: str) -> List[tuple]:
"""Extract key phrases from content for grouping metadata"""
if not content:
return []
# Simple key phrase extraction
words = re.findall(r'\b\w+\b', content.lower())
word_freq = {}
for word in words:
if len(word) > 3: # Only words longer than 3 characters
word_freq[word] = word_freq.get(word, 0) + 1
# Return top 5 most frequent words
return sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:5]
def get_content_groups_info(content: str) -> Dict[str, Any]:
"""Get detailed information about content groups for analysis and debugging"""
if not content:
return {"groups": [], "summary": {"total_groups": 0, "total_content_length": 0}}
groups = parse_content_into_groups(content)
# Add key phrases to each group
for group in groups:
group['key_phrases'] = extract_key_phrases(group['content'])
# Calculate summary statistics
total_content_length = sum(group['content_length'] for group in groups)
total_word_count = sum(group['word_count'] for group in groups)
groups_with_charts = sum(1 for group in groups if group['has_charts'])
summary = {
"total_groups": len(groups),
"total_content_length": total_content_length,
"total_word_count": total_word_count,
"groups_with_charts": groups_with_charts,
"average_group_size": total_content_length / len(groups) if groups else 0,
"largest_group": max(groups, key=lambda g: g['content_length'])['subheading_clean'] if groups else None,
"smallest_group": min(groups, key=lambda g: g['content_length'])['subheading_clean'] if groups else None
}
return {
"groups": groups,
"summary": summary
} |