Skills › Scientific Agent Skills › scientific-slides › validate_presentation.py
validate_presentation.py
412 lines · skills/scientific-slides/scripts/validate_presentation.py at 5f2bc03
1#!/usr/bin/env python3
2"""
3Presentation Validation Script
5Validates scientific presentations for common issues:
6- Slide count vs. duration
7- LaTeX compilation
8- File size checks
9- Basic format validation
10"""
12import sys
13import os
14import argparse
15import subprocess
16from pathlib import Path
17from typing import Dict, List, Tuple, Optional
19# PDF page and geometry analysis. pypdf is the maintained continuation of
20# PyPDF2 and exposes the same PdfReader API, so accept whichever is installed.
21try:
22 from pypdf import PdfReader
23 HAS_PDF_READER = True
24except ImportError:
25 try:
26 from PyPDF2 import PdfReader
27 HAS_PDF_READER = True
28 except ImportError:
29 HAS_PDF_READER = False
31# Try to import python-pptx for PowerPoint analysis
32try:
33 from pptx import Presentation
34 HAS_PPTX = True
35except ImportError:
36 HAS_PPTX = False
39class PresentationValidator:
40 """Validates presentations for common issues."""
42 # Recommended slide counts by duration (min, recommended, max)
43 SLIDE_GUIDELINES = {
44 5: (5, 6, 8),
45 10: (8, 11, 14),
46 15: (13, 16, 20),
47 20: (18, 22, 26),
48 30: (22, 27, 33),
49 45: (32, 40, 50),
50 60: (40, 52, 65),
51 }
53 def __init__(self, filepath: str, duration: Optional[int] = None):
54 self.filepath = Path(filepath)
55 self.duration = duration
56 self.file_type = self.filepath.suffix.lower()
57 self.issues = []
58 self.warnings = []
59 self.info = []
61 def validate(self) -> Dict:
62 """Run all validations and return results."""
63 print(f"Validating: {self.filepath.name}")
64 print(f"File type: {self.file_type}")
65 print("=" * 60)
67 # Check file exists
68 if not self.filepath.exists():
69 self.issues.append(f"File not found: {self.filepath}")
70 return self._format_results()
72 # File size check
73 self._check_file_size()
75 # Type-specific validation
76 if self.file_type == '.pdf':
77 self._validate_pdf()
78 elif self.file_type == '.ppt':
79 self.issues.append('Legacy .ppt is unsupported; convert it to .pptx or PDF first.')
80 elif self.file_type == '.pptx':
81 self._validate_pptx()
82 elif self.file_type in ['.tex']:
83 self._validate_latex()
84 else:
85 self.warnings.append(f"Unknown file type: {self.file_type}")
87 return self._format_results()
89 def _check_file_size(self):
90 """Check if file size is reasonable."""
91 size_mb = self.filepath.stat().st_size / (1024 * 1024)
92 self.info.append(f"File size: {size_mb:.2f} MB")
94 if size_mb > 100:
95 self.issues.append(
96 f"File is very large ({size_mb:.1f} MB). "
97 "Consider compressing images."
98 )
99 elif size_mb > 50:
100 self.warnings.append(
101 f"File is large ({size_mb:.1f} MB). "
102 "May be slow to email or upload."
103 )
105 def _validate_pdf(self):
106 """Validate PDF presentation."""
107 if not HAS_PDF_READER:
108 self.warnings.append(
109 "pypdf not installed. Install with: uv pip install pypdf"
110 )
111 return
113 try:
114 with open(self.filepath, 'rb') as f:
115 reader = PdfReader(f)
116 num_pages = len(reader.pages)
117 if num_pages == 0:
118 self.issues.append("PDF has no pages")
119 return
121 self.info.append(f"Number of slides: {num_pages}")
123 # Check slide count against duration
124 if self.duration:
125 self._check_slide_count(num_pages)
127 # Get page size
128 first_page = reader.pages[0]
129 media_box = first_page.mediabox
130 width = float(media_box.width)
131 height = float(media_box.height)
133 # Convert points to inches (72 points = 1 inch)
134 width_in = width / 72
135 height_in = height / 72
136 aspect = width / height
138 self.info.append(
139 f"Slide dimensions: {width_in:.1f}\" x {height_in:.1f}\" "
140 f"(aspect ratio: {aspect:.2f})"
141 )
143 # Check common aspect ratios
144 if abs(aspect - 16/9) < 0.01:
145 self.info.append("Aspect ratio: 16:9 (widescreen)")
146 elif abs(aspect - 4/3) < 0.01:
147 self.info.append("Aspect ratio: 4:3 (standard)")
148 else:
149 self.warnings.append(
150 f"Unusual aspect ratio: {aspect:.2f}. "
151 "Confirm this matches venue requirements."
152 )
154 except Exception as e:
155 self.issues.append(f"Error reading PDF: {str(e)}")
157 def _validate_pptx(self):
158 """Validate PowerPoint presentation."""
159 if not HAS_PPTX:
160 self.warnings.append(
161 "python-pptx not installed. Install with: uv pip install python-pptx"
162 )
163 return
165 try:
166 prs = Presentation(self.filepath)
167 num_slides = len(prs.slides)
169 self.info.append(f"Number of slides: {num_slides}")
171 # Check slide count against duration
172 if self.duration:
173 self._check_slide_count(num_slides)
175 # Get slide dimensions
176 width_inches = prs.slide_width / 914400 # EMU to inches
177 height_inches = prs.slide_height / 914400
178 aspect = prs.slide_width / prs.slide_height
180 self.info.append(
181 f"Slide dimensions: {width_inches:.1f}\" x {height_inches:.1f}\" "
182 f"(aspect ratio: {aspect:.2f})"
183 )
185 # Check fonts and text
186 self._check_pptx_content(prs)
188 except Exception as e:
189 self.issues.append(f"Error reading PowerPoint: {str(e)}")
191 def _check_pptx_content(self, prs):
192 """Check PowerPoint content for common issues."""
193 small_text_slides = []
194 many_bullets_slides = []
196 for idx, slide in enumerate(prs.slides, start=1):
197 for shape in slide.shapes:
198 if not shape.has_text_frame:
199 continue
201 text_frame = shape.text_frame
203 # Check for small fonts
204 for paragraph in text_frame.paragraphs:
205 for run in paragraph.runs:
206 if run.font.size and run.font.size.pt < 18:
207 small_text_slides.append(idx)
208 break
210 # Check for too many bullets
211 bullet_count = sum(1 for p in text_frame.paragraphs if p.level == 0)
212 if bullet_count > 6:
213 many_bullets_slides.append(idx)
215 # Report issues
216 if small_text_slides:
217 unique_slides = sorted(set(small_text_slides))
218 self.warnings.append(
219 f"Small text (<18pt) found on slides: {unique_slides[:5]}"
220 + (" ..." if len(unique_slides) > 5 else "")
221 )
223 if many_bullets_slides:
224 unique_slides = sorted(set(many_bullets_slides))
225 self.warnings.append(
226 f"Many bullets (>6) on slides: {unique_slides[:5]}"
227 + (" ..." if len(unique_slides) > 5 else "")
228 )
230 def _validate_latex(self):
231 """Validate LaTeX Beamer presentation."""
232 self.info.append("LaTeX source file detected")
234 # Try to compile
235 if self._try_compile_latex():
236 self.info.append("LaTeX compilation: SUCCESS")
238 # If PDF was generated, validate it
239 pdf_path = self.filepath.with_suffix('.pdf')
240 if pdf_path.exists():
241 pdf_validator = PresentationValidator(str(pdf_path), self.duration)
242 pdf_results = pdf_validator.validate()
244 # Merge results
245 self.info.extend(pdf_results['info'])
246 self.warnings.extend(pdf_results['warnings'])
247 self.issues.extend(pdf_results['issues'])
248 else:
249 self.issues.append(
250 "LaTeX compilation failed. Check .log file for errors."
251 )
253 def _try_compile_latex(self) -> bool:
254 """Try to compile LaTeX file."""
255 try:
256 # Try pdflatex
257 result = subprocess.run(
258 ['pdflatex', '-no-shell-escape', '-interaction=nonstopmode', self.filepath.name],
259 cwd=self.filepath.parent,
260 capture_output=True,
261 timeout=60
262 )
263 return result.returncode == 0
264 except (subprocess.TimeoutExpired, FileNotFoundError):
265 return False
267 def _check_slide_count(self, num_slides: int):
268 """Check if slide count is appropriate for duration."""
269 if self.duration not in self.SLIDE_GUIDELINES:
270 # Find nearest duration
271 durations = sorted(self.SLIDE_GUIDELINES.keys())
272 nearest = min(durations, key=lambda x: abs(x - self.duration))
273 min_slides, rec_slides, max_slides = self.SLIDE_GUIDELINES[nearest]
274 self.info.append(
275 f"Using guidelines for {nearest}-minute talk "
276 f"(closest to {self.duration} minutes)"
277 )
278 else:
279 min_slides, rec_slides, max_slides = self.SLIDE_GUIDELINES[self.duration]
281 self.info.append(
282 f"Recommended slides for {self.duration}-minute talk: "
283 f"{min_slides}-{max_slides} (optimal: ~{rec_slides})"
284 )
286 if num_slides < min_slides:
287 self.warnings.append(
288 f"Fewer slides ({num_slides}) than recommended ({min_slides}-{max_slides}). "
289 "May have too much time or too little content."
290 )
291 elif num_slides > max_slides:
292 self.warnings.append(
293 f"More slides ({num_slides}) than recommended ({min_slides}-{max_slides}). "
294 "Likely to run over time."
295 )
296 else:
297 self.info.append(
298 f"Slide count ({num_slides}) is within recommended range."
299 )
301 def _format_results(self) -> Dict:
302 """Format validation results."""
303 return {
304 'filepath': str(self.filepath),
305 'file_type': self.file_type,
306 'info': self.info,
307 'warnings': self.warnings,
308 'issues': self.issues,
309 'valid': len(self.issues) == 0
310 }
313def print_results(results: Dict):
314 """Print validation results in a readable format."""
315 print()
316 print("=" * 60)
317 print("VALIDATION RESULTS")
318 print("=" * 60)
320 # Print info
321 if results['info']:
322 print("\nInformation:")
323 for item in results['info']:
324 print(f" - {item}")
326 # Print warnings
327 if results['warnings']:
328 print("\n[WARN] Warnings:")
329 for item in results['warnings']:
330 print(f" - {item}")
332 # Print issues
333 if results['issues']:
334 print("\n[FAIL] Issues:")
335 for item in results['issues']:
336 print(f" - {item}")
338 # Overall status
339 print("\n" + "=" * 60)
340 if results['valid']:
341 print("[OK] Validation PASSED")
342 if results['warnings']:
343 print(f" ({len(results['warnings'])} warning(s) found)")
344 else:
345 print("[FAIL] Validation FAILED")
346 print(f" ({len(results['issues'])} issue(s) found)")
347 print("=" * 60)
350def main():
351 parser = argparse.ArgumentParser(
352 description='Validate scientific presentations',
353 formatter_class=argparse.RawDescriptionHelpFormatter,
354 epilog="""
355Examples:
356 %(prog)s presentation.pdf --duration 15
357 %(prog)s slides.pptx --duration 45
358 %(prog)s beamer_talk.tex --duration 20
360Supported file types:
361 - PDF (.pdf)
362 - PowerPoint (.pptx; convert legacy .ppt first)
363 - LaTeX Beamer (.tex)
365Validation checks:
366 - Slide count vs. duration
367 - File size
368 - Slide dimensions
369 - Font sizes (PowerPoint)
370 - LaTeX compilation (Beamer)
371 """
372 )
374 parser.add_argument(
375 'filepath',
376 help='Path to presentation file (PDF, PPTX, or TEX)'
377 )
379 parser.add_argument(
380 '--duration', '-d',
381 type=int,
382 help='Presentation duration in minutes'
383 )
385 parser.add_argument(
386 '--quiet', '-q',
387 action='store_true',
388 help='Only show issues and warnings'
389 )
391 args = parser.parse_args()
393 # Validate
394 validator = PresentationValidator(args.filepath, args.duration)
395 results = validator.validate()
397 # Print results
398 if args.quiet:
399 # Only show warnings and issues
400 if results['warnings'] or results['issues']:
401 print_results(results)
402 else:
403 print("[OK] No issues found")
404 else:
405 print_results(results)
407 # Exit with appropriate code
408 sys.exit(0 if results['valid'] else 1)
411if __name__ == '__main__':
412 main()