orbis-backend / src /utils /design_system.py
Deusxx1234's picture
Initial deployment to HF Spaces
c84fdae
"""
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)
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
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]
@property
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
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class AspectRatio:
"""Social media aspect ratio definition."""
name: str
width: int
height: int
platform: str
@property
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
@dataclass
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
@property
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)