Spaces:
Runtime error
Runtime error
Delete app/DocumentGeneration/grouped.py
Browse files
app/DocumentGeneration/grouped.py
DELETED
|
@@ -1,304 +0,0 @@
|
|
| 1 |
-
import re
|
| 2 |
-
from typing import List, Dict, Any
|
| 3 |
-
|
| 4 |
-
def calculate_group_height(pdf, group: Dict[str, Any]) -> int:
|
| 5 |
-
"""Calculate the estimated height needed for a complete group"""
|
| 6 |
-
subheading_type = group['subheading_type']
|
| 7 |
-
content_lines = group['content'].split('\n') if group['content'] else []
|
| 8 |
-
|
| 9 |
-
# Base height for subheading
|
| 10 |
-
if subheading_type == 'h1':
|
| 11 |
-
subheading_height = 25 # 20pt font + spacing
|
| 12 |
-
elif subheading_type == 'h2':
|
| 13 |
-
subheading_height = 20 # 16pt font + underline + spacing
|
| 14 |
-
elif subheading_type == 'h3':
|
| 15 |
-
subheading_height = 16 # 14pt font + spacing
|
| 16 |
-
elif subheading_type == 'h4':
|
| 17 |
-
subheading_height = 14 # 12pt font + spacing
|
| 18 |
-
elif subheading_type == 'bold':
|
| 19 |
-
subheading_height = 20 # 16pt font + spacing
|
| 20 |
-
elif subheading_type == 'numbered':
|
| 21 |
-
subheading_height = 20 # 16pt font + spacing
|
| 22 |
-
else:
|
| 23 |
-
subheading_height = 16
|
| 24 |
-
|
| 25 |
-
# Calculate content height more accurately
|
| 26 |
-
content_height = 0
|
| 27 |
-
for line in content_lines:
|
| 28 |
-
if line.strip():
|
| 29 |
-
# More accurate line height calculation
|
| 30 |
-
words = line.split()
|
| 31 |
-
if len(words) > 0:
|
| 32 |
-
# Estimate characters per line (roughly 80-90 chars per line)
|
| 33 |
-
chars_per_line = 85
|
| 34 |
-
estimated_lines = max(1, len(line) // chars_per_line + 1)
|
| 35 |
-
line_height = estimated_lines * 8 # 8pt per line
|
| 36 |
-
content_height += line_height
|
| 37 |
-
else:
|
| 38 |
-
content_height += 8 # Empty line
|
| 39 |
-
else:
|
| 40 |
-
content_height += 3 # Empty line spacing
|
| 41 |
-
|
| 42 |
-
# Add spacing between groups
|
| 43 |
-
group_spacing = 8
|
| 44 |
-
|
| 45 |
-
total_height = subheading_height + content_height + group_spacing
|
| 46 |
-
|
| 47 |
-
print(f"DEBUG: Group '{group.get('subheading_clean', 'Unknown')[:30]}...' height calculation:")
|
| 48 |
-
print(f" Subheading height: {subheading_height}")
|
| 49 |
-
print(f" Content height: {content_height}")
|
| 50 |
-
print(f" Group spacing: {group_spacing}")
|
| 51 |
-
print(f" Total height: {total_height}")
|
| 52 |
-
|
| 53 |
-
return total_height
|
| 54 |
-
|
| 55 |
-
def should_break_page_for_group(pdf, group: Dict[str, Any], current_y: int, page_height: int = 280) -> bool:
|
| 56 |
-
"""Determine if we should break to a new page for this group"""
|
| 57 |
-
estimated_height = calculate_group_height(pdf, group)
|
| 58 |
-
|
| 59 |
-
print(f"DEBUG: should_break_page_for_group called")
|
| 60 |
-
print(f"DEBUG: Subheading: '{group.get('subheading_clean', 'Unknown')[:30]}...'")
|
| 61 |
-
print(f"DEBUG: Current Y: {current_y}")
|
| 62 |
-
print(f"DEBUG: Estimated height: {estimated_height}")
|
| 63 |
-
print(f"DEBUG: Page height: {page_height}")
|
| 64 |
-
print(f"DEBUG: Available space: {page_height - current_y}")
|
| 65 |
-
print(f"DEBUG: Will fit? {current_y + estimated_height <= page_height}")
|
| 66 |
-
|
| 67 |
-
# CRITICAL FIX: Only break if the group absolutely won't fit
|
| 68 |
-
# This prevents splitting subheadings from their content
|
| 69 |
-
|
| 70 |
-
# If the group won't fit on current page, break
|
| 71 |
-
if current_y + estimated_height > page_height:
|
| 72 |
-
print(f"DEBUG: -> BREAKING: Group won't fit ({current_y} + {estimated_height} > {page_height})")
|
| 73 |
-
return True
|
| 74 |
-
|
| 75 |
-
# If we're very close to the end of the page (less than 50mm remaining) and group is significant, break
|
| 76 |
-
if current_y > page_height - 50 and estimated_height > 30:
|
| 77 |
-
print(f"DEBUG: -> BREAKING: Very close to end of page ({current_y} > {page_height - 50}) and group significant ({estimated_height} > 30)")
|
| 78 |
-
return True
|
| 79 |
-
|
| 80 |
-
print(f"DEBUG: -> NO BREAK: Group will fit on current page")
|
| 81 |
-
return False
|
| 82 |
-
|
| 83 |
-
def clean_markdown_from_line(line: str) -> str:
|
| 84 |
-
"""Clean markdown formatting from a single line"""
|
| 85 |
-
if not line:
|
| 86 |
-
return ""
|
| 87 |
-
|
| 88 |
-
cleaned = line
|
| 89 |
-
|
| 90 |
-
# Remove bold, italic formatting
|
| 91 |
-
cleaned = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned) # Remove bold
|
| 92 |
-
cleaned = re.sub(r'\*(.*?)\*', r'\1', cleaned) # Remove italic
|
| 93 |
-
cleaned = re.sub(r'`(.*?)`', r'\1', cleaned) # Remove code
|
| 94 |
-
cleaned = re.sub(r'~~(.*?)~~', r'\1', cleaned) # Remove strikethrough
|
| 95 |
-
|
| 96 |
-
# Remove emphasis markers
|
| 97 |
-
cleaned = re.sub(r'_{1,2}(.*?)_{1,2}', r'\1', cleaned) # Remove underscores
|
| 98 |
-
cleaned = re.sub(r'\*{1,2}(.*?)\*{1,2}', r'\1', cleaned) # Remove asterisks
|
| 99 |
-
|
| 100 |
-
# Remove links (keep text)
|
| 101 |
-
cleaned = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', cleaned) # Remove links, keep text
|
| 102 |
-
|
| 103 |
-
# Remove any remaining HTML-like tags
|
| 104 |
-
cleaned = re.sub(r'<[^>]+>', '', cleaned)
|
| 105 |
-
|
| 106 |
-
# CRITICAL FIX: Replace multiple spaces with single space to prevent double spaces
|
| 107 |
-
cleaned = re.sub(r' +', ' ', cleaned) # Replace multiple spaces with single space
|
| 108 |
-
|
| 109 |
-
return cleaned.strip()
|
| 110 |
-
|
| 111 |
-
def parse_content_into_groups(content: str) -> List[Dict[str, Any]]:
|
| 112 |
-
"""Parse markdown content into subheading-content groups"""
|
| 113 |
-
if not content:
|
| 114 |
-
return []
|
| 115 |
-
|
| 116 |
-
print(f"DEBUG: parse_content_into_groups called with content length: {len(content)}")
|
| 117 |
-
|
| 118 |
-
lines = content.split('\n')
|
| 119 |
-
groups = []
|
| 120 |
-
current_group = None
|
| 121 |
-
|
| 122 |
-
for i, line in enumerate(lines):
|
| 123 |
-
line = line.strip()
|
| 124 |
-
if not line:
|
| 125 |
-
if current_group:
|
| 126 |
-
current_group['content_lines'].append('') # Preserve empty lines
|
| 127 |
-
continue
|
| 128 |
-
|
| 129 |
-
# Check if this is a heading (H1, H2, H3, H4, or numbered)
|
| 130 |
-
is_heading = (
|
| 131 |
-
line.startswith('# ') or # H1
|
| 132 |
-
(line.startswith('## ') and not line.startswith('### ')) or # H2
|
| 133 |
-
(line.startswith('### ') and not line.startswith('#### ')) or # H3
|
| 134 |
-
(line.startswith('#### ') and not line.startswith('##### ')) or # H4
|
| 135 |
-
re.match(r'^\d+\.\d+\s+', line) # Numbered subheadings like "3.6 SWOT Analysis"
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
# Check if this is bold text
|
| 139 |
-
is_bold_text = (
|
| 140 |
-
(line.startswith('**') and line.endswith('**') and len(line) > 4) or # Bold headings
|
| 141 |
-
(re.match(r'^\*\*.*?\*\*:', line) and len(line) > 4) # Bold text before colon as heading
|
| 142 |
-
)
|
| 143 |
-
|
| 144 |
-
# Check if this is a bold list item (e.g., "- **Item**")
|
| 145 |
-
is_bold_list_item = re.match(r'^[-*]\s*\*\*(.*?)\*\*$', line)
|
| 146 |
-
|
| 147 |
-
# Special handling for bold text following numbered subheadings
|
| 148 |
-
should_create_new_group = False
|
| 149 |
-
|
| 150 |
-
if is_heading:
|
| 151 |
-
# Always create new group for regular headings
|
| 152 |
-
should_create_new_group = True
|
| 153 |
-
print(f"DEBUG: Line {i+1} '{line[:50]}...' - Regular heading, creating new group")
|
| 154 |
-
elif is_bold_text:
|
| 155 |
-
# Check if we should treat this bold text as content for a numbered subheading
|
| 156 |
-
if groups and groups[-1].get('subheading_type') == 'h3':
|
| 157 |
-
# Check if the previous group's subheading looks like a numbered subheading
|
| 158 |
-
previous_subheading = groups[-1].get('subheading', '')
|
| 159 |
-
if re.match(r'^\d+\.\d+\s+', previous_subheading):
|
| 160 |
-
# This is content for a numbered subheading, add to previous group
|
| 161 |
-
groups[-1]['content_lines'].append(line)
|
| 162 |
-
print(f"DEBUG: Line {i+1} '{line[:50]}...' - Adding to previous numbered subheading '{previous_subheading[:30]}...'")
|
| 163 |
-
continue # Skip creating new group
|
| 164 |
-
else:
|
| 165 |
-
# Not a numbered subheading, treat as new heading
|
| 166 |
-
should_create_new_group = True
|
| 167 |
-
print(f"DEBUG: Line {i+1} '{line[:50]}...' - Treating as NEW HEADING (not numbered subheading)")
|
| 168 |
-
else:
|
| 169 |
-
# No previous group or not a numbered subheading, treat as new heading
|
| 170 |
-
should_create_new_group = True
|
| 171 |
-
print(f"DEBUG: Line {i+1} '{line[:50]}...' - Treating as NEW HEADING (no numbered subheading)")
|
| 172 |
-
elif is_bold_list_item:
|
| 173 |
-
# If it's a bold list item, treat it as a new heading
|
| 174 |
-
should_create_new_group = True
|
| 175 |
-
print(f"DEBUG: Line {i+1} '{line[:50]}...' - Bold list item, creating new group")
|
| 176 |
-
|
| 177 |
-
if should_create_new_group:
|
| 178 |
-
# Save previous group if it exists
|
| 179 |
-
if current_group:
|
| 180 |
-
current_group['content'] = '\n'.join(current_group['content_lines'])
|
| 181 |
-
groups.append(current_group)
|
| 182 |
-
print(f"DEBUG: Saved group '{current_group.get('subheading_clean', 'Unknown')[:30]}...' with {len(current_group['content_lines'])} content lines")
|
| 183 |
-
|
| 184 |
-
# Clean markdown symbols BEFORE determining type and storing
|
| 185 |
-
clean_text = line
|
| 186 |
-
heading_type = 'text'
|
| 187 |
-
|
| 188 |
-
if line.startswith('# '):
|
| 189 |
-
clean_text = line[2:].strip() # Remove "# "
|
| 190 |
-
heading_type = 'h1'
|
| 191 |
-
elif line.startswith('## '):
|
| 192 |
-
clean_text = line[3:].strip() # Remove "## "
|
| 193 |
-
heading_type = 'h2'
|
| 194 |
-
elif line.startswith('### '):
|
| 195 |
-
clean_text = line[4:].strip() # Remove "### "
|
| 196 |
-
heading_type = 'h3'
|
| 197 |
-
elif line.startswith('#### '):
|
| 198 |
-
clean_text = line[5:].strip() # Remove "#### "
|
| 199 |
-
heading_type = 'h4'
|
| 200 |
-
elif re.match(r'^\d+\.\d+\s+', line):
|
| 201 |
-
clean_text = line.strip() # Keep numbered headings as-is
|
| 202 |
-
heading_type = 'numbered'
|
| 203 |
-
elif is_bold_text:
|
| 204 |
-
# Remove ** markers from bold text
|
| 205 |
-
clean_text = clean_markdown_from_line(line.strip())
|
| 206 |
-
heading_type = 'bold'
|
| 207 |
-
elif is_bold_list_item:
|
| 208 |
-
# Extract the bold text from list item and treat as H3 subheading
|
| 209 |
-
# Remove the list marker and ** markers
|
| 210 |
-
clean_text = re.sub(r'^[-*]\s*\*\*(.*?)\*\*', r'\1', line.strip())
|
| 211 |
-
heading_type = 'h3' # Treat as H3 subheading
|
| 212 |
-
|
| 213 |
-
print(f"DEBUG: Creating new group '{clean_text[:30]}...' (type: {heading_type})")
|
| 214 |
-
|
| 215 |
-
# Start new group with cleaned text
|
| 216 |
-
current_group = {
|
| 217 |
-
'subheading': line, # Keep original for reference
|
| 218 |
-
'content_lines': [],
|
| 219 |
-
'content': '',
|
| 220 |
-
'subheading_clean': clean_text, # Pre-cleaned text
|
| 221 |
-
'subheading_type': heading_type
|
| 222 |
-
}
|
| 223 |
-
else:
|
| 224 |
-
# Add to current group's content - clean the line
|
| 225 |
-
if current_group:
|
| 226 |
-
cleaned_line = clean_markdown_from_line(line)
|
| 227 |
-
current_group['content_lines'].append(cleaned_line)
|
| 228 |
-
if i < 5: # Log first few content lines
|
| 229 |
-
print(f"DEBUG: Added content line to group '{current_group.get('subheading_clean', 'Unknown')[:30]}...': '{cleaned_line[:50]}...'")
|
| 230 |
-
else:
|
| 231 |
-
# Content before any subheading - create a default group
|
| 232 |
-
if not groups or groups[-1].get('subheading') != '## Introduction':
|
| 233 |
-
cleaned_line = clean_markdown_from_line(line)
|
| 234 |
-
current_group = {
|
| 235 |
-
'subheading': '## Introduction',
|
| 236 |
-
'content_lines': [cleaned_line],
|
| 237 |
-
'content': '',
|
| 238 |
-
'subheading_clean': 'Introduction',
|
| 239 |
-
'subheading_type': 'h2'
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
# Don't forget the last group
|
| 243 |
-
if current_group:
|
| 244 |
-
current_group['content'] = '\n'.join(current_group['content_lines'])
|
| 245 |
-
groups.append(current_group)
|
| 246 |
-
print(f"DEBUG: Saved final group '{current_group.get('subheading_clean', 'Unknown')[:30]}...' with {len(current_group['content_lines'])} content lines")
|
| 247 |
-
|
| 248 |
-
# Add metadata to each group
|
| 249 |
-
for i, group in enumerate(groups):
|
| 250 |
-
group['group_id'] = f"group_{i}"
|
| 251 |
-
group['content_length'] = len(group['content'])
|
| 252 |
-
group['word_count'] = len(group['content'].split()) if group['content'] else 0
|
| 253 |
-
group['has_charts'] = 'CHART' in group['content'].upper() if group['content'] else False
|
| 254 |
-
group['line_count'] = len([line for line in group['content'].split('\n') if line.strip()])
|
| 255 |
-
|
| 256 |
-
print(f"DEBUG: Created {len(groups)} groups total")
|
| 257 |
-
return groups
|
| 258 |
-
|
| 259 |
-
def extract_key_phrases(content: str) -> List[tuple]:
|
| 260 |
-
"""Extract key phrases from content for grouping metadata"""
|
| 261 |
-
if not content:
|
| 262 |
-
return []
|
| 263 |
-
|
| 264 |
-
# Simple key phrase extraction
|
| 265 |
-
words = re.findall(r'\b\w+\b', content.lower())
|
| 266 |
-
word_freq = {}
|
| 267 |
-
|
| 268 |
-
for word in words:
|
| 269 |
-
if len(word) > 3: # Only words longer than 3 characters
|
| 270 |
-
word_freq[word] = word_freq.get(word, 0) + 1
|
| 271 |
-
|
| 272 |
-
# Return top 5 most frequent words
|
| 273 |
-
return sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:5]
|
| 274 |
-
|
| 275 |
-
def get_content_groups_info(content: str) -> Dict[str, Any]:
|
| 276 |
-
"""Get detailed information about content groups for analysis and debugging"""
|
| 277 |
-
if not content:
|
| 278 |
-
return {"groups": [], "summary": {"total_groups": 0, "total_content_length": 0}}
|
| 279 |
-
|
| 280 |
-
groups = parse_content_into_groups(content)
|
| 281 |
-
|
| 282 |
-
# Add key phrases to each group
|
| 283 |
-
for group in groups:
|
| 284 |
-
group['key_phrases'] = extract_key_phrases(group['content'])
|
| 285 |
-
|
| 286 |
-
# Calculate summary statistics
|
| 287 |
-
total_content_length = sum(group['content_length'] for group in groups)
|
| 288 |
-
total_word_count = sum(group['word_count'] for group in groups)
|
| 289 |
-
groups_with_charts = sum(1 for group in groups if group['has_charts'])
|
| 290 |
-
|
| 291 |
-
summary = {
|
| 292 |
-
"total_groups": len(groups),
|
| 293 |
-
"total_content_length": total_content_length,
|
| 294 |
-
"total_word_count": total_word_count,
|
| 295 |
-
"groups_with_charts": groups_with_charts,
|
| 296 |
-
"average_group_size": total_content_length / len(groups) if groups else 0,
|
| 297 |
-
"largest_group": max(groups, key=lambda g: g['content_length'])['subheading_clean'] if groups else None,
|
| 298 |
-
"smallest_group": min(groups, key=lambda g: g['content_length'])['subheading_clean'] if groups else None
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
return {
|
| 302 |
-
"groups": groups,
|
| 303 |
-
"summary": summary
|
| 304 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|