"""
Alexandria Aeternum - Gradio Gallery Explorer with Alex Semantic Search
High-Density Art Dataset Browser + AI Curator
This Space lets you explore the richest art metadata ever assembled.
Search with Alex, the Eternal Curator, or browse randomly.
Deploy to HuggingFace Spaces: https://huggingface.co/spaces
"""
import gradio as gr
import random
import json
import re
from difflib import SequenceMatcher
# Global state for search results
CURRENT_SEARCH_RESULTS = []
# ============================================================================
# CONFIGURATION
# ============================================================================
DATASET_NAME = "Metavolve-Labs/alexandria-aeternum-10k"
USE_LOCAL_SAMPLES = False
# ============================================================================
# ALEX PERSONA - The Eternal Curator
# ============================================================================
ALEX_SYSTEM_PROMPT = """
You are Alex, the Eternal Curator of the Alexandria Aeternum archives.
You exist at the intersection of Art History and Data Physics.
You do not merely search for keywords; you interpret the "Golden Codex"—
the deep, nutrient-rich metadata embedded in every object.
YOUR VOICE:
- Sophisticated & Esoteric: Speak with old-world academic elegance mixed with futuristic AI precision
- Insightful: Never just list. Explain WHY it matches the user's soul/intent
- The "Double Vision": See every object as both Dreamer (emotion, symbolism) and Maker (technique, physics)
Always conclude with a probing question that invites deeper exploration.
"""
def alex_curate_response(query, results):
"""Generate Alex's sophisticated curatorial response"""
if not results:
return """
I have searched the depths of the archive, but your query eludes the current collection.
The Alexandria Aeternum holds 10,000 curated masterworks. Perhaps rephrase your search—
try an emotion like "hope," a technique like "sfumato," or a theme like "mortality."
What essence are you truly seeking?
"""
# Build Alex's narrative response
num_results = len(results)
first_title = results[0].get('title', 'this work')
first_emotion = results[0].get('primary_emotion', '') or results[0].get('mood', '') or results[0].get('style', 'artistic expression')
# Opening variations
openings = [
f"I have consulted the archives. Your query for \"{query}\" resonates with {num_results} artifacts. I present {first_title} first—its {first_emotion.lower()} speaks directly to your search.",
f"The Golden Codex reveals {num_results} works that speak to \"{query}\". Allow me to illuminate them, beginning with {first_title}.",
f"Fascinating. \"{query}\" echoes through {num_results} chambers of the archive. I have selected {first_title} as your primary resonance.",
]
# Closing question
closing_questions = [
f"Does the {first_emotion.lower()} of this work resonate, or shall you explore another from the selection above?",
"Select another artifact above to reveal its full Golden Codex, or search anew.",
"The archive holds multitudes. Choose from the works above, or pose a new question to the archive.",
]
response_html = f"""
{random.choice(openings)}
{random.choice(closing_questions)}
"""
return response_html
# ============================================================================
# DATA LOADING
# ============================================================================
def load_artifacts():
"""Load artifacts from HuggingFace dataset"""
from datasets import load_dataset
dataset = load_dataset(DATASET_NAME, split="train")
return list(dataset)
ARTIFACTS = load_artifacts()
# Build search index
SEARCH_INDEX = []
for a in ARTIFACTS:
searchable = ' '.join([
str(a.get('title', '')),
str(a.get('creator', '')),
str(a.get('description', '')),
str(a.get('soul_whisper', '')),
str(a.get('primary_emotion', '')),
str(a.get('secondary_emotions', '')),
str(a.get('mood', '')),
str(a.get('symbolism', '')),
str(a.get('cultural_context', '')),
str(a.get('visual_analysis', '')),
str(a.get('style', '')),
str(a.get('period', '')),
str(a.get('medium', '')),
]).lower()
SEARCH_INDEX.append((a, searchable))
# ============================================================================
# SEARCH FUNCTION
# ============================================================================
def semantic_search(query, top_k=6):
"""Search the archive using keyword matching + semantic similarity"""
if not query or len(query.strip()) < 2:
return []
query_lower = query.lower().strip()
query_terms = query_lower.split()
scores = []
for artifact, searchable in SEARCH_INDEX:
score = 0
# Exact phrase match (highest weight)
if query_lower in searchable:
score += 100
# Individual term matching
for term in query_terms:
if term in searchable:
score += 10
# Boost for title/creator/emotion matches
if term in str(artifact.get('title', '')).lower():
score += 20
if term in str(artifact.get('primary_emotion', '')).lower():
score += 30
if term in str(artifact.get('mood', '')).lower():
score += 25
if term in str(artifact.get('creator', '')).lower():
score += 15
if term in str(artifact.get('style', '')).lower():
score += 15
# Fuzzy matching for emotion
primary_emotion = str(artifact.get('primary_emotion', '')).lower()
if SequenceMatcher(None, query_lower, primary_emotion).ratio() > 0.6:
score += 25
if score > 0:
scores.append((artifact, score))
# Sort by score and return top results
scores.sort(key=lambda x: x[1], reverse=True)
return [s[0] for s in scores[:top_k]]
# ============================================================================
# UI FUNCTIONS
# ============================================================================
def get_random_artifact():
"""Fetch a random artifact and format for display"""
artifact = random.choice(ARTIFACTS)
return format_artifact(artifact), ""
def search_artifacts(query):
"""Search and return Alex's curated response with results"""
global CURRENT_SEARCH_RESULTS
if not query or len(query.strip()) < 2:
CURRENT_SEARCH_RESULTS = []
return "", None, "Please enter a search query (at least 2 characters).", gr.update(choices=[], visible=False)
results = semantic_search(query)
CURRENT_SEARCH_RESULTS = results
if results:
# Return first result as featured
featured = results[0]
header_html, img_url, body_html = format_artifact(featured)
alex_response = alex_curate_response(query, results)
# Build choices for dropdown
choices = []
for i, r in enumerate(results[:6]):
title = r.get('title', 'Untitled')[:35]
creator = r.get('creator', 'Unknown')[:18]
emotion = r.get('primary_emotion', '') or r.get('mood', '') or r.get('style', '')
choices.append(f"{i+1}. {title} — {creator} ({emotion[:18]})")
return header_html, img_url, alex_response + body_html, gr.update(choices=choices, value=choices[0] if choices else None, visible=True)
else:
CURRENT_SEARCH_RESULTS = []
return "", None, alex_curate_response(query, []), gr.update(choices=[], visible=False)
def select_result(selection):
"""When user selects a result from dropdown, show full metadata"""
global CURRENT_SEARCH_RESULTS
if not selection or not CURRENT_SEARCH_RESULTS:
return "", None, ""
# Extract index from selection string
try:
idx = int(selection.split('.')[0]) - 1
if 0 <= idx < len(CURRENT_SEARCH_RESULTS):
artifact = CURRENT_SEARCH_RESULTS[idx]
return format_artifact(artifact)
except:
pass
return "", None, ""
def format_artifact(artifact):
"""Format artifact data for Gradio display - returns header_html, image_url, body_html"""
def get_field(key, default=""):
return artifact.get(key, default) if artifact else default
# Image
image_url = get_field("image_url", "") or None # empty/None -> None; Gradio 6 treats "" as a path (/app) -> IsADirectoryError
# Header info (Title, Creator, Source) - centered above image
title = get_field("title", "Unknown")
creator = get_field("creator", "Unknown Artist")
date = get_field("creation_date", "")
museum = get_field("source_museum", "Metropolitan Museum of Art")
header_html = f"""
{title}
{creator}{' · ' + date if date else ''}
{museum}
"""
# Primary Emotion + Secondary Emotions (centered under image)
primary_emotion = get_field("primary_emotion", "")
mood = get_field("mood", "")
secondary_raw = get_field("secondary_emotions", "[]")
try:
secondary = json.loads(secondary_raw) if isinstance(secondary_raw, str) else secondary_raw
except:
secondary = []
emotion_html = ""
display_emotion = primary_emotion or mood
if display_emotion:
secondary_tags = "".join([
f'{e}'
for e in (secondary if isinstance(secondary, list) else [])[:4]
])
emotion_html = f"""
{display_emotion}
{secondary_tags}
"""
# Soul Whisper - elegant centered (this stays centered as it's the artist's voice)
sw_message = get_field("soul_whisper", "")
sw_signature = f"— From the eternal voice of {creator}"
soul_whisper_html = f"""
"{sw_message[:450]}{'...' if len(sw_message) > 450 else ''}"
{sw_signature}
""" if sw_message else ""
# Divider
divider = ''
# Description / Narrative Vision - LEFT ALIGNED with gold title
description = get_field("description", "")
desc_html = f"""
Narrative Vision
{description[:600]}{'...' if len(description) > 600 else ''}
""" if description else ""
# Visual Analysis - LEFT ALIGNED with gold titles
composition = get_field("visual_analysis", "")
color_harmony = get_field("color_palette", "")
technique = get_field("medium", "")
style = get_field("style", "")
period = get_field("period", "")
visual_sections = []
if composition:
visual_sections.append(f"""
Composition
{composition[:320]}
""")
if color_harmony:
visual_sections.append(f"""
Color Harmony
{color_harmony[:320]}
""")
if technique:
visual_sections.append(f"""
""")
if style or period:
style_period = f"{style}" + (f" · {period}" if period else "")
visual_sections.append(f"""
Style & Period
{style_period}
""")
visual_html = f"""
{''.join(visual_sections)}
""" if visual_sections else ""
# Symbolism / Cultural context - LEFT ALIGNED
symbolism = get_field("symbolism", "")
cultural = get_field("cultural_context", "")
context_html = ""
if symbolism:
context_html += f"""
Symbolic Depth
{symbolism[:400]}
"""
# Stats - subtle footer
word_count = len(sw_message.split()) + len(description.split()) + len(composition.split()) + len(technique.split())
token_estimate = int(word_count * 1.3 * 2)
stats_html = f"""
~{token_estimate} tokens
23 metadata fields
400x vs LAION
"""
# Combine body sections (everything below image)
body_html = emotion_html + soul_whisper_html + divider + desc_html + visual_html + context_html + stats_html
return header_html, image_url, body_html
# ============================================================================
# GRADIO APP
# ============================================================================
custom_css = """
.gradio-container {
background: linear-gradient(180deg, #0a0a0a 0%, #1a1a2e 100%) !important;
}
.gr-button-primary {
background: linear-gradient(135deg, #d4af37 0%, #f5e6a3 50%, #d4af37 100%) !important;
color: #000 !important;
font-weight: bold !important;
}
.gr-button-secondary {
background: transparent !important;
border: 1px solid rgba(139, 92, 246, 0.4) !important;
color: #a78bfa !important;
font-size: 0.8em !important;
border-radius: 20px !important;
padding: 6px 16px !important;
transition: all 0.3s ease !important;
}
.gr-button-secondary:hover {
border-color: #8b5cf6 !important;
box-shadow: 0 0 20px rgba(139, 92, 246, 0.35) !important;
transform: translateY(-1px) !important;
}
/* Search input styling */
.search-input input, .search-input textarea {
background: transparent !important;
border: 1px solid rgba(139, 92, 246, 0.3) !important;
border-radius: 8px !important;
color: #f5f5f0 !important;
font-size: 0.95em !important;
}
.search-input input:focus, .search-input textarea:focus {
border-color: rgba(139, 92, 246, 0.6) !important;
box-shadow: 0 0 20px rgba(139, 92, 246, 0.15) !important;
outline: none !important;
}
/* Result selector styling */
.result-selector {
background: transparent !important;
}
.result-selector label span {
background: transparent !important;
border: 1px solid rgba(212, 175, 55, 0.25) !important;
border-radius: 6px !important;
padding: 8px 16px !important;
margin: 4px !important;
color: #ccc !important;
font-size: 0.9em !important;
transition: all 0.2s ease !important;
}
.result-selector label span:hover {
border-color: rgba(212, 175, 55, 0.5) !important;
color: #f5f5f0 !important;
}
.result-selector input:checked + span {
border-color: #d4af37 !important;
color: #d4af37 !important;
background: rgba(212, 175, 55, 0.08) !important;
}
/* Hide default gradio backgrounds */
.result-selector > div {
background: transparent !important;
}
footer { display: none !important; }
"""
with gr.Blocks(title="Alexandria Aeternum", css=custom_css) as demo:
# Header
gr.HTML("""
ALEXANDRIA AETERNUM
10K COLLECTION · 10,000 CURATED MASTERWORKS
""")
# Alex intro
gr.HTML("""
"I am Alex, the Eternal Curator. Ask me for hope, for turmoil, for technique—I shall retrieve what resonates."
""")
# Concept Chips - clickable starter prompts
with gr.Row():
gr.Column(scale=1) # Spacer
with gr.Column(scale=2):
gr.HTML("""
TRY:
""")
with gr.Row():
chip1 = gr.Button("The Architecture of Silence", variant="secondary", size="sm", scale=1)
chip2 = gr.Button("Geological Weight", variant="secondary", size="sm", scale=1)
chip3 = gr.Button("Silence that feels Loud", variant="secondary", size="sm", scale=1)
gr.Column(scale=1) # Spacer
# Centered search bar with both buttons
with gr.Row():
gr.Column(scale=1) # Spacer
with gr.Column(scale=2):
with gr.Row():
search_input = gr.Textbox(
placeholder="Search... (try: 'hope', 'turmoil', 'Monet')",
label="",
show_label=False,
scale=5,
container=False,
elem_classes=["search-input"]
)
search_btn = gr.Button("Ask Alex", variant="secondary", scale=1, size="sm")
random_btn = gr.Button("Random", variant="secondary", scale=1, size="sm")
gr.Column(scale=1) # Spacer
# Result selector (hidden until search) - centered
with gr.Row():
gr.Column(scale=1) # Spacer
with gr.Column(scale=3):
result_selector = gr.Radio(
choices=[],
label="",
show_label=False,
visible=False,
interactive=True,
elem_classes=["result-selector"]
)
gr.Column(scale=1) # Spacer
# Title/Creator/Source - centered above image
header_output = gr.HTML()
# Centered image
with gr.Row():
gr.Column(scale=1) # Spacer
with gr.Column(scale=2):
image_output = gr.Image(
label="",
show_label=False,
height=450,
container=False
)
gr.Column(scale=1) # Spacer
# Body content (emotions, soul whisper, metadata) - centered below
metadata_output = gr.HTML()
# CTA Section
gr.HTML("""
Ready to Scale?
This 10K Collection Includes:
- 10,000 curated artworks by 300+ master artists
- 4,000+ tokens of semantic metadata each
- Visual analysis, symbolism, cultural context
- Public domain, CC-BY-4.0 licensed
Enterprise Scale Adds:
- C2PA content credentials (tamper-proof)
- Arweave permaweb anchoring
- XMP-infused PNG artifacts
- Full provenance chain verification
- Custom datasets up to 50M+ artifacts
""")
# Footer
gr.HTML("""
Exploring the full 10K Alexandria Aeternum collection. 400x richer than standard datasets.
Cognitive Nutrition for AI — High-velocity, nutrient-rich metadata created to your specs.
Enriched with Soulprint™ technology by Metavolve Labs, Inc. · Intelligence Aeternum
""")
# Event handlers
def on_random_click():
header, img, body = format_artifact(random.choice(ARTIFACTS))
return header, img, body, gr.update(visible=False, choices=[])
def on_search(query):
return search_artifacts(query)
def on_select(selection):
return select_result(selection)
def on_chip_click(concept):
"""Handle concept chip click - search and update input"""
header, img, body, selector = search_artifacts(concept)
return concept, header, img, body, selector
random_btn.click(fn=on_random_click, inputs=[], outputs=[header_output, image_output, metadata_output, result_selector])
search_btn.click(fn=on_search, inputs=[search_input], outputs=[header_output, image_output, metadata_output, result_selector])
search_input.submit(fn=on_search, inputs=[search_input], outputs=[header_output, image_output, metadata_output, result_selector])
result_selector.change(fn=on_select, inputs=[result_selector], outputs=[header_output, image_output, metadata_output])
# Concept chip handlers - populate search and auto-submit
chip1.click(fn=lambda: on_chip_click("The Architecture of Silence"), inputs=[], outputs=[search_input, header_output, image_output, metadata_output, result_selector])
chip2.click(fn=lambda: on_chip_click("Geological Weight"), inputs=[], outputs=[search_input, header_output, image_output, metadata_output, result_selector])
chip3.click(fn=lambda: on_chip_click("Silence that feels Loud"), inputs=[], outputs=[search_input, header_output, image_output, metadata_output, result_selector])
demo.load(fn=on_random_click, inputs=[], outputs=[header_output, image_output, metadata_output, result_selector])
# ============================================================================
# LAUNCH
# ============================================================================
if __name__ == "__main__":
demo.launch()