Zodiac-AI / app.py
Klim Mikhailov
update app 403
4f31f2e
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"""<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 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"<style>{CSS}</style>")
# Header
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"):
# Left column - Input controls
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>")
# 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)