mood-palette / app.py
bebechien's picture
Upload folder using huggingface_hub
76ca028 verified
import gradio as gr
import numpy as np
import os
from huggingface_hub import login
from sentence_transformers import SentenceTransformer, util
# --- 1. CONFIGURATION ---
# Centralized place for all settings and constants.
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')
# --- 2. COLOR DATA ---
# The color palette data is kept separate for clarity and easy modification.
COLOR_DATA = [
{
"name": "Crimson",
"hex": "#DC143C",
"description": "A deep, rich red color, leaning slightly towards purple."
},
{
"name": "Scarlet",
"hex": "#FF2400",
"description": "A brilliant, vivid red with a hint of orange."
},
{
"name": "Coral",
"hex": "#FF7F50",
"description": "A vibrant pinkish-orange reminiscent of marine invertebrates."
},
{
"name": "Tangerine",
"hex": "#F28500",
"description": "A saturated, zesty orange, like the ripe citrus fruit."
},
{
"name": "Gold",
"hex": "#FFD700",
"description": "A bright, metallic yellow associated with wealth and luxury."
},
{
"name": "Lemon Chiffon",
"hex": "#FFFACD",
"description": "A pale, light yellow, as soft and airy as the dessert."
},
{
"name": "Lime Green",
"hex": "#32CD32",
"description": "A bright green color, evoking freshness and zesty energy."
},
{
"name": "Forest Green",
"hex": "#228B22",
"description": "A dark, shaded green, like the canopy of a dense forest."
},
{
"name": "Teal",
"hex": "#008080",
"description": "A medium blue-green color, often seen as sophisticated and calming."
},
{
"name": "Cyan",
"hex": "#00FFFF",
"description": "A vibrant greenish-blue, one of the primary subtractive colors."
},
{
"name": "Sky Blue",
"hex": "#87CEEB",
"description": "A light, pale blue, like the color of a clear daytime sky."
},
{
"name": "Royal Blue",
"hex": "#4169E1",
"description": "A deep, vivid blue that is both rich and bright."
},
{
"name": "Indigo",
"hex": "#4B0082",
"description": "A deep, rich color between blue and violet in the spectrum."
},
{
"name": "Lavender",
"hex": "#E6E6FA",
"description": "A light, pale purple with a bluish hue, named after the flower."
},
{
"name": "Plum",
"hex": "#DDA0DD",
"description": "A reddish-purple color, like the ripe fruit it's named after."
},
{
"name": "Magenta",
"hex": "#FF00FF",
"description": "A purplish-red color that lies between red and violet."
},
{
"name": "Hot Pink",
"hex": "#FF69B4",
"description": "A bright, vivid pink that is both bold and energetic."
},
{
"name": "Ivory",
"hex": "#FFFFF0",
"description": "An off-white color that resembles the material from tusks and teeth."
},
{
"name": "Beige",
"hex": "#F5F5DC",
"description": "A pale sandy fawn color, often used as a warm, neutral tone."
},
{
"name": "Taupe",
"hex": "#483C32",
"description": "A dark grayish-brown or brownish-gray color."
},
{
"name": "Slate Gray",
"hex": "#708090",
"description": "A medium gray with a slight blue tinge, like the metamorphic rock."
},
{
"name": "Charcoal",
"hex": "#36454F",
"description": "A dark, almost black gray, like burnt wood."
},
{
"name": "Onyx",
"hex": "#353839",
"description": "A deep, rich black, often with a subtle hint of dark blue."
},
{
"name": "Emerald",
"hex": "#50C878",
"description": "A brilliant green, named after the precious gemstone."
},
{
"name": "Sapphire",
"hex": "#0F52BA",
"description": "A deep, lustrous blue, reminiscent of the valuable gemstone."
},
{
"name": "Ruby",
"hex": "#E0115F",
"description": "A deep red color, inspired by the gemstone of the same name."
},
{
"name": "Amethyst",
"hex": "#9966CC",
"description": "A moderate, violet-purple color, like the quartz gemstone."
},
{
"name": "Peridot",
"hex": "#E6E200",
"description": "A light olive-green or yellowish-green, named for the gem."
},
{
"name": "Turquoise",
"hex": "#40E0D0",
"description": "A greenish-blue color, often associated with tropical waters."
},
{
"name": "Silver",
"hex": "#C0C0C0",
"description": "A metallic gray color that resembles polished silver."
},
{
"name": "Bronze",
"hex": "#CD7F32",
"description": "A metallic brown color that resembles the alloy of copper and tin."
},
{
"name": "Obsidian",
"hex": "#000000",
"description": "A pure, deep black, like the volcanic glass formed from cooled lava."
}
]
# --- 3. CORE LOGIC ---
# Encapsulated in a class to manage state (model, embeddings) cleanly.
class MoodPaletteGenerator:
"""Handles model loading, embedding generation, and palette creation."""
def __init__(self, config: Config, color_data: list[dict[str, any]]):
"""Initializes the generator, logs in, and loads necessary assets."""
self.config = config
self.color_data = color_data
self._login_to_hf()
self.embedding_model = self._load_model()
self.color_embeddings = self._precompute_color_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.")
print("Note: This may fail if the model is gated.")
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_color_embeddings(self) -> np.ndarray:
"""Generates and stores embeddings for the color descriptions."""
print("Pre-computing embeddings for color palette...")
color_texts = [
f"{color['name']}, {color['description']}"
for color in self.color_data
]
embeddings = self.embedding_model.encode(
color_texts,
prompt_name=self.config.PROMPT_NAME
)
print("Embeddings computed successfully.")
return embeddings
def _get_text_color_for_bg(self, hex_color: str) -> str:
"""
Calculates the luminance of a hex color and returns black ('#000000')
or white ('#FFFFFF') for the best text contrast.
"""
hex_color = hex_color.lstrip('#')
try:
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
luminance = (0.299 * r + 0.587 * g + 0.114 * b)
return '#000000' if luminance > 150 else '#FFFFFF'
except (ValueError, IndexError):
return '#000000' # Default to black on invalid hex
def _format_palette_as_html(self, top_hits: list[dict[str, any]]) -> str:
"""Formats the top color hits into a displayable HTML string."""
if not top_hits:
return "<p>Could not generate a palette. Please try another mood.</p>"
cards_html = ""
for hit in top_hits:
color_info = self.color_data[hit['corpus_id']]
hex_code = color_info['hex']
name = color_info['name']
score = hit['score']
text_color = self._get_text_color_for_bg(hex_code)
cards_html += f"""
<div class="color-card" style="background-color: {hex_code}; color: {text_color};">
{name} | {hex_code} | Score: {score:.2f}
</div>
"""
return f"<div class='palette-container'>{cards_html}</div>"
def _create_dynamic_theme_css(self, top_hits: list[dict[str, any]]) -> str:
"""Generates a <style> block to override Gradio theme variables."""
theme_colors = []
if top_hits:
theme_colors = [
{
"bg": self.color_data[hit['corpus_id']]['hex'],
"txt": self._get_text_color_for_bg(self.color_data[hit['corpus_id']]['hex'])
}
for hit in top_hits
]
css_map = {
"button-primary-background-fill": (0, 'bg'),
"button-primary-text-color": (0, 'txt'),
"button-secondary-background-fill": (1, 'bg'),
"button-secondary-text-color": (1, 'txt'),
"block-background-fill": (2, 'bg'),
"block-info-text-color": (2, 'txt'),
"block-title-background-fill": (3, 'bg'),
"button-primary-background-fill-hover": (3, 'bg'),
"block-title-text-color": (3, 'txt'),
"button-primary-text-color-hover": (3, 'txt'),
"button-secondary-background-fill-hover": (4, 'bg'),
"button-secondary-text-color-hover": (4, 'txt'),
}
css_rules = []
num_available_colors = len(theme_colors)
for var_suffix, (index, key) in css_map.items():
if index < num_available_colors:
color_value = theme_colors[index][key]
css_rules.append(f"--{var_suffix}: {color_value};")
css_rules_str = "\n".join(css_rules)
# Create CSS variables to inject
css = f"""
<style>
:root {{
{css_rules_str}
}}
:root .dark {{
{css_rules_str}
}}
.gallery-item .gallery {{
background: {theme_colors[4]['bg']};
color: {theme_colors[4]['txt']};
}}
</style>
"""
return css
def generate_palette_and_theme(self, mood_text: str) -> tuple[str, str]:
"""
Generates a color palette HTML and a dynamic theme CSS string.
"""
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.color_embeddings, top_k=self.config.TOP_K
)[0]
palette_html = self._format_palette_as_html(top_hits)
theme_css = self._create_dynamic_theme_css(top_hits)
return palette_html, theme_css
def clear_theme(self) -> tuple[str, str]:
return "", ""
# --- 4. GRADIO UI ---
# Defines and launches the web interface.
def create_ui(generator: MoodPaletteGenerator):
"""Creates the Gradio web interface."""
with gr.Blocks(theme=gr.themes.Soft()) as demo:
# This invisible component will hold our dynamic CSS
dynamic_css_output = gr.HTML()
gr.Markdown("""
# 🎨 Mood Palette Generator
Describe a mood, a scene, or a feeling, and get a matching color palette.<br>
**The UI theme will update to match your mood!**
""")
with gr.Row():
with gr.Column(scale=4):
mood_input = gr.Textbox(
value="Strawberry ice cream",
label="Enter Your Mood or Scene",
info="Be as descriptive as you like!"
)
with gr.Column(scale=1, min_width=150):
submit_button = gr.Button("Generate Palette", variant="primary")
clear_button = gr.Button("Clear", variant="secondary")
palette_output = gr.HTML(label="Your Generated Palette")
# Define CSS for palette cards here once, instead of in every update
gr.HTML("""
<style>
.palette-container {
display: flex; flex-direction: column; gap: 10px;
align-items: center; width: 100%;
q}
.color-card {
border-radius: 10px; text-align: center; padding: 15px 10px;
width: 90%; max-width: 400px;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
font-family: sans-serif; font-size: 16px; font-weight: bold;
transition: transform 0.2s;
}
.color-card:hover { transform: scale(1.05); }
</style>
""")
# Define the function to be called by events
event_handler = generator.generate_palette_and_theme
outputs_list = [palette_output, dynamic_css_output]
gr.Examples(
[
"A futuristic city at night, slick with rain",
"The feeling of a cozy cabin during a blizzard",
"Joyful chaos at a summer music festival",
"Beach sunset with the palm tree",
"A calm and lonely winter morning",
"Vintage romance in a dusty library",
"Cyberpunk alleyway with neon signs",
],
inputs=mood_input,
outputs=outputs_list,
fn=event_handler,
run_on_click=True,
)
submit_button.click(
fn=event_handler,
inputs=mood_input,
outputs=outputs_list,
)
clear_button.click(
fn=generator.clear_theme,
outputs=outputs_list,
)
# Also allow submitting by pressing Enter in the textbox
mood_input.submit(
fn=event_handler,
inputs=mood_input,
outputs=outputs_list,
)
gr.Markdown("""
----
## What is this?
This interactive application, the **Mood Palette Generator**, transforms your words into a vibrant color palette. Simply describe a mood, a scene, or a feeling and the app will generate a set of matching colors. As a unique touch, the entire user interface dynamically updates its theme to reflect the generated palette, immersing you in your chosen mood.
## How It Works?
At its core, this tool is powered by [**EmbeddingGemma**](http://huggingface.co/google/embeddinggemma-300M), a state-of-the-art text embedding model. The process works in a few simple steps:
1. **Text to Vector**: When you enter a description, EmbeddingGemma converts your text into a numerical representation called an **embedding**. This embedding captures the semantic essence, or the "vibe" of your words.
2. **Semantic Color Search**: The application has a pre-defined library of colors, where each color is associated with its own descriptive text and a pre-computed embedding.
3. **Finding the Match**: Your input embedding is compared against the entire library of color embeddings to find the closest matches based on a similarity score.
4. **Palette Creation**: The colors with the highest similarity scores are selected and presented to you as a complete palette.
The Mood Palette Generator is a perfect example of how embeddings can be used for creative applications beyond simple text search.
""")
return demo
if __name__ == "__main__":
# Initialize application components
generator = MoodPaletteGenerator(config=Config(), color_data=COLOR_DATA)
demo = create_ui(generator)
demo.launch()