!/usr/bin/env python3
""" Process HTML files containing slide/voiceover content into beautiful Grav episode pages. Run this script in the directory containing your HTML files. """
import os import re from pathlib import Path from html.parser import HTMLParser
class SlideContentParser(HTMLParser): """Parse the HTML content and extract slides and voiceovers."""
def __init__(self):
super().__init__()
self.slides = []
self.voiceovers = {}
self.cold_open = ""
self.current_slide = None
self.current_text = ""
self.in_slide_content = False
self.in_voiceover = False
self.in_cold_open = False
self.current_tag = None
self.title = ""
def handle_starttag(self, tag, attrs):
self.current_tag = tag
def handle_endtag(self, tag):
self.current_tag = None
def handle_data(self, data):
data = data.strip()
if not data:
return
# Extract title from first h1
if self.current_tag == 'h1' and not self.title:
self.title = data
# Check for section markers
if data == "SLIDE CONTENT":
self.in_slide_content = True
self.in_voiceover = False
return
elif data == "VOICEOVER SCRIPTS":
self.in_slide_content = False
self.in_voiceover = True
return
# Handle slide content
if self.in_slide_content:
if data.startswith("Slide ") and ":" in data:
# New slide
slide_match = re.match(r'Slide (\d+):\s*(.+)', data)
if slide_match:
slide_num = int(slide_match.group(1))
slide_title = slide_match.group(2).strip()
self.current_slide = {
'number': slide_num,
'title': slide_title,
'content': []
}
self.slides.append(self.current_slide)
elif self.current_slide and data:
# Add content to current slide
self.current_slide['content'].append(data)
# Handle voiceover content
elif self.in_voiceover:
if data.startswith("Cold Open"):
self.in_cold_open = True
self.current_text = ""
elif data.endswith("Voiceover:"):
self.in_cold_open = False
# Extract slide number from voiceover header
match = re.match(r'Slide (\d+) Voiceover:', data)
if match:
self.current_slide_num = int(match.group(1))
self.current_text = ""
elif self.in_cold_open:
self.cold_open = data
self.in_cold_open = False
elif hasattr(self, 'current_slide_num'):
# Add voiceover to the appropriate slide
self.voiceovers[self.current_slide_num] = data
delattr(self, 'current_slide_num')
def create_episode_page(title, slides, voiceovers, cold_open): """Generate the HTML for the episode page."""
html = """<style>
/ Episode Page Styles / .episode-container { max-width: 800px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.6; color: #333; }
.episode-header { background: linear-gradient(135deg, #87CEEB 0%, #B0E0E6 100%); color: white; padding: 40px; border-radius: 12px; margin-bottom: 40px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); text-align: center; }
.episode-title { font-size: 2.5em; margin: 0; font-weight: 700; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); }
.episode-subtitle { font-size: 1.2em; margin-top: 10px; opacity: 0.95; font-weight: 300; }
.cold-open { background: #f8fbfd; border-left: 4px solid #87CEEB; padding: 25px; margin-bottom: 40px; border-radius: 0 8px 8px 0; font-style: italic; font-size: 1.1em; color: #555; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
.slide-section { margin-bottom: 50px; }
.slide-header { display: flex; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 2px solid #87CEEB; }
.slide-number { background: #87CEEB; color: white; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 15px; flex-shrink: 0; }
.slide-title { font-size: 1.8em; color: #2c3e50; margin: 0; font-weight: 600; }
.slide-content { background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); margin-bottom: 20px; }
.slide-content ul { margin: 15px 0; padding-left: 25px; }
.slide-content li { margin-bottom: 10px; color: #555; }
.slide-content strong { color: #2c3e50; font-weight: 600; }
.voiceover { background: linear-gradient(to right, #f0f9ff, #e6f4f9); padding: 20px 25px; border-radius: 8px; font-size: 1.05em; line-height: 1.8; color: #444; position: relative; overflow: hidden; }
.voiceover::before { content: "🎙️"; position: absolute; top: 15px; right: 15px; font-size: 2em; opacity: 0.15; }
.key-insight { background: #fff9e6; border: 2px solid #ffd700; padding: 20px; border-radius: 8px; margin: 30px 0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
.key-insight h4 { margin-top: 0; color: #d4a017; font-size: 1.2em; }
.final-thoughts { background: #2c3e50; color: white; padding: 30px; border-radius: 12px; margin-top: 50px; text-align: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }
.final-thoughts p { font-size: 1.15em; line-height: 1.8; margin: 0; }
/ Responsive Design / @media (max-width: 768px) { .episode-header { padding: 30px 20px; }
.episode-title {
font-size: 2em;
}
.slide-content {
padding: 20px;
}
.cold-open {
padding: 20px;
font-size: 1.05em;
}
}
{main_title}
{f'{subtitle}
' if subtitle else ''}{cold_open}
đź’ˇ Key Insight
The patterns we're discovering aren't just theoretical—they're the practical blueprint for how reality operates at every level.
Thank you for exploring these profound insights with us. Each pattern we uncover reveals more about the deep structure of reality and our place within it.