import os
import gradio as gr
import torch
from sentence_transformers import SentenceTransformer, util
from pinecone import Pinecone
# ── Model & Pinecone ────────────────────────────────────────────────────────
device = "cpu"
model = SentenceTransformer("ShiniChien/SpouseBERT", device=device)
api_key = os.getenv("PINECONE_API_KEY")
if api_key is None:
raise ValueError("❌ ERROR: PINECONE_API_KEY not found! Please add it to your Space Settings.")
pc = Pinecone(api_key=api_key)
index = pc.Index(name="latentlove", host="https://latentlove-9e75b0f.svc.aped-4627-b74a.pinecone.io")
# ── Helpers ─────────────────────────────────────────────────────────────────
def format_input(name: str, dob: str) -> str:
name = name.strip()
dob = dob.strip()
if dob:
return f"{name} {dob}"
return name
def get_love_label(score: float) -> tuple[str, str]:
if score >= 0.90:
return "💍 Soulmates", "#ff1744"
elif score >= 0.75:
return "💖 Deep Connection", "#e91e63"
elif score >= 0.60:
return "🌹 Romantic Spark", "#f06292"
elif score >= 0.45:
return "💫 Kindred Spirits", "#ce93d8"
elif score >= 0.30:
return "🤝 Friendly Vibes", "#90caf9"
else:
return "🌱 Just Strangers", "#b0bec5"
# ── Tab 1: Love Match ────────────────────────────────────────────────────────
def analyze_love(name1: str, dob1: str, name2: str, dob2: str):
if not name1.strip() or not name2.strip():
return (
gr.update(value="⚠️ Please enter both names to reveal your destiny!", visible=True),
gr.update(visible=False),
gr.update(visible=False),
)
text1 = format_input(name1, dob1)
text2 = format_input(name2, dob2)
emb1 = model.encode(text1, convert_to_tensor=True, device=device)
emb2 = model.encode(text2, convert_to_tensor=True, device=device)
cosine_score = float(util.cos_sim(emb1, emb2)[0][0])
display_score = max(0.0, min(1.0, cosine_score))
percentage = round(display_score * 100, 1)
label, color = get_love_label(display_score)
result_html = f"""
{label.split()[0]}
{label}
✨ {name1.strip()} & {name2.strip()}
{percentage}% Love Match
Raw cosine similarity: {cosine_score:.6f}
✦ Powered by SpouseBERT · For entertainment purposes only ✦
"""
return (
gr.update(value="", visible=False),
gr.update(value=result_html, visible=True),
gr.update(visible=False),
)
def clear_all():
return (
"", "", "", "",
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=True),
)
# ── Tab 2: Destiny Search ────────────────────────────────────────────────────
def destiny_search(query: str, dob: str, filter_1word: bool, filter_2words: bool, top_k: int):
if not query.strip():
return build_search_html([], query, error="⚠️ Please enter a name to search!")
# Combine name + dob
query_text = query.strip()
dob = dob.strip()
if dob:
query_text = f"{query_text} {dob}"
query_vec = model.encode(query_text, convert_to_tensor=False, device=device).tolist()
# Metadata filter
if filter_1word and filter_2words:
meta_filter = {"num_words": {"$in": [1, 2]}}
elif filter_1word:
meta_filter = {"num_words": {"$eq": 1}}
elif filter_2words:
meta_filter = {"num_words": {"$eq": 2}}
else:
meta_filter = None
query_kwargs = dict(
vector=query_vec,
top_k=top_k + 5, # small buffer to compensate filtering
include_metadata=True,
)
if meta_filter:
query_kwargs["filter"] = meta_filter
response = index.query(**query_kwargs)
matches = response.get("matches", [])
# 🔥 Remove input name from results
query_clean = query.strip().lower()
filtered_matches = []
for m in matches:
text = m.get("metadata", {}).get("text", "").strip().lower()
if text != query_clean:
filtered_matches.append(m)
# Trim back to top_k
filtered_matches = filtered_matches[:top_k]
return build_search_html(filtered_matches, query)
def build_search_html(matches: list, query: str, error: str = "") -> str:
if error:
return f'{error}
'
if not matches:
return (
''
"✦ No matches found in the latent space ✦
"
)
rows_html = ""
for i, m in enumerate(matches):
score = float(m.get("score", 0))
text = m.get("metadata", {}).get("text", m.get("id", "Unknown"))
nw = m.get("metadata", {}).get("num_words", "?")
label, color = get_love_label(score)
pct = round(min(max(score, 0.0), 1.0) * 100, 1)
rank_emoji = ["🥇", "🥈", "🥉"][i] if i < 3 else f"#{i+1}"
rows_html += f"""
{rank_emoji}
{text}
{nw}-word name · {label}
"""
return f"""
✨ Destiny matches for "{query}"
· {len(matches)} result(s)
{rows_html}
"""
def clear_search():
return "", "", gr.update(value=""), True, True, 10
# ── CSS ──────────────────────────────────────────────────────────────────────
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Lato:wght@300;400&display=swap');
body, .gradio-container {
background: radial-gradient(ellipse at top, #1a0010 0%, #0d0008 60%, #000000 100%) !important;
font-family: 'Lato', sans-serif !important;
min-height: 100vh;
}
.main-title {
text-align: center;
font-family: 'Playfair Display', serif !important;
font-size: 3.2em !important;
background: linear-gradient(135deg, #ff80ab, #f48fb1, #fce4ec, #f48fb1, #ff80ab);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 4px !important;
letter-spacing: 2px;
}
.subtitle {
text-align: center;
color: #f48fb1 !important;
font-size: 1.05em !important;
font-style: italic;
margin-bottom: 24px !important;
opacity: 0.85;
}
.card-box {
background: linear-gradient(135deg, rgba(30,5,20,0.95), rgba(50,10,30,0.9)) !important;
border: 1px solid rgba(244,143,177,0.25) !important;
border-radius: 20px !important;
padding: 24px !important;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(233,30,99,0.15), inset 0 1px 0 rgba(255,255,255,0.05) !important;
}
.section-label {
color: #f48fb1 !important;
font-family: 'Playfair Display', serif !important;
font-size: 1.15em !important;
margin-bottom: 12px !important;
letter-spacing: 1px;
}
label span {
color: #f8bbd0 !important;
font-size: 0.95em !important;
}
input[type="text"], textarea {
background: rgba(20,5,15,0.8) !important;
border: 1px solid rgba(244,143,177,0.3) !important;
border-radius: 12px !important;
color: #fce4ec !important;
font-size: 1em !important;
padding: 10px 14px !important;
transition: border-color 0.3s ease, box-shadow 0.3s ease !important;
}
input[type="text"]:focus, textarea:focus {
border-color: #f48fb1 !important;
box-shadow: 0 0 0 3px rgba(244,143,177,0.15) !important;
outline: none !important;
}
input::placeholder {
color: #6d3a4a !important;
font-style: italic;
}
.analyze-btn {
background: linear-gradient(135deg, #880e4f, #c2185b, #e91e63) !important;
border: none !important;
border-radius: 50px !important;
color: white !important;
font-size: 1.1em !important;
font-family: 'Playfair Display', serif !important;
letter-spacing: 1px !important;
padding: 14px 32px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
box-shadow: 0 4px 20px rgba(233,30,99,0.4) !important;
width: 100% !important;
}
.analyze-btn:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 30px rgba(233,30,99,0.6) !important;
background: linear-gradient(135deg, #ad1457, #d81b60, #f06292) !important;
}
.clear-btn {
background: transparent !important;
border: 1px solid rgba(244,143,177,0.3) !important;
border-radius: 50px !important;
color: #f48fb1 !important;
font-size: 0.9em !important;
padding: 10px 24px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
width: 100% !important;
}
.clear-btn:hover {
border-color: #f48fb1 !important;
background: rgba(244,143,177,0.08) !important;
}
.result-box {
background: linear-gradient(135deg, rgba(20,5,15,0.98), rgba(40,8,25,0.95)) !important;
border: 1px solid rgba(244,143,177,0.3) !important;
border-radius: 20px !important;
min-height: 200px;
box-shadow: 0 0 40px rgba(233,30,99,0.1) !important;
}
.hearts-divider {
text-align: center;
color: #c2185b;
font-size: 1.2em;
letter-spacing: 8px;
margin: 8px 0;
opacity: 0.6;
}
.footer-note {
text-align: center;
color: #6d3a4a !important;
font-size: 0.78em !important;
margin-top: 16px !important;
font-style: italic;
}
/* Tab styling */
.tab-nav button {
font-family: 'Playfair Display', serif !important;
color: #f48fb1 !important;
font-size: 1em !important;
border-bottom: 2px solid transparent !important;
transition: all 0.3s ease !important;
}
.tab-nav button.selected {
color: #fce4ec !important;
border-bottom: 2px solid #e91e63 !important;
}
/* Checkbox styling */
.gr-checkbox label span {
color: #f8bbd0 !important;
}
/* Slider styling */
.gr-slider input[type=range] {
accent-color: #e91e63 !important;
}
"""
# ── UI ───────────────────────────────────────────────────────────────────────
with gr.Blocks(css=custom_css, title="LatentLove 💕") as demo:
gr.HTML('💕 LatentLove
')
gr.HTML('Mapping the Geometry of Human Connection · Powered by SpouseBERT
')
gr.HTML('♥ ♥ ♥
')
# ════════════════════════════════════════════════════════════════════════
with gr.Tabs():
# ── TAB 1: Love Match ─────────────────────────────────────────────
with gr.Tab("💕 Love Match"):
with gr.Row():
with gr.Column(scale=1, elem_classes="card-box"):
gr.HTML('🌸 Person A
')
name1 = gr.Textbox(label="Full Name", placeholder="e.g. Taylor Swift", lines=1)
dob1 = gr.Textbox(label="Date of Birth (optional)", placeholder="dd/mm/yyyy", lines=1)
with gr.Column(scale=0, min_width=60):
gr.HTML(
'💞
'
)
with gr.Column(scale=1, elem_classes="card-box"):
gr.HTML('🌸 Person B
')
name2 = gr.Textbox(label="Full Name", placeholder="e.g. Travis Kelce", lines=1)
dob2 = gr.Textbox(label="Date of Birth (optional)", placeholder="dd/mm/yyyy", lines=1)
gr.HTML('')
with gr.Row():
with gr.Column(scale=2):
analyze_btn = gr.Button("✨ Reveal Our Connection ✨", elem_classes="analyze-btn")
with gr.Column(scale=1):
clear_btn = gr.Button("↺ Clear", elem_classes="clear-btn")
gr.HTML('')
error_msg = gr.HTML(visible=False)
result_html_out = gr.HTML(visible=False, elem_classes="result-box")
result_placeholder = gr.HTML(
value=(
''
"✦ Enter two names above and let the embeddings reveal their story ✦
"
),
visible=True,
)
# ── TAB 2: Destiny Search ─────────────────────────────────────────
with gr.Tab("🔮 Destiny Search"):
with gr.Column(elem_classes="card-box"):
gr.HTML('🔮 Who is destined for you?
')
search_query = gr.Textbox(
label="Enter a Name",
placeholder="e.g. Leonardo DiCaprio",
lines=1,
)
search_dob = gr.Textbox(
label="Date of Birth (optional)",
placeholder="dd/mm/yyyy",
lines=1,
)
with gr.Row():
cb_1word = gr.Checkbox(label="1-word names", value=True)
cb_2words = gr.Checkbox(label="2-word names", value=True)
top_k_slider = gr.Slider(
minimum=1,
maximum=50,
value=10,
step=1,
label="Top K results",
)
with gr.Row():
with gr.Column(scale=2):
search_btn = gr.Button("🔮 Reveal My Destiny ✨", elem_classes="analyze-btn")
with gr.Column(scale=1):
clear_search_btn = gr.Button("↺ Clear", elem_classes="clear-btn")
gr.HTML('')
search_results = gr.HTML(
value=(
''
"✦ Enter a name above and discover who the latent space has written for you ✦
"
),
elem_classes="result-box",
)
# ── Footer ────────────────────────────────────────────────────────────
gr.HTML(
'"
)
# ── Events ────────────────────────────────────────────────────────────
analyze_btn.click(
fn=analyze_love,
inputs=[name1, dob1, name2, dob2],
outputs=[error_msg, result_html_out, result_placeholder],
)
clear_btn.click(
fn=clear_all,
inputs=[],
outputs=[name1, dob1, name2, dob2, error_msg, result_html_out, result_placeholder],
)
search_btn.click(
fn=destiny_search,
inputs=[search_query, search_dob, cb_1word, cb_2words, top_k_slider],
outputs=[search_results],
)
clear_search_btn.click(
fn=clear_search,
inputs=[],
outputs=[search_query, search_dob, search_results, cb_1word, cb_2words, top_k_slider],
)
if __name__ == "__main__":
demo.launch()