import os
import logging
import base64
import gradio as gr
import requests
# Configure logging
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
# Validate and format the date
if not date_str:
date_str = datetime.now().strftime('%Y-%m-%d')
logger.info(f"Fetching horoscope for sign={sign}, date={date_str}")
# Fetch horoscope data from Beandev API
url = "https://api.aistrology.beandev.xyz/v1"
# Send JSON and include a User-Agent to avoid trivial blocks
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 the request and provide clearer logging on failures (especially 403)
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 the API actively forbids access (403), return a helpful message
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.")
# Log unexpected non-2xx responses to aid debugging
if not response.ok:
logger.error(f"Beandev API error: status={response.status_code}, body={response.text}")
response.raise_for_status()
# API returns array of all signs, find the requested sign
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 sign emojis
zodiac_emojis = {
'aries': '♈', 'taurus': '♉', 'gemini': '♊', 'cancer': '♋',
'leo': '♌', 'virgo': '♍', 'libra': '♎', 'scorpio': '♏',
'sagittarius': '♐', 'capricorn': '♑', 'aquarius': '♒', 'pisces': '♓'
}
emoji = zodiac_emojis.get(sign.lower(), '✨')
# Format the response with HTML for better styling
formatted = f"""
{emoji} {sign.capitalize()}
{horoscope['date_range']}
📅 {horoscope['current_date']}
✨ Your Daily Horoscope
{horoscope['description']}
😊 Mood
{horoscope['mood'].capitalize()}
💖 Best Match
{horoscope['compatibility'].capitalize()}
🍀 Lucky Elements
Number
{horoscope['lucky_number']}
Time
{horoscope['lucky_time']}
Color
{horoscope['color'].capitalize()}
"""
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 the call failed, return error message
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:]}" # Strip '__API call failed: '
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
# Use a remote background image URL instead of a tracked local file
# (keeps repository small and avoids storing binaries in git)
bg_image_data = "https://astrogyanvi.com/uploads/blog/1719644082-blog_image.png"
logger.info("Using remote background image URL for CSS background")
# Generate CSS with background image
CSS = CSS_TEMPLATE.replace('{{BACKGROUND_IMAGE}}', bg_image_data)
with gr.Blocks(title="🔮 Horoscope — Zodiac Insights") as demo:
gr.HTML(f"")
# Header
gr.HTML("""
🔮 Cosmic Horoscope
✨ Unveil Your Celestial Destiny ✨
""")
with gr.Row(elem_classes="row"):
# Left column - Input controls
with gr.Column(scale=1, min_width=400):
gr.HTML("")
# Right column - Horoscope display
with gr.Column(scale=1, min_width=500):
output = gr.HTML(
label='Your Horoscope',
value=""
)
def on_get(s, d):
s_norm = s.lower()
# Convert datetime to string format YYYY-MM-DD
if isinstance(d, str):
# Handle string format
date_str = d.split('T')[0] if 'T' in d else d.split(' ')[0]
elif isinstance(d, float) or isinstance(d, int):
# Handle timestamp (Unix timestamp in seconds)
date_str = datetime.fromtimestamp(d).strftime('%Y-%m-%d')
elif hasattr(d, 'strftime'):
# Handle datetime object
date_str = d.strftime('%Y-%m-%d')
else:
# Default to today
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)