Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
AI-powered design system generator that produces complete, tailored design systems from project requirements.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/slide_search_core.py
1#!/usr/bin/env python32# -*- coding: utf-8 -*-3"""4Slide Search Core - BM25 search engine for slide design databases5"""67import csv8import re9from pathlib import Path10from math import log11from collections import defaultdict1213# ============ CONFIGURATION ============14DATA_DIR = Path(__file__).parent.parent / "data"15MAX_RESULTS = 31617CSV_CONFIG = {18"strategy": {19"file": "slide-strategies.csv",20"search_cols": ["strategy_name", "keywords", "goal", "audience", "narrative_arc"],21"output_cols": ["strategy_name", "keywords", "slide_count", "structure", "goal", "audience", "tone", "narrative_arc", "sources"]22},23"layout": {24"file": "slide-layouts.csv",25"search_cols": ["layout_name", "keywords", "use_case", "recommended_for"],26"output_cols": ["layout_name", "keywords", "use_case", "content_zones", "visual_weight", "cta_placement", "recommended_for", "avoid_for", "css_structure"]27},28"copy": {29"file": "slide-copy.csv",30"search_cols": ["formula_name", "keywords", "use_case", "emotion_trigger", "slide_type"],31"output_cols": ["formula_name", "keywords", "components", "use_case", "example_template", "emotion_trigger", "slide_type", "source"]32},33"chart": {34"file": "slide-charts.csv",35"search_cols": ["chart_type", "keywords", "best_for", "when_to_use", "slide_context"],36"output_cols": ["chart_type", "keywords", "best_for", "data_type", "when_to_use", "when_to_avoid", "max_categories", "slide_context", "css_implementation", "accessibility_notes"]37}38}3940AVAILABLE_DOMAINS = list(CSV_CONFIG.keys())414243# ============ BM25 IMPLEMENTATION ============44class BM25:45"""BM25 ranking algorithm for text search"""4647def __init__(self, k1=1.5, b=0.75):48self.k1 = k149self.b = b50self.corpus = []51self.doc_lengths = []52self.avgdl = 053self.idf = {}54self.doc_freqs = defaultdict(int)55self.N = 05657def tokenize(self, text):58"""Lowercase, split, remove punctuation, filter short words"""59text = re.sub(r'[^\w\s]', ' ', str(text).lower())60return [w for w in text.split() if len(w) > 2]6162def fit(self, documents):63"""Build BM25 index from documents"""64self.corpus = [self.tokenize(doc) for doc in documents]65self.N = len(self.corpus)66if self.N == 0:67return68self.doc_lengths = [len(doc) for doc in self.corpus]69self.avgdl = sum(self.doc_lengths) / self.N7071for doc in self.corpus:72seen = set()73for word in doc:74if word not in seen:75self.doc_freqs[word] += 176seen.add(word)7778for word, freq in self.doc_freqs.items():79self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)8081def score(self, query):82"""Score all documents against query"""83query_tokens = self.tokenize(query)84scores = []8586for idx, doc in enumerate(self.corpus):87score = 088doc_len = self.doc_lengths[idx]89term_freqs = defaultdict(int)90for word in doc:91term_freqs[word] += 19293for token in query_tokens:94if token in self.idf:95tf = term_freqs[token]96idf = self.idf[token]97numerator = tf * (self.k1 + 1)98denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)99score += idf * numerator / denominator100101scores.append((idx, score))102103return sorted(scores, key=lambda x: x[1], reverse=True)104105106# ============ SEARCH FUNCTIONS ============107def _load_csv(filepath):108"""Load CSV and return list of dicts"""109with open(filepath, 'r', encoding='utf-8') as f:110return list(csv.DictReader(f))111112113def _search_csv(filepath, search_cols, output_cols, query, max_results):114"""Core search function using BM25"""115if not filepath.exists():116return []117118data = _load_csv(filepath)119120# Build documents from search columns121documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]122123# BM25 search124bm25 = BM25()125bm25.fit(documents)126ranked = bm25.score(query)127128# Get top results with score > 0129results = []130for idx, score in ranked[:max_results]:131if score > 0:132row = data[idx]133results.append({col: row.get(col, "") for col in output_cols if col in row})134135return results136137138def detect_domain(query):139"""Auto-detect the most relevant domain from query"""140query_lower = query.lower()141142domain_keywords = {143"strategy": ["pitch", "deck", "investor", "yc", "seed", "series", "demo", "sales", "webinar",144"conference", "board", "qbr", "all-hands", "duarte", "kawasaki", "structure"],145"layout": ["slide", "layout", "grid", "column", "title", "hero", "section", "cta",146"screenshot", "quote", "timeline", "comparison", "pricing", "team"],147"copy": ["headline", "copy", "formula", "aida", "pas", "hook", "cta", "benefit",148"objection", "proof", "testimonial", "urgency", "scarcity"],149"chart": ["chart", "graph", "bar", "line", "pie", "funnel", "metrics", "data",150"visualization", "kpi", "trend", "comparison", "heatmap", "gauge"]151}152153scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}154best = max(scores, key=scores.get)155return best if scores[best] > 0 else "strategy"156157158def search(query, domain=None, max_results=MAX_RESULTS):159"""Main search function with auto-domain detection"""160if domain is None:161domain = detect_domain(query)162163config = CSV_CONFIG.get(domain, CSV_CONFIG["strategy"])164filepath = DATA_DIR / config["file"]165166if not filepath.exists():167return {"error": f"File not found: {filepath}", "domain": domain}168169results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)170171return {172"domain": domain,173"query": query,174"file": config["file"],175"count": len(results),176"results": results177}178179180def search_all(query, max_results=2):181"""Search across all domains for comprehensive results"""182all_results = {}183184for domain in AVAILABLE_DOMAINS:185result = search(query, domain, max_results)186if result.get("count", 0) > 0:187all_results[domain] = result188189return all_results190191192# ============ CONTEXTUAL SEARCH (Premium Slide System) ============193194# New CSV configurations for decision system195DECISION_CSV_CONFIG = {196"layout-logic": {197"file": "slide-layout-logic.csv",198"key_col": "goal"199},200"typography": {201"file": "slide-typography.csv",202"key_col": "content_type"203},204"color-logic": {205"file": "slide-color-logic.csv",206"key_col": "emotion"207},208"backgrounds": {209"file": "slide-backgrounds.csv",210"key_col": "slide_type"211}212}213214215def _load_decision_csv(csv_type):216"""Load a decision CSV and return as dict keyed by primary column."""217config = DECISION_CSV_CONFIG.get(csv_type)218if not config:219return {}220221filepath = DATA_DIR / config["file"]222if not filepath.exists():223return {}224225data = _load_csv(filepath)226return {row[config["key_col"]]: row for row in data if config["key_col"] in row}227228229def get_layout_for_goal(goal, previous_emotion=None):230"""231Get layout recommendation based on slide goal.232Uses slide-layout-logic.csv for decision.233"""234layouts = _load_decision_csv("layout-logic")235row = layouts.get(goal, layouts.get("features", {}))236237result = dict(row) if row else {}238239# Apply pattern-breaking logic240if result.get("break_pattern") == "true" and previous_emotion:241result["_pattern_break"] = True242result["_contrast_with"] = previous_emotion243244return result245246247def get_typography_for_slide(slide_type, has_metrics=False, has_quote=False):248"""249Get typography recommendation based on slide content.250Uses slide-typography.csv for decision.251"""252typography = _load_decision_csv("typography")253254if has_metrics:255return typography.get("metric-callout", {})256if has_quote:257return typography.get("quote-block", {})258259# Map slide types to typography260type_map = {261"hero": "hero-statement",262"hook": "hero-statement",263"title": "title-only",264"problem": "subtitle-heavy",265"agitation": "metric-callout",266"solution": "subtitle-heavy",267"features": "feature-grid",268"proof": "metric-callout",269"traction": "data-insight",270"social": "quote-block",271"testimonial": "testimonial",272"pricing": "pricing",273"team": "team",274"cta": "cta-action",275"comparison": "comparison",276"timeline": "timeline",277}278279content_type = type_map.get(slide_type, "feature-grid")280return typography.get(content_type, {})281282283def get_color_for_emotion(emotion):284"""285Get color treatment based on emotional beat.286Uses slide-color-logic.csv for decision.287"""288colors = _load_decision_csv("color-logic")289return colors.get(emotion, colors.get("clarity", {}))290291292def get_background_config(slide_type):293"""294Get background image configuration.295Uses slide-backgrounds.csv for decision.296"""297backgrounds = _load_decision_csv("backgrounds")298return backgrounds.get(slide_type, {})299300301def should_use_full_bleed(slide_index, total_slides, emotion):302"""303Determine if slide should use full-bleed background.304Premium decks use 2-3 full-bleed slides strategically.305306Rules:3071. Never consecutive full-bleed3082. One in first third, one in middle, one at end3093. Reserved for high-emotion beats (hope, urgency, fear)310"""311high_emotion_beats = ["hope", "urgency", "fear", "curiosity"]312313if emotion not in high_emotion_beats:314return False315316if total_slides < 3:317return False318319third = total_slides // 3320strategic_positions = [1, third, third * 2, total_slides - 1]321322return slide_index in strategic_positions323324325def calculate_pattern_break(slide_index, total_slides, previous_emotion=None):326"""327Determine if this slide should break the visual pattern.328Used for emotional contrast (Duarte Sparkline technique).329"""330# Pattern breaks at strategic positions331if total_slides < 5:332return False333334# Break at 1/3 and 2/3 points335third = total_slides // 3336if slide_index in [third, third * 2]:337return True338339# Break when switching between frustration and hope340contrasting_emotions = {341"frustration": ["hope", "relief"],342"hope": ["frustration", "fear"],343"fear": ["hope", "relief"],344}345346if previous_emotion in contrasting_emotions:347return True348349return False350351352def search_with_context(query, slide_position=1, total_slides=9, previous_emotion=None):353"""354Enhanced search that considers deck context.355356Args:357query: Search query358slide_position: Current slide index (1-based)359total_slides: Total slides in deck360previous_emotion: Emotion of previous slide (for contrast)361362Returns:363Search results enriched with contextual recommendations364"""365# Get base results from existing BM25 search366base_results = search_all(query, max_results=2)367368# Detect likely slide goal from query369goal = detect_domain(query.lower())370if "problem" in query.lower():371goal = "problem"372elif "solution" in query.lower():373goal = "solution"374elif "cta" in query.lower() or "call to action" in query.lower():375goal = "cta"376elif "hook" in query.lower() or "title" in query.lower():377goal = "hook"378elif "traction" in query.lower() or "metric" in query.lower():379goal = "traction"380381# Enrich with contextual recommendations382context = {383"slide_position": slide_position,384"total_slides": total_slides,385"previous_emotion": previous_emotion,386"inferred_goal": goal,387}388389# Get layout recommendation390layout = get_layout_for_goal(goal, previous_emotion)391if layout:392context["recommended_layout"] = layout.get("layout_pattern")393context["layout_direction"] = layout.get("direction")394context["visual_weight"] = layout.get("visual_weight")395context["use_background_image"] = layout.get("use_bg_image") == "true"396397# Get typography recommendation398typography = get_typography_for_slide(goal)399if typography:400context["typography"] = {401"primary_size": typography.get("primary_size"),402"secondary_size": typography.get("secondary_size"),403"weight_contrast": typography.get("weight_contrast"),404}405406# Get color treatment407emotion = layout.get("emotion", "clarity") if layout else "clarity"408color = get_color_for_emotion(emotion)409if color:410context["color_treatment"] = {411"background": color.get("background"),412"text_color": color.get("text_color"),413"accent_usage": color.get("accent_usage"),414"card_style": color.get("card_style"),415}416417# Calculate pattern breaking418context["should_break_pattern"] = calculate_pattern_break(419slide_position, total_slides, previous_emotion420)421context["should_use_full_bleed"] = should_use_full_bleed(422slide_position, total_slides, emotion423)424425# Get background config if needed426if context.get("use_background_image"):427bg_config = get_background_config(goal)428if bg_config:429context["background"] = {430"image_category": bg_config.get("image_category"),431"overlay_style": bg_config.get("overlay_style"),432"search_keywords": bg_config.get("search_keywords"),433}434435# Suggested animation classes436animation_map = {437"hook": "animate-fade-up",438"problem": "animate-fade-up",439"agitation": "animate-count animate-stagger",440"solution": "animate-scale",441"features": "animate-stagger",442"traction": "animate-chart animate-count",443"proof": "animate-stagger-scale",444"social": "animate-fade-up",445"cta": "animate-pulse",446}447context["animation_class"] = animation_map.get(goal, "animate-fade-up")448449return {450"query": query,451"context": context,452"base_results": base_results,453}454