Spaces:
Running
Running
| """ | |
| Design System for AI-Generated Social Posts | |
| Defines typography scales, color palettes, spacing rules, and layout templates. | |
| Ensures consistent, professional-looking posts across all platforms. | |
| """ | |
| from enum import Enum | |
| from typing import NamedTuple, Dict, List, Tuple | |
| from dataclasses import dataclass | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TYPOGRAPHY SCALE (Following design system best practices) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TypographyScale(NamedTuple): | |
| """Typography scale for different text elements.""" | |
| headline_xl: int = 96 # Main hook/title (1080px width) | |
| headline_lg: int = 72 # Large secondary title | |
| headline_md: int = 56 # Medium headline | |
| headline_sm: int = 44 # Small headline | |
| body_lg: int = 40 # Large body text | |
| body_md: int = 32 # Medium body (primary) | |
| body_sm: int = 24 # Small body (secondary) | |
| caption: int = 20 # Captions/hashtags | |
| tiny: int = 16 # Micro text | |
| TYPOGRAPHY = TypographyScale() | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SPACING SYSTEM (8px base unit) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class Spacing(NamedTuple): | |
| """8px-based spacing scale.""" | |
| xs: int = 8 # 8px | |
| sm: int = 16 # 16px | |
| md: int = 24 # 24px | |
| lg: int = 32 # 32px | |
| xl: int = 48 # 48px | |
| xxl: int = 64 # 64px | |
| xxxl: int = 80 # 80px | |
| SPACING = Spacing() | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LINE HEIGHT SCALE (Better readability) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| LINE_HEIGHT = { | |
| "tight": 1.1, # Headlines | |
| "normal": 1.3, # Body text (optimal for reading) | |
| "loose": 1.5, # Large body text | |
| "extra_loose": 1.8, # Captions | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # COLOR PALETTES (Instagram-optimized + high contrast) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ColorPalette: | |
| """A complete color palette for a post.""" | |
| name: str | |
| bg_top: Tuple[int, int, int] | |
| bg_bottom: Tuple[int, int, int] | |
| text_primary: Tuple[int, int, int] | |
| text_secondary: Tuple[int, int, int] | |
| accent_primary: Tuple[int, int, int] | |
| accent_secondary: Tuple[int, int, int] | |
| def contrast_text(self) -> Tuple[int, int, int]: | |
| """Get high-contrast text color.""" | |
| # Calculate luminance | |
| r, g, b = self.bg_top | |
| luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 | |
| return (20, 20, 20) if luminance > 0.5 else (255, 255, 255) | |
| # Professional Instagram-style palettes | |
| PALETTES = { | |
| # Modern Minimal | |
| "minimal_white": ColorPalette( | |
| name="Minimal White", | |
| bg_top=(255, 255, 255), | |
| bg_bottom=(248, 248, 248), | |
| text_primary=(18, 18, 18), | |
| text_secondary=(100, 100, 100), | |
| accent_primary=(0, 122, 255), | |
| accent_secondary=(255, 149, 0), | |
| ), | |
| # Bold Dark | |
| "dark_navy": ColorPalette( | |
| name="Dark Navy", | |
| bg_top=(8, 16, 32), | |
| bg_bottom=(16, 24, 48), | |
| text_primary=(255, 255, 255), | |
| text_secondary=(200, 200, 200), | |
| accent_primary=(64, 224, 255), | |
| accent_secondary=(255, 100, 200), | |
| ), | |
| # Warm Gradient | |
| "warm_sunset": ColorPalette( | |
| name="Warm Sunset", | |
| bg_top=(255, 189, 89), | |
| bg_bottom=(255, 140, 80), | |
| text_primary=(255, 255, 255), | |
| text_secondary=(240, 240, 240), | |
| accent_primary=(255, 255, 255), | |
| accent_secondary=(220, 60, 60), | |
| ), | |
| # Cool Modern | |
| "cool_gradient": ColorPalette( | |
| name="Cool Gradient", | |
| bg_top=(16, 48, 100), | |
| bg_bottom=(60, 100, 200), | |
| text_primary=(255, 255, 255), | |
| text_secondary=(200, 220, 255), | |
| accent_primary=(100, 255, 218), | |
| accent_secondary=(255, 200, 100), | |
| ), | |
| # Premium Dark | |
| "premium_black": ColorPalette( | |
| name="Premium Black", | |
| bg_top=(12, 12, 12), | |
| bg_bottom=(24, 24, 24), | |
| text_primary=(255, 255, 255), | |
| text_secondary=(180, 180, 180), | |
| accent_primary=(255, 192, 0), | |
| accent_secondary=(0, 200, 255), | |
| ), | |
| # Organic Green | |
| "organic_green": ColorPalette( | |
| name="Organic Green", | |
| bg_top=(240, 248, 245), | |
| bg_bottom=(220, 240, 235), | |
| text_primary=(24, 64, 48), | |
| text_secondary=(80, 120, 100), | |
| accent_primary=(34, 177, 76), | |
| accent_secondary=(255, 140, 0), | |
| ), | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ASPECT RATIOS FOR DIFFERENT PLATFORMS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class AspectRatio: | |
| """Social media aspect ratio definition.""" | |
| name: str | |
| width: int | |
| height: int | |
| platform: str | |
| def ratio(self) -> float: | |
| return self.width / self.height | |
| ASPECT_RATIOS = { | |
| "instagram_square": AspectRatio("Instagram Square", 1080, 1080, "instagram"), | |
| "instagram_feed": AspectRatio("Instagram Feed", 1080, 1350, "instagram"), | |
| "instagram_story": AspectRatio("Instagram Story", 1080, 1920, "instagram"), | |
| "tiktok": AspectRatio("TikTok", 1080, 1920, "tiktok"), | |
| "linkedin": AspectRatio("LinkedIn", 1200, 627, "linkedin"), | |
| "twitter": AspectRatio("Twitter/X", 1200, 675, "twitter"), | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LAYOUT TEMPLATES (Hook-first rendering) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class LayoutType(str, Enum): | |
| """Different layout templates for posts.""" | |
| TYPOGRAPHY_BOLD = "typography_bold" # Text-only, maximum impact | |
| TYPOGRAPHY_MINIMAL = "typography_minimal" # Clean, simple text card | |
| EDITORIAL_PHOTO = "editorial_photo" # Full-bleed photo + text overlay | |
| SPLIT_HALF = "split_half" # 50/50 image + text | |
| IMAGE_FOCUS = "image_focus" # Image-dominant with subtle text | |
| CAROUSEL_SLIDE = "carousel_slide" # For carousel posts | |
| INFOGRAPHIC = "infographic" # Data/stats focused | |
| QUOTE_CARD = "quote_card" # Famous quote style | |
| class LayoutTemplate: | |
| """Definition of a layout template.""" | |
| layout_type: LayoutType | |
| name: str | |
| description: str | |
| best_for: List[str] # Content types: ["short_text", "long_text", "stats", "quote"] | |
| uses_image: bool | |
| margin: int | |
| headline_size: int | |
| body_size: int | |
| line_height_ratio: float | |
| aspect_ratio: AspectRatio | |
| def inner_width(self) -> int: | |
| return self.aspect_ratio.width - (2 * self.margin) | |
| # Template definitions | |
| LAYOUT_TEMPLATES = { | |
| LayoutType.TYPOGRAPHY_BOLD: LayoutTemplate( | |
| layout_type=LayoutType.TYPOGRAPHY_BOLD, | |
| name="Bold Typography", | |
| description="Maximum impact text card with gradient background", | |
| best_for=["short_text", "quote", "hook"], | |
| uses_image=False, | |
| margin=64, | |
| headline_size=96, | |
| body_size=40, | |
| line_height_ratio=1.15, | |
| aspect_ratio=ASPECT_RATIOS["instagram_square"], | |
| ), | |
| LayoutType.TYPOGRAPHY_MINIMAL: LayoutTemplate( | |
| layout_type=LayoutType.TYPOGRAPHY_MINIMAL, | |
| name="Minimal Typography", | |
| description="Clean, spacious text card for readability", | |
| best_for=["text", "education", "thoughts"], | |
| uses_image=False, | |
| margin=80, | |
| headline_size=72, | |
| body_size=36, | |
| line_height_ratio=1.3, | |
| aspect_ratio=ASPECT_RATIOS["instagram_square"], | |
| ), | |
| LayoutType.EDITORIAL_PHOTO: LayoutTemplate( | |
| layout_type=LayoutType.EDITORIAL_PHOTO, | |
| name="Editorial Photo", | |
| description="Full-bleed photo with dark overlay and text", | |
| best_for=["story", "visual", "announcement"], | |
| uses_image=True, | |
| margin=60, | |
| headline_size=80, | |
| body_size=36, | |
| line_height_ratio=1.15, | |
| aspect_ratio=ASPECT_RATIOS["instagram_feed"], | |
| ), | |
| LayoutType.SPLIT_HALF: LayoutTemplate( | |
| layout_type=LayoutType.SPLIT_HALF, | |
| name="Split Half", | |
| description="50/50 split between image and text", | |
| best_for=["product", "comparison", "before_after"], | |
| uses_image=True, | |
| margin=48, | |
| headline_size=56, | |
| body_size=32, | |
| line_height_ratio=1.3, | |
| aspect_ratio=ASPECT_RATIOS["instagram_feed"], | |
| ), | |
| LayoutType.IMAGE_FOCUS: LayoutTemplate( | |
| layout_type=LayoutType.IMAGE_FOCUS, | |
| name="Image Focus", | |
| description="Image-dominant with subtle text badge", | |
| best_for=["visual", "design", "lifestyle"], | |
| uses_image=True, | |
| margin=40, | |
| headline_size=48, | |
| body_size=28, | |
| line_height_ratio=1.25, | |
| aspect_ratio=ASPECT_RATIOS["instagram_square"], | |
| ), | |
| LayoutType.INFOGRAPHIC: LayoutTemplate( | |
| layout_type=LayoutType.INFOGRAPHIC, | |
| name="Infographic", | |
| description="Stats and data-focused layout", | |
| best_for=["stats", "data", "comparison"], | |
| uses_image=False, | |
| margin=72, | |
| headline_size=64, | |
| body_size=36, | |
| line_height_ratio=1.4, | |
| aspect_ratio=ASPECT_RATIOS["instagram_square"], | |
| ), | |
| LayoutType.QUOTE_CARD: LayoutTemplate( | |
| layout_type=LayoutType.QUOTE_CARD, | |
| name="Quote Card", | |
| description="Famous quote or standalone thought", | |
| best_for=["quote", "inspiration", "thought"], | |
| uses_image=False, | |
| margin=64, | |
| headline_size=88, | |
| body_size=36, | |
| line_height_ratio=1.2, | |
| aspect_ratio=ASPECT_RATIOS["instagram_square"], | |
| ), | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CONTENT TYPE DETECTION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def detect_content_type(text: str, has_image: bool = False) -> str: | |
| """Detect content type from text for layout selection.""" | |
| text_lower = text.lower() | |
| word_count = len(text.split()) | |
| # Quote detection | |
| if any(quote in text_lower for quote in ['"', '"', '"', "'", "β"]): | |
| return "quote" | |
| # Stats detection | |
| if any(stat_marker in text_lower for stat_marker in ["#", "%", "$", "x", "times", "increase"]): | |
| return "stats" | |
| # Length-based | |
| if word_count <= 10: | |
| return "short_text" | |
| elif word_count <= 30: | |
| return "text" | |
| else: | |
| return "long_text" | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ACCESSIBILITY & CONTRAST | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def calculate_luminance(rgb: Tuple[int, int, int]) -> float: | |
| """Calculate relative luminance for WCAG contrast ratio.""" | |
| r, g, b = [x / 255.0 for x in rgb] | |
| r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4 | |
| g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4 | |
| b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4 | |
| return 0.2126 * r + 0.7152 * g + 0.0722 * b | |
| def get_contrast_ratio(color1: Tuple[int, int, int], color2: Tuple[int, int, int]) -> float: | |
| """Calculate WCAG contrast ratio between two colors.""" | |
| l1 = calculate_luminance(color1) | |
| l2 = calculate_luminance(color2) | |
| lighter = max(l1, l2) | |
| darker = min(l1, l2) | |
| return (lighter + 0.05) / (darker + 0.05) | |
| def ensure_readable(fg: Tuple[int, int, int], bg: Tuple[int, int, int]) -> Tuple[int, int, int]: | |
| """Ensure foreground is readable on background (WCAG AA minimum 4.5:1).""" | |
| ratio = get_contrast_ratio(fg, bg) | |
| if ratio >= 4.5: | |
| return fg | |
| # Adjust foreground to be lighter or darker | |
| fg_lum = calculate_luminance(fg) | |
| bg_lum = calculate_luminance(bg) | |
| if bg_lum > 0.5: | |
| # Dark background β use white | |
| return (255, 255, 255) | |
| else: | |
| # Light background β use black | |
| return (0, 0, 0) | |