|
|
import os |
|
|
import logging |
|
|
import base64 |
|
|
import gradio as gr |
|
|
import requests |
|
|
|
|
|
|
|
|
logging.basicConfig( |
|
|
level=logging.INFO, |
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
|
|
) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
ZODIAC_SIGNS = ['aries','taurus','gemini','cancer','leo','virgo','libra','scorpio','sagittarius','capricorn','aquarius','pisces'] |
|
|
|
|
|
|
|
|
def call_beandev_api(sign: str, date_str: str) -> str: |
|
|
"""Call the Beandev Aistrology API. |
|
|
|
|
|
Uses the Beandev Aistrology API to get English horoscope data. |
|
|
|
|
|
Args: |
|
|
sign: Zodiac sign name (e.g., 'aries', 'taurus'). |
|
|
date_str: Date in YYYY-MM-DD format (e.g., '2025-11-21'). |
|
|
|
|
|
Returns: |
|
|
Formatted horoscope text in English, or an error message starting with |
|
|
'__API call failed:' if the request fails. |
|
|
""" |
|
|
try: |
|
|
from datetime import datetime |
|
|
|
|
|
|
|
|
if not date_str: |
|
|
date_str = datetime.now().strftime('%Y-%m-%d') |
|
|
|
|
|
logger.info(f"Fetching horoscope for sign={sign}, date={date_str}") |
|
|
|
|
|
|
|
|
url = "https://api.aistrology.beandev.xyz/v1" |
|
|
|
|
|
headers = { |
|
|
'Accept': 'application/json', |
|
|
'Content-Type': 'application/json', |
|
|
'User-Agent': 'Zodiac-AI/1.0 (+https://huggingface.co/spaces/mixklim/Zodiac-AI)' |
|
|
} |
|
|
payload = {'sign': sign.lower(), 'date': date_str} |
|
|
|
|
|
|
|
|
try: |
|
|
response = requests.post(url, headers=headers, json=payload, timeout=10) |
|
|
except requests.RequestException as e: |
|
|
logger.error(f"HTTP request to Beandev API failed: {e}") |
|
|
raise |
|
|
|
|
|
|
|
|
if response.status_code == 403: |
|
|
logger.error(f"Beandev API returned 403 Forbidden. Response body: {response.text}") |
|
|
return ("__API call failed: 403 Forbidden: The external Beandev API refused the request.") |
|
|
|
|
|
|
|
|
if not response.ok: |
|
|
logger.error(f"Beandev API error: status={response.status_code}, body={response.text}") |
|
|
response.raise_for_status() |
|
|
|
|
|
|
|
|
data = response.json() |
|
|
horoscope = None |
|
|
for item in data: |
|
|
if item['sign'].lower() == sign.lower(): |
|
|
horoscope = item |
|
|
break |
|
|
|
|
|
if not horoscope: |
|
|
raise ValueError(f"Sign {sign} not found in API response") |
|
|
|
|
|
logger.info(f"Successfully fetched horoscope for {sign} - {date_str}") |
|
|
|
|
|
|
|
|
zodiac_emojis = { |
|
|
'aries': '♈', 'taurus': '♉', 'gemini': '♊', 'cancer': '♋', |
|
|
'leo': '♌', 'virgo': '♍', 'libra': '♎', 'scorpio': '♏', |
|
|
'sagittarius': '♐', 'capricorn': '♑', 'aquarius': '♒', 'pisces': '♓' |
|
|
} |
|
|
|
|
|
emoji = zodiac_emojis.get(sign.lower(), '✨') |
|
|
|
|
|
|
|
|
formatted = f"""<div style="font-family: 'Segoe UI', system-ui, sans-serif; line-height: 1.8; color: #f0f0f0;"> |
|
|
|
|
|
<div style="text-align: center; margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; box-shadow: 0 8px 20px rgba(0,0,0,0.3);"> |
|
|
<h1 style="margin: 0; font-size: 2.2em; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);">{emoji} {sign.capitalize()}</h1> |
|
|
<p style="margin: 8px 0 0 0; font-size: 1.1em; color: #e0e0ff; opacity: 0.95;">{horoscope['date_range']}</p> |
|
|
<p style="margin: 5px 0 0 0; font-size: 0.95em; color: #fff; opacity: 0.8;">📅 {horoscope['current_date']}</p> |
|
|
</div> |
|
|
|
|
|
<div style="background: rgba(255,255,255,0.05); padding: 20px; border-radius: 12px; margin-bottom: 15px; border-left: 4px solid #667eea;"> |
|
|
<h3 style="margin: 0 0 12px 0; color: #a8b3ff; font-size: 1.2em;">✨ Your Daily Horoscope</h3> |
|
|
<p style="margin: 0; font-size: 1.05em; line-height: 1.9; color: #e8e8e8;">{horoscope['description']}</p> |
|
|
</div> |
|
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;"> |
|
|
<div style="background: linear-gradient(135deg, rgba(255,107,107,0.15) 0%, rgba(255,107,107,0.05) 100%); padding: 18px; border-radius: 12px; border: 1px solid rgba(255,107,107,0.3);"> |
|
|
<h4 style="margin: 0 0 8px 0; color: #ffb3ba; font-size: 1em;">😊 Mood</h4> |
|
|
<p style="margin: 0; font-size: 1.1em; font-weight: 500; color: #ffd4d4;">{horoscope['mood'].capitalize()}</p> |
|
|
</div> |
|
|
|
|
|
<div style="background: linear-gradient(135deg, rgba(255,193,7,0.15) 0%, rgba(255,193,7,0.05) 100%); padding: 18px; border-radius: 12px; border: 1px solid rgba(255,193,7,0.3);"> |
|
|
<h4 style="margin: 0 0 8px 0; color: #ffd54f; font-size: 1em;">💖 Best Match</h4> |
|
|
<p style="margin: 0; font-size: 1.1em; font-weight: 500; color: #ffe082;">{horoscope['compatibility'].capitalize()}</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div style="background: linear-gradient(135deg, rgba(76,175,80,0.15) 0%, rgba(76,175,80,0.05) 100%); padding: 20px; border-radius: 12px; border: 1px solid rgba(76,175,80,0.3);"> |
|
|
<h3 style="margin: 0 0 15px 0; color: #a5d6a7; font-size: 1.15em;">🍀 Lucky Elements</h3> |
|
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;"> |
|
|
<div> |
|
|
<p style="margin: 0; font-size: 0.85em; color: #c8e6c9; opacity: 0.8;">Number</p> |
|
|
<p style="margin: 4px 0 0 0; font-size: 1.3em; font-weight: bold; color: #e8f5e9;">{horoscope['lucky_number']}</p> |
|
|
</div> |
|
|
<div> |
|
|
<p style="margin: 0; font-size: 0.85em; color: #c8e6c9; opacity: 0.8;">Time</p> |
|
|
<p style="margin: 4px 0 0 0; font-size: 1.3em; font-weight: bold; color: #e8f5e9;">{horoscope['lucky_time']}</p> |
|
|
</div> |
|
|
<div> |
|
|
<p style="margin: 0; font-size: 0.85em; color: #c8e6c9; opacity: 0.8;">Color</p> |
|
|
<p style="margin: 4px 0 0 0; font-size: 1.3em; font-weight: bold; color: #e8f5e9;">{horoscope['color'].capitalize()}</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</div>""" |
|
|
|
|
|
return formatted |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to fetch horoscope for {sign} - {date_str}: {e}", exc_info=True) |
|
|
error_msg = str(e) |
|
|
if "503" in error_msg: |
|
|
return "__API call failed: The Beandev API service is currently unavailable (503 Service Unavailable). This is a temporary issue with the external API provider." |
|
|
return f"__API call failed: {e}" |
|
|
|
|
|
|
|
|
def get_horoscope(sign: str, date_str: str) -> str: |
|
|
"""Retrieve a horoscope from Beandev API. |
|
|
|
|
|
Calls the Beandev Aistrology API for English horoscope data. |
|
|
|
|
|
Args: |
|
|
sign: Zodiac sign name (e.g., 'aries', 'taurus'). |
|
|
date_str: Date in YYYY-MM-DD format (e.g., '2025-11-21'). |
|
|
|
|
|
Returns: |
|
|
A formatted horoscope string in English from the Beandev API, |
|
|
or an error message if the API is unavailable. |
|
|
""" |
|
|
logger.info(f"User requested horoscope: sign={sign}, date={date_str}") |
|
|
|
|
|
result = call_beandev_api(sign, date_str) |
|
|
|
|
|
|
|
|
if result.startswith('__API call failed'): |
|
|
logger.warning(f"Returning error message to user for {sign} - {date_str}") |
|
|
return f"❌ Unable to fetch horoscope data.\n\nThe Beandev API is currently unavailable. Please check your internet connection and try again later.\n\nError details: {result[18:]}" |
|
|
|
|
|
logger.info(f"Successfully returning horoscope for {sign} - {date_str}") |
|
|
return result |
|
|
|
|
|
|
|
|
CSS_TEMPLATE = """ |
|
|
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Poppins:wght@400;500;600&display=swap'); |
|
|
|
|
|
body { |
|
|
background-color: #0f0a23; |
|
|
color: #e6e6fa; |
|
|
font-family: 'Poppins', sans-serif; |
|
|
min-height: 100vh; |
|
|
} |
|
|
|
|
|
.gradio-container { |
|
|
background-image: url('{{BACKGROUND_IMAGE}}'); |
|
|
background-size: cover; |
|
|
background-attachment: fixed; |
|
|
background-position: center; |
|
|
background-repeat: no-repeat; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.gradio-container::before { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background: rgba(15, 10, 35, 0.75); |
|
|
z-index: 0; |
|
|
pointer-events: none; |
|
|
} |
|
|
|
|
|
.gradio-container > * { |
|
|
position: relative; |
|
|
z-index: 1; |
|
|
} |
|
|
|
|
|
.gradio-container { |
|
|
max-width: 100% !important; |
|
|
width: 100% !important; |
|
|
padding: 20px !important; |
|
|
box-sizing: border-box !important; |
|
|
} |
|
|
|
|
|
.main-header { |
|
|
text-align: center; |
|
|
padding: 20px; |
|
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.25) 0%, rgba(118, 75, 162, 0.25) 100%); |
|
|
border-radius: 20px; |
|
|
margin-bottom: 20px; |
|
|
backdrop-filter: blur(15px); |
|
|
border: 1px solid rgba(150, 120, 220, 0.4); |
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); |
|
|
} |
|
|
|
|
|
.main-title { |
|
|
font-family: 'Playfair Display', serif; |
|
|
font-size: 2.5em; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
background-clip: text; |
|
|
margin: 0; |
|
|
text-shadow: 0 0 30px rgba(102, 126, 234, 0.5); |
|
|
letter-spacing: 2px; |
|
|
} |
|
|
|
|
|
.subtitle { |
|
|
color: #c8c8e0; |
|
|
font-size: 1.1em; |
|
|
margin-top: 8px; |
|
|
font-weight: 300; |
|
|
letter-spacing: 1px; |
|
|
} |
|
|
|
|
|
.input-card { |
|
|
background: rgba(20, 15, 45, 0.85); |
|
|
backdrop-filter: blur(20px); |
|
|
border-radius: 20px; |
|
|
padding: 25px; |
|
|
border: 1px solid rgba(150, 120, 220, 0.3); |
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); |
|
|
height: 100%; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.row { |
|
|
display: flex !important; |
|
|
align-items: stretch !important; |
|
|
gap: 20px !important; |
|
|
height: calc(100vh - 280px) !important; |
|
|
} |
|
|
|
|
|
.row > * { |
|
|
flex: 1 1 0 !important; |
|
|
display: flex !important; |
|
|
flex-direction: column !important; |
|
|
} |
|
|
|
|
|
.output-card { |
|
|
background: rgba(20, 15, 45, 0.85); |
|
|
backdrop-filter: blur(20px); |
|
|
border-radius: 20px; |
|
|
padding: 25px; |
|
|
border: 1px solid rgba(150, 120, 220, 0.3); |
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); |
|
|
height: 100%; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.zodiac-emoji { |
|
|
font-size: 80px; |
|
|
animation: float 3s ease-in-out infinite, rotate 20s linear infinite; |
|
|
display: inline-block; |
|
|
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.3)); |
|
|
} |
|
|
|
|
|
@keyframes float { |
|
|
0%, 100% { transform: translateY(0px) rotate(0deg); } |
|
|
50% { transform: translateY(-15px) rotate(5deg); } |
|
|
} |
|
|
|
|
|
@keyframes rotate { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
label { |
|
|
font-weight: 600 !important; |
|
|
font-size: 1.1em !important; |
|
|
color: #d0d0e8 !important; |
|
|
margin-bottom: 8px !important; |
|
|
} |
|
|
|
|
|
.input-section-title { |
|
|
color: #a8a8d0; |
|
|
font-size: 0.95em; |
|
|
margin-bottom: 15px; |
|
|
padding-bottom: 10px; |
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
button { |
|
|
margin-top: 15px !important; |
|
|
} |
|
|
|
|
|
.tips-section { |
|
|
background: rgba(102, 126, 234, 0.08); |
|
|
padding: 15px; |
|
|
border-radius: 12px; |
|
|
border-left: 3px solid #667eea; |
|
|
margin-top: 20px; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
def build_interface(): |
|
|
"""Build and configure the Gradio user interface. |
|
|
|
|
|
Creates a two-column layout with zodiac sign and time range dropdowns on the left, |
|
|
and a horoscope output textbox on the right. Applies custom CSS styling for a |
|
|
horoscope-themed appearance. |
|
|
|
|
|
Returns: |
|
|
A configured Gradio Blocks interface ready to launch. |
|
|
""" |
|
|
from datetime import datetime |
|
|
|
|
|
|
|
|
|
|
|
bg_image_data = "https://astrogyanvi.com/uploads/blog/1719644082-blog_image.png" |
|
|
logger.info("Using remote background image URL for CSS background") |
|
|
|
|
|
|
|
|
CSS = CSS_TEMPLATE.replace('{{BACKGROUND_IMAGE}}', bg_image_data) |
|
|
|
|
|
with gr.Blocks(title="🔮 Horoscope — Zodiac Insights") as demo: |
|
|
gr.HTML(f"<style>{CSS}</style>") |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class='main-header'> |
|
|
<h1 class='main-title'>🔮 Cosmic Horoscope</h1> |
|
|
<p class='subtitle'>✨ Unveil Your Celestial Destiny ✨</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Row(elem_classes="row"): |
|
|
|
|
|
with gr.Column(scale=1, min_width=400): |
|
|
gr.HTML("<div class='input-card'>") |
|
|
gr.Markdown("<div class='input-section-title'>🌟 Select Your Details</div>") |
|
|
|
|
|
sign = gr.Dropdown( |
|
|
label="♈ Zodiac Sign", |
|
|
choices=[s.capitalize() for s in ZODIAC_SIGNS], |
|
|
value='Aries', |
|
|
info="Choose your astrological sign" |
|
|
) |
|
|
|
|
|
date_picker = gr.DateTime( |
|
|
label="📅 Date", |
|
|
value=datetime.now(), |
|
|
include_time=False, |
|
|
info="Select the date for your reading" |
|
|
) |
|
|
|
|
|
btn = gr.Button('✨ Reveal My Horoscope', size='lg', variant='primary') |
|
|
|
|
|
gr.HTML(""" |
|
|
<div class='tips-section'> |
|
|
<p style='margin: 0; color: #b8b8e0; font-size: 0.95em;'> |
|
|
<strong>💫 How to use:</strong><br/> |
|
|
1. Select your zodiac sign<br/> |
|
|
2. Choose your desired date<br/> |
|
|
3. Click to reveal your cosmic forecast |
|
|
</p> |
|
|
</div> |
|
|
""") |
|
|
gr.HTML("</div>") |
|
|
|
|
|
|
|
|
with gr.Column(scale=1, min_width=500): |
|
|
output = gr.HTML( |
|
|
label='Your Horoscope', |
|
|
value="" |
|
|
) |
|
|
|
|
|
def on_get(s, d): |
|
|
s_norm = s.lower() |
|
|
|
|
|
if isinstance(d, str): |
|
|
|
|
|
date_str = d.split('T')[0] if 'T' in d else d.split(' ')[0] |
|
|
elif isinstance(d, float) or isinstance(d, int): |
|
|
|
|
|
date_str = datetime.fromtimestamp(d).strftime('%Y-%m-%d') |
|
|
elif hasattr(d, 'strftime'): |
|
|
|
|
|
date_str = d.strftime('%Y-%m-%d') |
|
|
else: |
|
|
|
|
|
date_str = datetime.now().strftime('%Y-%m-%d') |
|
|
return get_horoscope(s_norm, date_str) |
|
|
|
|
|
btn.click(on_get, inputs=[sign, date_picker], outputs=[output]) |
|
|
return demo |
|
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
app = build_interface() |
|
|
app.launch(share=True) |
|
|
|