Spaces:
Running
Running
import gradio as gr | |
import numpy as np | |
import os | |
from huggingface_hub import login | |
from sentence_transformers import SentenceTransformer, util | |
# --- CONFIGURATION --- | |
class Config: | |
"""Configuration settings for the application.""" | |
EMBEDDING_MODEL_ID = "google/embeddinggemma-300M" | |
PROMPT_NAME = "STS" | |
TOP_K = 5 | |
HF_TOKEN = os.getenv('HF_TOKEN') | |
# --- FONT DATA --- | |
FONT_DATA = [ | |
{ | |
"name": "Playfair Display", | |
"family": "serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&display=swap", | |
"description": "Elegant, sophisticated, editorial, high-contrast serif with dramatic flair, perfect for luxury brands and fashion magazines" | |
}, | |
{ | |
"name": "Inter", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap", | |
"description": "Modern, clean, professional, highly legible sans-serif designed for digital interfaces and contemporary design" | |
}, | |
{ | |
"name": "Amatic SC", | |
"family": "handwriting", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Amatic+SC:wght@400;700&display=swap", | |
"description": "Playful, casual, handwritten, fun, child-like, informal font perfect for creative and whimsical projects" | |
}, | |
{ | |
"name": "Crimson Text", | |
"family": "serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Crimson+Text:wght@400;600&display=swap", | |
"description": "Classical, scholarly, academic, readable serif inspired by old-style typefaces, ideal for books and literature" | |
}, | |
{ | |
"name": "Roboto", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap", | |
"description": "Friendly, approachable, geometric sans-serif with a mechanical skeleton, widely used in digital applications" | |
}, | |
{ | |
"name": "Dancing Script", | |
"family": "script", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400;700&display=swap", | |
"description": "Romantic, flowing, elegant script font perfect for wedding invitations, greeting cards, and feminine designs" | |
}, | |
{ | |
"name": "Oswald", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Oswald:wght@300;400;600&display=swap", | |
"description": "Bold, condensed, impactful sans-serif with strong presence, ideal for headlines and masculine designs" | |
}, | |
{ | |
"name": "Lora", | |
"family": "serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Lora:wght@400;600&display=swap", | |
"description": "Warm, friendly, contemporary serif with calligraphic roots, perfect for body text and storytelling" | |
}, | |
{ | |
"name": "Montserrat", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600&display=swap", | |
"description": "Urban, modern, versatile sans-serif inspired by Buenos Aires signage, great for branding and corporate use" | |
}, | |
{ | |
"name": "Pacifico", | |
"family": "script", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Pacifico&display=swap", | |
"description": "Surfing, California, retro, casual script font with beach vibes and laid-back summer feeling" | |
}, | |
{ | |
"name": "Source Code Pro", | |
"family": "monospace", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@300;400;600&display=swap", | |
"description": "Technical, programming, coding, monospaced font designed for developers and technical documentation" | |
}, | |
{ | |
"name": "Merriweather", | |
"family": "serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700&display=swap", | |
"description": "Traditional, readable, pleasant serif designed for comfortable reading on screens and in print" | |
}, | |
{ | |
"name": "Abril Fatface", | |
"family": "serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Abril+Fatface&display=swap", | |
"description": "Bold, dramatic, high-contrast display serif inspired by French and Italian typography, perfect for headlines" | |
}, | |
{ | |
"name": "Great Vibes", | |
"family": "script", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap", | |
"description": "Elegant, formal, calligraphic script with sophisticated curves, ideal for luxury and premium branding" | |
}, | |
{ | |
"name": "Raleway", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Raleway:wght@300;400;600&display=swap", | |
"description": "Sophisticated, thin, elegant sans-serif with distinctive 'W', perfect for fashion and high-end design" | |
}, | |
{ | |
"name": "Fredoka One", | |
"family": "display", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap", | |
"description": "Friendly, rounded, playful display font perfect for children's content, toys, and fun applications" | |
}, | |
{ | |
"name": "Libre Baskerville", | |
"family": "serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&display=swap", | |
"description": "Classic, traditional, scholarly serif based on American Type Founder's Baskerville, perfect for academic texts" | |
}, | |
{ | |
"name": "Poppins", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap", | |
"description": "Geometric, modern, friendly sans-serif with circular forms, popular for contemporary web design" | |
}, | |
{ | |
"name": "Lobster", | |
"family": "script", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Lobster&display=swap", | |
"description": "Bold, retro, vintage script font with a 1950s diner feel, perfect for nostalgic and Americana designs" | |
}, | |
{ | |
"name": "Open Sans", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600&display=swap", | |
"description": "Neutral, friendly, optimistic sans-serif designed for legibility across interfaces, print, and web" | |
}, | |
{ | |
"name": "Shadows Into Light", | |
"family": "handwriting", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Shadows+Into+Light&display=swap", | |
"description": "Casual, handwritten, personal font that feels like natural handwriting with a marker or pen" | |
}, | |
{ | |
"name": "Creepster", | |
"family": "display", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Creepster&display=swap", | |
"description": "Horror, scary, Halloween, gothic font with dripping effect, perfect for spooky and thriller themes" | |
}, | |
{ | |
"name": "Righteous", | |
"family": "display", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Righteous&display=swap", | |
"description": "Futuristic, sci-fi, technology, bold display font with unique character shapes for modern designs" | |
}, | |
{ | |
"name": "Satisfy", | |
"family": "script", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Satisfy&display=swap", | |
"description": "Casual, relaxed, handwritten script with natural flow, perfect for personal and informal communications" | |
}, | |
{ | |
"name": "Anton", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Anton&display=swap", | |
"description": "Bold, condensed, impactful sans-serif perfect for headlines, posters, and attention-grabbing text" | |
}, | |
{ | |
"name": "Courgette", | |
"family": "script", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Courgette&display=swap", | |
"description": "French, bistro, café, elegant script font with continental European charm and sophistication" | |
}, | |
{ | |
"name": "Indie Flower", | |
"family": "handwriting", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap", | |
"description": "Indie, hipster, handwritten font with quirky personality, perfect for creative and artistic projects" | |
}, | |
{ | |
"name": "PT Serif", | |
"family": "serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=PT+Serif:wght@400;700&display=swap", | |
"description": "Russian, Cyrillic, transitional serif with excellent readability for both Latin and Cyrillic scripts" | |
}, | |
{ | |
"name": "Questrial", | |
"family": "sans-serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Questrial&display=swap", | |
"description": "Simple, clean, minimal sans-serif with subtle quirks, perfect for modern and understated designs" | |
}, | |
{ | |
"name": "Bangers", | |
"family": "display", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Bangers&display=swap", | |
"description": "Comic book, superhero, pop art font inspired by mid-20th century comic books and advertisements" | |
}, | |
{ | |
"name": "Sacramento", | |
"family": "script", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Sacramento&display=swap", | |
"description": "Monoline, cursive script with vintage charm, perfect for elegant and sophisticated branding" | |
}, | |
{ | |
"name": "Bitter", | |
"family": "serif", | |
"google_fonts_url": "https://fonts.googleapis.com/css2?family=Bitter:wght@400;700&display=swap", | |
"description": "Contemporary, slab serif with slight contrast, designed for comfortable reading in long texts" | |
} | |
] | |
# --- CORE LOGIC --- | |
class FontMoodGenerator: | |
"""Handles model loading, embedding generation, and font palette creation.""" | |
def __init__(self, config: Config, font_data: list[dict[str, any]]): | |
"""Initializes the generator, logs in, and loads necessary assets.""" | |
self.config = config | |
self.font_data = font_data | |
self._login_to_hf() | |
self.embedding_model = self._load_model() | |
self.font_embeddings = self._precompute_font_embeddings() | |
def _login_to_hf(self): | |
"""Logs into Hugging Face Hub if a token is provided.""" | |
if self.config.HF_TOKEN: | |
print("Logging into Hugging Face Hub...") | |
login(token=self.config.HF_TOKEN) | |
else: | |
print("HF_TOKEN not found. Proceeding without login.") | |
def _load_model(self) -> SentenceTransformer: | |
"""Loads the Sentence Transformer model.""" | |
print(f"Initializing embedding model: {self.config.EMBEDDING_MODEL_ID}...") | |
try: | |
return SentenceTransformer(self.config.EMBEDDING_MODEL_ID) | |
except Exception as e: | |
print(f"Error loading model: {e}") | |
raise | |
def _precompute_font_embeddings(self) -> np.ndarray: | |
"""Generates and stores embeddings for the font descriptions.""" | |
print("Pre-computing embeddings for font palette...") | |
font_texts = [ | |
f"{font['name']}, {font['description']}" | |
for font in self.font_data | |
] | |
embeddings = self.embedding_model.encode( | |
font_texts, | |
prompt_name=self.config.PROMPT_NAME | |
) | |
print("Embeddings computed successfully.") | |
return embeddings | |
def _create_persistent_font_imports(self, top_hits: list[dict[str, any]]) -> str: | |
"""Creates persistent font imports that work across all steps.""" | |
if not top_hits: | |
return "" | |
imports = [] | |
seen_urls = set() | |
for hit in top_hits: | |
font_info = self.font_data[hit['corpus_id']] | |
google_fonts_url = font_info['google_fonts_url'] | |
if google_fonts_url not in seen_urls: | |
imports.append(f"@import url('{google_fonts_url}');") | |
seen_urls.add(google_fonts_url) | |
return "\n".join(imports) | |
def _format_palette_as_html(self, top_hits: list[dict[str, any]]) -> str: | |
"""Formats the top font hits into a displayable HTML string with embedded font imports.""" | |
if not top_hits: | |
return "<p>Could not generate a font palette. Please try another mood.</p>" | |
# Get font imports for the selected fonts | |
font_imports = self._create_persistent_font_imports(top_hits) | |
sample_texts = [ | |
"The Quick Brown Fox Jumps Over The Lazy Dog", | |
"Sphinx of black quartz, judge my vow", | |
"How vexingly quick daft zebras jump!", | |
"Pack my box with five dozen liquor jugs", | |
"Waltz, bad nymph, for quick jigs vex" | |
] | |
cards_html = "" | |
for i, hit in enumerate(top_hits): | |
font_info = self.font_data[hit['corpus_id']] | |
font_name = font_info['name'] | |
font_family = font_info['family'] | |
score = hit['score'] | |
sample_text = sample_texts[i % len(sample_texts)] | |
cards_html += f""" | |
<div class="font-card"> | |
<div class="font-header"> | |
<h3>{font_name}</h3> | |
<span class="font-score">Score: {score:.2f}</span> | |
</div> | |
<div class="font-sample" style="font-family: '{font_name}', {font_family};"> | |
{sample_text} | |
</div> | |
<div class="font-details"> | |
<span class="font-family">{font_family.title()}</span> | |
<span class="font-description">{font_info['description'][:100]}...</span> | |
</div> | |
</div> | |
""" | |
# Include font imports directly in the HTML with unique ID to prevent conflicts | |
return f""" | |
<style id="palette-fonts"> | |
{font_imports} | |
</style> | |
<div class='font-palette-container'>{cards_html}</div> | |
""" | |
def generate_palette(self, mood_text: str) -> tuple[str, list[dict[str, any]]]: | |
"""Generates font palette and returns both HTML and raw data.""" | |
if not mood_text or not mood_text.strip(): | |
return "<p>Please enter a mood or a description.</p>", [] | |
mood_embedding = self.embedding_model.encode( | |
mood_text, | |
prompt_name=self.config.PROMPT_NAME | |
) | |
top_hits = util.semantic_search( | |
mood_embedding, self.font_embeddings, top_k=self.config.TOP_K | |
)[0] | |
palette_html = self._format_palette_as_html(top_hits) | |
return palette_html, top_hits | |
def generate_css_code(self, top_hits: list[dict[str, any]]) -> str: | |
"""Generates exportable CSS code.""" | |
if not top_hits: | |
return "/* No fonts generated yet */" | |
font_imports = self._create_persistent_font_imports(top_hits) | |
css_code = f"""/* Generated Font Palette CSS */ | |
{font_imports} | |
/* Font Variables */ | |
:root {{""" | |
for i, hit in enumerate(top_hits): | |
font_info = self.font_data[hit['corpus_id']] | |
font_name = font_info['name'] | |
css_code += f""" | |
--font-{i+1}: '{font_name}', {font_info['family']};""" | |
css_code += """ | |
} | |
/* Usage Examples */ | |
.heading { font-family: var(--font-1); } | |
.body-text { font-family: var(--font-2); } | |
.accent { font-family: var(--font-3); }""" | |
return css_code | |
def apply_theme_css(self, top_hits: list[dict[str, any]]) -> str: | |
"""Generates CSS to apply fonts to the UI while preserving palette fonts.""" | |
if not top_hits: | |
return "" | |
font_imports = self._create_persistent_font_imports(top_hits) | |
css_rules = [] | |
if len(top_hits) >= 1: | |
primary_font = self.font_data[top_hits[0]['corpus_id']]['name'].replace("'", "\\'") | |
css_rules.append(f"h1, h2, h3:not(.font-card h3), .gr-button-primary {{ font-family: '{primary_font}', sans-serif !important; }}") | |
if len(top_hits) >= 2: | |
secondary_font = self.font_data[top_hits[1]['corpus_id']]['name'].replace("'", "\\'") | |
css_rules.append(f".gr-textbox input, .gr-textbox textarea {{ font-family: '{secondary_font}', sans-serif !important; }}") | |
if len(top_hits) >= 3: | |
tertiary_font = self.font_data[top_hits[2]['corpus_id']]['name'].replace("'", "\\'") | |
css_rules.append(f".gr-button-secondary {{ font-family: '{tertiary_font}', sans-serif !important; }}") | |
css_rules_str = "\n ".join(css_rules) | |
css = f"""<style id="theme-fonts"> | |
{font_imports} | |
{css_rules_str} | |
/* Preserve palette fonts */ | |
.font-sample {{ | |
font-family: inherit !important; | |
}} | |
* {{ | |
transition: font-family 0.3s ease-in-out; | |
}} | |
</style>""" | |
return css | |
# --- GRADIO UI WITH WALKTHROUGH --- | |
def create_ui(generator: FontMoodGenerator): | |
"""Creates the Gradio web interface with Walkthrough.""" | |
with gr.Blocks(theme="ocean") as demo: | |
gr.Markdown(""" | |
# 📝 Font Mood Generator | |
Follow the steps below to generate and apply a personalized font palette based on your mood or description. | |
""") | |
with gr.Walkthrough(selected=0) as walkthrough: | |
# STEP 1: Input Mood | |
with gr.Step("🎯 Describe Your Mood", id=0): | |
gr.Markdown(""" | |
### Step 1: Tell us about your mood or vision | |
Describe the feeling, atmosphere, or aesthetic you're aiming for. | |
Be as detailed as you like - the more descriptive, the better the results! | |
""") | |
mood_input = gr.Textbox( | |
value="Horror movie poster with scary atmosphere", | |
label="Enter Your Mood or Scene", | |
info="Examples: 'Modern tech startup', 'Playful children's book', 'Gothic horror movie'", | |
lines=3 | |
) | |
gr.Examples( | |
[ | |
"Elegant wedding invitation with vintage charm", | |
"Modern tech startup with clean aesthetics", | |
"Playful children's book with whimsical characters", | |
"Horror movie poster with scary atmosphere", | |
"Luxury fashion brand with sophisticated appeal", | |
"Retro 1950s diner with nostalgic vibes", | |
"Academic research paper with scholarly tone", | |
"Surf shop with California beach culture", | |
"Gothic medieval manuscript with ancient feel", | |
"Futuristic sci-fi interface with cyber aesthetics" | |
], | |
inputs=mood_input, | |
) | |
generate_btn = gr.Button("Generate Font Palette →", variant="primary", size="lg") | |
# Hidden outputs to store results | |
palette_html_hidden = gr.HTML(visible=False) | |
font_data_hidden = gr.JSON(visible=False) | |
def generate_and_move(mood_text): | |
palette_html, top_hits = generator.generate_palette(mood_text) | |
# Convert to serializable format | |
font_data_json = [{"corpus_id": hit["corpus_id"], "score": hit["score"]} for hit in top_hits] | |
return palette_html, font_data_json, gr.Walkthrough(selected=1) | |
generate_btn.click( | |
fn=generate_and_move, | |
inputs=mood_input, | |
outputs=[palette_html_hidden, font_data_hidden, walkthrough] | |
) | |
# STEP 2: Review Generated Fonts | |
with gr.Step("🎨 Review Your Font Palette", id=1): | |
gr.Markdown(""" | |
### Step 2: Review your generated fonts | |
Here are the fonts that best match your mood, ranked by similarity score. | |
""") | |
palette_display = gr.HTML() | |
with gr.Row(): | |
back_to_input_btn = gr.Button("← Back to Input", variant="secondary") | |
apply_theme_btn = gr.Button("Apply Typography Theme →", variant="primary", size="lg") | |
back_to_input_btn.click( | |
fn=lambda: gr.Walkthrough(selected=0), | |
outputs=walkthrough | |
) | |
# Update display when entering this step | |
def show_generated_palette(palette_html): | |
return palette_html | |
palette_html_hidden.change( | |
fn=show_generated_palette, | |
inputs=palette_html_hidden, | |
outputs=palette_display | |
) | |
# Hidden CSS output for theming | |
theme_css_hidden = gr.HTML(visible=False) | |
def apply_theme_and_move(font_data_json): | |
# Convert back to the format expected by apply_theme_css | |
top_hits = [{"corpus_id": item["corpus_id"], "score": item["score"]} for item in font_data_json] | |
theme_css = generator.apply_theme_css(top_hits) | |
return theme_css, gr.Walkthrough(selected=2) | |
apply_theme_btn.click( | |
fn=apply_theme_and_move, | |
inputs=font_data_hidden, | |
outputs=[theme_css_hidden, walkthrough] | |
) | |
# STEP 3: Experience the Typography | |
with gr.Step("✨ Experience Your Typography", id=2): | |
gr.Markdown(""" | |
### Step 3: See your fonts in action! | |
Notice how the entire interface has transformed to reflect your chosen aesthetic. | |
""") | |
# Apply CSS when entering this step | |
theme_css_display = gr.HTML() | |
theme_css_hidden.change( | |
fn=lambda css: css, | |
inputs=theme_css_hidden, | |
outputs=theme_css_display | |
) | |
gr.Markdown(""" | |
**🎉 Your typography theme is now active!** | |
Look around the interface - the headings, buttons, and text inputs now use fonts from your generated palette. | |
**Font Roles:** | |
- **Primary Font**: Used for headings and primary buttons | |
- **Secondary Font**: Used for input fields and body text | |
- **Accent Font**: Used for secondary buttons and highlights | |
""") | |
with gr.Row(): | |
back_to_palette_btn = gr.Button("← Back to Palette", variant="secondary") | |
get_code_btn = gr.Button("Get CSS Code →", variant="primary", size="lg") | |
back_to_palette_btn.click( | |
fn=lambda: gr.Walkthrough(selected=1), | |
outputs=walkthrough | |
) | |
get_code_btn.click( | |
fn=lambda: gr.Walkthrough(selected=3), | |
outputs=walkthrough | |
) | |
# STEP 4: Export and Use | |
with gr.Step("💾 Export & Use Your Fonts", id=3): | |
theme_css_display_step4 = gr.HTML() | |
gr.Markdown(""" | |
### Step 4: Get the code and use your fonts | |
Copy the CSS code below to use your font palette in your own projects. | |
""") | |
css_code_output = gr.Code( | |
language="css", | |
label="Your Font Palette CSS", | |
value="/* Generate a palette first to see CSS code here */", | |
lines=15 | |
) | |
# Update CSS code when font data changes | |
def update_css_code(font_data_json): | |
if not font_data_json: | |
return "/* Generate a palette first to see CSS code here */" | |
top_hits = [{"corpus_id": item["corpus_id"], "score": item["score"]} for item in font_data_json] | |
return generator.generate_css_code(top_hits) | |
font_data_hidden.change( | |
fn=update_css_code, | |
inputs=font_data_hidden, | |
outputs=css_code_output | |
) | |
def apply_theme_to_step4(theme_css): | |
return theme_css | |
theme_css_hidden.change( | |
fn=apply_theme_to_step4, | |
inputs=theme_css_hidden, | |
outputs=theme_css_display_step4 | |
) | |
gr.Markdown(""" | |
**🚀 Next Steps:** | |
1. Copy the CSS code above | |
2. Include it in your website's stylesheet | |
3. Apply the font variables to your HTML elements | |
4. Enjoy your new typography! | |
""") | |
start_over_btn = gr.Button("🔄 Start Over", variant="secondary", size="lg") | |
def restart(): | |
return "", [], "", "", gr.Walkthrough(selected=0) | |
start_over_btn.click( | |
fn=restart, | |
outputs=[palette_html_hidden, font_data_hidden, theme_css_hidden, theme_css_display, walkthrough] | |
) | |
# Static CSS for font cards with DARK THEME SUPPORT | |
gr.HTML(""" | |
<style> | |
.font-palette-container { | |
display: flex; flex-direction: column; gap: 8px; | |
align-items: center; width: 100%; | |
} | |
.font-card { | |
border: 2px solid var(--border-color-primary, #e0e0e0); | |
border-radius: 8px; | |
padding: 10px; width: 90%; max-width: 600px; | |
background: var(--background-fill-primary, #ffffff); | |
box-shadow: 0 2px 6px rgba(0,0,0,0.1); | |
transition: all 0.3s ease; | |
} | |
.font-card:hover { | |
transform: translateY(-1px); | |
box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
} | |
.font-header { | |
display: flex; justify-content: space-between; | |
align-items: center; margin-bottom: 8px; | |
} | |
.font-header h3 { | |
margin: 0; | |
color: var(--body-text-color, #2c3e50); | |
font-size: 1.1em; | |
} | |
.font-score { | |
background: #3498db; color: white; padding: 3px 6px; | |
border-radius: 8px; font-size: 0.75em; font-weight: bold; | |
} | |
.font-sample { | |
font-size: 18px; line-height: 1.2; margin: 8px 0; | |
padding: 8px; | |
background: var(--background-fill-secondary, #f8f9fa); | |
border-radius: 6px; | |
border-left: 3px solid #3498db; | |
color: var(--body-text-color, #2c3e50); | |
min-height: 30px; display: flex; align-items: center; | |
} | |
.font-details { | |
display: flex; flex-direction: column; gap: 4px; | |
} | |
.font-family { | |
font-weight: bold; | |
color: var(--body-text-color-subdued, #7f8c8d); | |
font-size: 0.8em; | |
text-transform: uppercase; letter-spacing: 1px; | |
} | |
.font-description { | |
color: var(--body-text-color-subdued, #5d6d7e); | |
font-size: 0.85em; line-height: 1.3; | |
} | |
/* Dark theme fallback using media query */ | |
@media (prefers-color-scheme: dark) { | |
.font-card { | |
border-color: #4a5568; | |
background: #2d3748; | |
} | |
.font-header h3 { | |
color: #e2e8f0; | |
} | |
.font-sample { | |
background: #4a5568; | |
color: #e2e8f0; | |
} | |
.font-family { | |
color: #a0aec0; | |
} | |
.font-description { | |
color: #cbd5e0; | |
} | |
} | |
/* Additional Gradio dark theme support */ | |
.dark .font-card { | |
border-color: #4a5568; | |
background: #2d3748; | |
} | |
.dark .font-header h3 { | |
color: #e2e8f0; | |
} | |
.dark .font-sample { | |
background: #4a5568; | |
color: #e2e8f0; | |
} | |
.dark .font-family { | |
color: #a0aec0; | |
} | |
.dark .font-description { | |
color: #cbd5e0; | |
} | |
/* Force text colors for better compatibility */ | |
html[data-theme="dark"] .font-card { | |
border-color: #4a5568 !important; | |
background: #2d3748 !important; | |
} | |
html[data-theme="dark"] .font-header h3 { | |
color: #e2e8f0 !important; | |
} | |
html[data-theme="dark"] .font-sample { | |
background: #4a5568 !important; | |
color: #e2e8f0 !important; | |
} | |
html[data-theme="dark"] .font-family { | |
color: #a0aec0 !important; | |
} | |
html[data-theme="dark"] .font-description { | |
color: #cbd5e0 !important; | |
} | |
</style> | |
""") | |
return demo | |
if __name__ == "__main__": | |
# Initialize application components | |
generator = FontMoodGenerator(config=Config(), font_data=FONT_DATA) | |
demo = create_ui(generator) | |
demo.launch() |