Spaces:
Running
Running
Download app.py from sisithefox/astrologybazi-api: direct link, hf CLI and curl.
- Browser
- Download file 163 kB
-
https://huggingface.co/spaces/sisithefox/astrologybazi-api/resolve/main/app.py
- Command line
-
hf download hf://spaces/sisithefox/astrologybazi-api/app.py
-
curl -L -o app.py https://huggingface.co/spaces/sisithefox/astrologybazi-api/resolve/main/app.py
163 kB
"""
🔮 AstrologyBazi API - Complete Eastern Mysticism Platform
BaZi (八字) | Nine Flying Stars (九宫飞星) | 28 Lunar Mansions (二十八星宿) | Chinese Calendar (农历)
Powering https://astrologybazi.com
🔒 Protected with API Key Authentication
"""
from birth_time_context import BirthTimeRequest
from fastapi import FastAPI, HTTPException, Header, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, field_validator
from typing import Optional, Dict, List, Any
import logging
import os
from datetime import date, datetime, timedelta
from dotenv import load_dotenv
# 🔧 Load environment variables from .env file
load_dotenv()
from bazi_calculator import BaZiCalculator
from lunar_python import Solar, Lunar
from nine_stars import NineStarsCalculator
from constellation_28 import Constellation28Calculator
from shensha_analyzer import ShenshaAnalyzer
from genju_analyzer import GenjuAnalyzer
from dayun_analyzer import DayunAnalyzer
from evidence import build_evidence, build_timing, build_meta, select_dayun_score_preview
from dayun_dynamic_assessment import build_dayun_dynamic_assessment
from ai_interpreter import AIInterpreter
from time_fortune_analyzer import TimeFortuneAnalyzer
from fortune_score_calculator import FortuneScoreCalculator
# 🎯 Phase 3 新增模块 (2025-12-24)
from marriage_analyzer import MarriageAnalyzer
from career_analyzer import CareerAnalyzer
from wealth_analyzer import WealthAnalyzer
from health_analyzer import HealthAnalyzer
from official_analyzer import OfficialAnalyzer # 官职分析
from legal_risk_analyzer import LegalRiskAnalyzer # 法律风险分析
from marriage_compatibility_analyzer import MarriageCompatibilityAnalyzer # 八字合婚
# 🌟 Rituals Service (2026-01-25)
from rituals_service import rituals_service
# 🖐️ Body Reading Analyzer (2026-02-16)
from body_reading_analyzer import analyze_body_reading
# 💕 Love Clarity Analyzer (2026-02-28)
from love_clarity_analyzer import LoveClarityAnalyzer
# 🌀 Qimen Dunjia Calculator (2026-04-14)
from qimen_calculator import QimenCalculator
# 🔮 Liu Yao Calculator (2026-04-17)
from liuyao_calculator import calculate_liuyao
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 🔐 API Key Configuration
# Support both naming conventions:
# - API_KEY: For HuggingFace Space settings (simple, since only one API key)
# - SISI_HF_API_KEY: For Cloudflare (clear distinction among many API keys)
# Fail closed: no hardcoded fallback — if neither env var is set, all
# protected endpoints refuse service instead of accepting a default key.
API_KEY = os.environ.get("API_KEY") or os.environ.get("SISI_HF_API_KEY")
if not API_KEY:
logger.error(
"🔒 API key is NOT configured (set API_KEY or SISI_HF_API_KEY). "
"All protected endpoints will return 503 until a key is set."
)
def verify_api_key(x_api_key: Optional[str] = Header(None)):
"""Verify API key from request header (fail closed when unconfigured)"""
if not API_KEY:
raise HTTPException(
status_code=503,
detail="🔒 API key is not configured on the server. Service unavailable."
)
if x_api_key != API_KEY:
raise HTTPException(
status_code=403,
detail="🔒 Invalid or missing API key. Please contact SisiTheFox for access."
)
return x_api_key
# ==================== Chinese Calendar Service ====================
class ChineseCalendarService:
"""Service for Chinese lunar calendar operations"""
# 地支到生肖的映射
ZHI_TO_ZODIAC = {
'子': '鼠', '丑': '牛', '寅': '虎', '卯': '兔',
'辰': '龙', '巳': '蛇', '午': '马', '未': '羊',
'申': '猴', '酉': '鸡', '戌': '狗', '亥': '猪'
}
# 生肖emoji映射
ZODIAC_EMOJI = {
'鼠': '🐀', '牛': '🐂', '虎': '🐅', '兔': '🐇',
'龙': '🐉', '蛇': '🐍', '马': '🐎', '羊': '🐑',
'猴': '🐒', '鸡': '🐓', '狗': '🐕', '猪': '🐖'
}
# 天神吉凶分类(传统六黄道六黑道)
# 六黄道(吉):青龙、明堂、金匮、天德、玉堂、司命
# 六黑道(凶):白虎、天刑、朱雀、天牢、玄武、勾陈
JI_SHEN_LIST = ["青龙", "明堂", "金匮", "天德", "玉堂", "司命"]
XIONG_SHEN_LIST = ["天刑", "白虎", "朱雀", "天牢", "玄武", "勾陈"]
@staticmethod
def get_today_calendar() -> Dict:
"""Get today's Chinese lunar calendar information"""
today = datetime.now().date()
return ChineseCalendarService.get_calendar_by_date(today)
@staticmethod
def get_calendar_by_date(target_date: date) -> Dict:
"""Get Chinese lunar calendar information for a specific date"""
try:
solar = Solar.fromYmd(target_date.year, target_date.month, target_date.day)
lunar = solar.getLunar()
lunar_year = lunar.getYear()
lunar_month = lunar.getMonth()
lunar_day = lunar.getDay()
lunar_year_cn = lunar.getYearInChinese()
lunar_month_cn = lunar.getMonthInChinese()
lunar_day_cn = lunar.getDayInChinese()
year_ganzhi = lunar.getYearInGanZhi()
month_ganzhi = lunar.getMonthInGanZhi()
day_ganzhi = lunar.getDayInGanZhi()
zodiac = lunar.getYearShengXiao()
weekday_num = target_date.weekday()
weekdays_cn = ['一', '二', '三', '四', '五', '六', '日']
weekday_cn = f"星期{weekdays_cn[weekday_num]}"
almanac = ChineseCalendarService._get_almanac_for_date(lunar)
auspicious_hours = ChineseCalendarService._get_auspicious_hours(lunar)
solar_term = ChineseCalendarService._get_solar_term(lunar)
festival = ChineseCalendarService._get_festival(target_date, lunar)
return {
"success": True,
"date": str(target_date),
"weekday": weekday_cn,
"solar": {
"year": target_date.year,
"month": target_date.month,
"day": target_date.day,
"date_string": f"{target_date.year}年{target_date.month}月{target_date.day}日"
},
"lunar": {
"year": lunar_year,
"month": lunar_month,
"day": lunar_day,
"year_cn": lunar_year_cn,
"month_cn": lunar_month_cn,
"day_cn": lunar_day_cn,
"date_string": f"{lunar_year_cn}{lunar_month_cn}{lunar_day_cn}",
"is_leap": lunar.getMonth() < 0
},
"ganzhi": {
"year": year_ganzhi,
"month": month_ganzhi,
"day": day_ganzhi,
"full": f"{year_ganzhi}年 {month_ganzhi}月 {day_ganzhi}日"
},
"zodiac": {
"animal": zodiac,
"animal_cn": zodiac
},
"almanac": almanac,
"auspicious_hours": auspicious_hours,
"solar_term": solar_term,
"festival": festival,
"timestamp": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"Error getting calendar for {target_date}: {str(e)}")
return {
"success": False,
"error": str(e),
"date": str(target_date)
}
@staticmethod
def get_calendar_range(start_date: date, end_date: date) -> List[Dict]:
"""Get calendar information for a date range"""
if (end_date - start_date).days > 365:
raise ValueError("Date range cannot exceed 365 days")
calendars = []
current_date = start_date
while current_date <= end_date:
calendar = ChineseCalendarService.get_calendar_by_date(current_date)
calendars.append(calendar)
current_date += timedelta(days=1)
return calendars
@staticmethod
def _get_almanac_for_date(lunar: Lunar) -> Dict:
"""Get authentic almanac data (宜忌) from lunar-python"""
try:
# 获取真实的宜忌
yi = lunar.getDayYi()
ji = lunar.getDayJi()
# 获取冲煞信息
chong_gan = lunar.getChongGan()
chong_zhi = lunar.getChong()
sha = lunar.getSha()
# 转换冲的地支为生肖
chong_zodiac = ChineseCalendarService.ZHI_TO_ZODIAC.get(chong_zhi, chong_zhi)
chong_emoji = ChineseCalendarService.ZODIAC_EMOJI.get(chong_zodiac, '')
# 组合完整的冲煞格式:"冲🐑羊(丁未)煞东"
chong_ganzhi = chong_gan + chong_zhi
chong_sha_full = f"冲{chong_emoji}{chong_zodiac}({chong_ganzhi})煞{sha}"
# 获取吉神宜趋和凶煞宜忌
ji_shen = lunar.getDayJiShen()
xiong_sha = lunar.getDayXiongSha()
# 获取彭祖百忌
pengzu_gan = lunar.getPengZuGan()
pengzu_zhi = lunar.getPengZuZhi()
return {
"appropriate": yi,
"avoid": ji,
"chong": chong_zhi, # 保留原始地支供兼容
"sha": sha, # 保留原始煞向供兼容
"chong_sha": chong_sha_full, # 新增:完整格式"冲🐑羊(丁未)煞东"
"ji_shen": ji_shen, # 吉神宜趋
"xiong_sha": xiong_sha, # 凶煞宜忌
"pengzu": {
"gan": pengzu_gan,
"zhi": pengzu_zhi
}
}
except Exception as e:
logger.error(f"Error getting almanac data: {e}")
# 返回空数据而不是报错
return {
"appropriate": [],
"avoid": [],
"chong": "",
"sha": "",
"chong_sha": "",
"ji_shen": [],
"xiong_sha": [],
"pengzu": {"gan": "", "zhi": ""}
}
@staticmethod
def _get_auspicious_hours(lunar: Lunar) -> List[Dict]:
"""Get 12-hour fortune analysis (十二时辰吉凶)"""
try:
times = lunar.getTimes()
hours = []
# 只取前12个时辰(lunar-python可能返回13个,包含下一天的子时)
for i, t in enumerate(times[:12]):
time_ganzhi = t.getGanZhi()
tian_shen = t.getTianShen()
chong = t.getChong()
sha = t.getSha()
# 判断吉凶(六黄道为吉,六黑道为凶)
if tian_shen in ChineseCalendarService.JI_SHEN_LIST:
fortune = "吉"
elif tian_shen in ChineseCalendarService.XIONG_SHEN_LIST:
fortune = "凶"
else:
fortune = "凶" # 未知天神默认为凶
# 时间段
start_hour = i * 2
end_hour = i * 2 + 1
period = f"{start_hour:02d}:00-{end_hour:02d}:59"
hours.append({
"time": f"{time_ganzhi}时",
"period": period,
"ganzhi": time_ganzhi,
"tian_shen": tian_shen,
"fortune": fortune,
"chong": chong,
"sha": sha
})
return hours
except Exception as e:
logger.error(f"Error getting auspicious hours: {e}")
return []
@staticmethod
def _get_solar_term(lunar: Lunar) -> Optional[str]:
"""Get solar term (节气)"""
try:
jieqi = lunar.getJieQi()
return jieqi if jieqi else None
except:
return None
@staticmethod
def _get_festival(target_date: date, lunar: Lunar) -> Optional[str]:
"""Get festival name"""
try:
solar_festivals = lunar.getFestivals()
if solar_festivals:
return ", ".join(solar_festivals)
lunar_festivals = lunar.getOtherFestivals()
if lunar_festivals:
return ", ".join(lunar_festivals)
return None
except:
return None
app = FastAPI(
title="AstrologyBazi API",
version="2.1.1",
description="🔮 Complete Eastern Mysticism API - BaZi Fortune Analysis (Phases 1-5), Nine Flying Stars, 28 Lunar Mansions, Chinese Calendar"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class BaZiRequest(BirthTimeRequest):
"""Request model for BaZi calculation"""
birth_date: str = Field(
...,
description="Birth date in YYYY-MM-DD format (e.g., '1990-01-15')",
example="1990-01-15"
)
birth_time: str = Field(
...,
description="Birth time in HH:MM format (e.g., '08:30')",
example="08:30"
)
gender: str = Field(
default="male",
description="Gender: 'male' or 'female'",
example="male"
)
is_lunar: bool = Field(
default=False,
description="True if birth_date is lunar calendar, False for solar/Gregorian",
example=False
)
class BaZiCompleteRequest(BirthTimeRequest):
"""Request model for complete BaZi calculation with 大运/流年"""
birth_date: str = Field(
...,
description="Birth date in YYYY-MM-DD format",
example="1990-01-15"
)
birth_time: str = Field(
...,
description="Birth time in HH:MM format",
example="08:30"
)
gender: str = Field(
default="male",
description="Gender: 'male' or 'female'",
example="male"
)
is_lunar: bool = Field(
default=False,
description="True if lunar calendar",
example=False
)
num_dayun: int = Field(
default=12,
description="Number of 大运 periods (each 10 years, default 12 = 120 years)",
example=12
)
class BaZiInterpretationRequest(BirthTimeRequest):
"""Request model for AI interpretation of BaZi"""
birth_date: str = Field(
...,
description="Birth date in YYYY-MM-DD format",
example="1990-01-15"
)
birth_time: str = Field(
...,
description="Birth time in HH:MM format",
example="08:30"
)
gender: str = Field(
default="male",
description="Gender: 'male' or 'female'",
example="male"
)
is_lunar: bool = Field(
default=False,
description="True if lunar calendar",
example=False
)
interpretation_level: str = Field(
default="basic",
description="Interpretation depth: 'basic' (3000 words, $29), 'professional' (8000 words, $99), 'master' (15000 words, $199)",
example="basic"
)
language: str = Field(
default="zh-CN",
description="Language: 'zh-CN', 'en', 'zh-TW'",
example="zh-CN"
)
num_dayun: int = Field(
default=12,
description="Number of 大运 periods for complete analysis",
example=12
)
class MonthlyFortuneRequest(BaseModel):
"""Request model for monthly fortune analysis"""
birth_date: str = Field(..., description="Birth date in YYYY-MM-DD format", example="1990-01-15")
birth_time: str = Field(..., description="Birth time in HH:MM format", example="08:30")
gender: str = Field(default="male", description="Gender: 'male' or 'female'", example="male")
is_lunar: bool = Field(default=False, description="True if lunar calendar", example=False)
target_year: int = Field(..., description="Target year", example=2026)
target_month: int = Field(..., description="Target month (1-12)", example=1, ge=1, le=12)
language: str = Field(
default="zh",
description="Output language: 'zh' for Chinese, 'en' for cold-reading English",
example="zh"
)
class YearlyFortuneRequest(BaseModel):
"""Request model for yearly fortune analysis (all 12 months)"""
birth_date: str = Field(..., description="Birth date in YYYY-MM-DD format", example="1990-01-15")
birth_time: str = Field(..., description="Birth time in HH:MM format", example="08:30")
gender: str = Field(default="male", description="Gender: 'male' or 'female'", example="male")
is_lunar: bool = Field(default=False, description="True if lunar calendar", example=False)
target_year: int = Field(default=None, description="Target year (defaults to current year)", example=2026)
language: str = Field(default="zh", description="Output language: 'zh' or 'en'", example="zh")
class DailyFortuneRequest(BaseModel):
"""Request model for daily fortune analysis"""
birth_date: str = Field(..., description="Birth date in YYYY-MM-DD format", example="1990-01-15")
birth_time: str = Field(..., description="Birth time in HH:MM format", example="08:30")
gender: str = Field(default="male", description="Gender: 'male' or 'female'", example="male")
is_lunar: bool = Field(default=False, description="True if lunar calendar", example=False)
target_date: str = Field(..., description="Target date in YYYY-MM-DD format", example="2026-01-01")
activities: List[str] = Field(default=[], description="Activities to analyze (e.g., ['嫁娶', '开业'])", example=["嫁娶"])
class CareerMonthlyFortuneRequest(BaseModel):
"""Request model for career-specific monthly fortune analysis"""
birth_date: str = Field(..., description="Birth date in YYYY-MM-DD format", example="1990-01-15")
birth_time: str = Field(..., description="Birth time in HH:MM format", example="08:30")
gender: str = Field(default="male", description="Gender: 'male' or 'female'", example="male")
is_lunar: bool = Field(default=False, description="True if lunar calendar", example=False)
target_year: int = Field(..., description="Target year to analyze (e.g., 2026)", example=2026)
class FindAuspiciousDatesRequest(BaseModel):
"""Request model for finding auspicious dates"""
birth_date: str = Field(..., description="Birth date in YYYY-MM-DD format", example="1990-01-15")
birth_time: str = Field(..., description="Birth time in HH:MM format", example="08:30")
gender: str = Field(default="male", description="Gender: 'male' or 'female'", example="male")
is_lunar: bool = Field(default=False, description="True if lunar calendar", example=False)
start_date: str = Field(..., description="Start date in YYYY-MM-DD format", example="2026-01-01")
end_date: str = Field(..., description="End date in YYYY-MM-DD format", example="2026-01-31")
activity: str = Field(..., description="Activity type", example="嫁娶")
min_score: int = Field(default=70, description="Minimum score requirement (0-100)", example=70, ge=0, le=100)
def _get_ordinal(n: int) -> str:
"""Convert number to ordinal string (1 -> 1st, 2 -> 2nd, etc.)"""
if 10 <= n % 100 <= 20:
suffix = 'th'
else:
suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
return f"{n}{suffix}"
def apply_cold_reading_english(monthly_fortune: Dict[str, Any], year: int, month: int) -> Dict[str, Any]:
"""
Transform Chinese fortune data into Barnum Effect English
NOT simple translation - apply psychological triggers
Core Principles:
- Specific numbers: "3 days", "the 15th", "2 weeks"
- Sensory details: "that sinking feeling", "you've noticed"
- Emotional triggers: "frustrated", "hopeful", "relieved"
- Universal specifics: "the conversation you've been avoiding"
- Time anchors: "Next Tuesday", "before the month ends"
Args:
monthly_fortune: Chinese fortune analysis data
year: Target year
month: Target month
Returns:
Cold-reading English version with Barnum Effect triggers
"""
import random
score = monthly_fortune['score']
quality_cn = monthly_fortune['quality_cn']
# Map Chinese quality to cold-reading English with Barnum triggers
quality_map = {
"大吉": "Peak Performance Window",
"顺遂": "Favorable Momentum",
"平稳": "Strategic Recalibration",
"波折": "Protective Defense Phase",
"困难": "Strategic Pause Period"
}
quality_en = quality_map.get(quality_cn, "Transition Phase")
# Generate specific peak days based on score patterns
if score >= 80:
peak_days = [3, 8, 15, 22]
caution_days = [18, 27]
elif score >= 65:
peak_days = [5, 12, 19, 24]
caution_days = [9, 21]
elif score >= 50:
peak_days = [7, 14, 21]
caution_days = [4, 11, 25]
else:
peak_days = [6, 16]
caution_days = [3, 10, 20, 28]
# Cold-reading matters based on score range
matters_templates = []
if score >= 80: # 大吉
matters_templates = [
f"That work project you've been overthinking? Your instincts were right. Start it before the {peak_days[0]}th.",
f"You'll see an unexpected opportunity around the {peak_days[1]}th. The one that feels 'too good to be true'? Trust it this time.",
f"Someone's been watching your work quietly. Around the {peak_days[2]}th, they speak up. It's the validation you needed.",
f"That financial decision you've been postponing? The window opens between the {peak_days[0]}th and {peak_days[2]}th. After that, conditions shift."
]
elif score >= 65: # 顺遂
matters_templates = [
f"The progress you've been making feels slow, but others notice. Around the {peak_days[0]}th, you'll see proof.",
f"That side project or idea you keep dismissing? Between the {peak_days[1]}th and {peak_days[2]}th, test it quietly.",
f"A relationship (work or personal) that's felt stuck will shift around the {peak_days[2]}th. Stay open.",
f"Financial stability improves, but not dramatically. The {peak_days[0]}th brings a small win that matters more than you think."
]
elif score >= 50: # 平稳
matters_templates = [
f"This month rewards patience over action. The urge to force things around the {caution_days[0]}th? Resist it.",
f"Something you started weeks ago finally stabilizes around the {peak_days[1]}th. Not exciting, but necessary.",
f"That person who's been distant lately? Around the {peak_days[0]}th, you'll understand why. Don't take it personally.",
f"Financially, hold steady. The temptation to spend around the {caution_days[1]}th looks harmless but isn't."
]
elif score >= 35: # 波折
matters_templates = [
f"That situation that keeps you up at 3am? Around the {caution_days[0]}th, it gets worse before it gets better.",
f"Someone will test your boundaries between the {caution_days[1]}th and {caution_days[2]}th. This is when you draw the line.",
f"A plan you've been counting on might delay around the {caution_days[0]}th. Have a backup ready before the {peak_days[0]}th.",
f"Financially, protect what you have. The 'opportunity' around the {caution_days[1]}th is actually a trap."
]
else: # 困难
matters_templates = [
f"This month is about defense, not offense. Between the {caution_days[0]}th and {caution_days[2]}th, just hold ground.",
f"That relationship that's been rocky? Around the {caution_days[1]}th, you'll need to decide: fix it or walk away.",
f"The financial pressure you're feeling peaks around the {caution_days[0]}th. Don't make desperate moves - it passes by the {peak_days[0]}th.",
f"Someone will offer a solution that sounds perfect around the {caution_days[2]}th. It's not. Wait until next month."
]
# Select appropriate number of matters based on score
num_matters = 3 if score >= 65 else 2
matters_en = random.sample(matters_templates, num_matters)
# Cold-reading advice with emotional triggers
if score >= 80:
advice_en = f"You know that idea you dismissed last week as 'not the right time'? THIS month IS the time. But don't broadcast it - skeptics drain energy you'll need. Start quietly between the {peak_days[0]}th and {peak_days[1]}th, refine as you go. The urge to perfect before starting? That's fear talking. Ignore it."
warning_en = f"Around the {caution_days[0]}th-{caution_days[1]}th: someone offers advice that sounds wise but feels off in your gut. Trust the feeling, not their credentials."
elif score >= 65:
advice_en = f"This month rewards steady progress over big moves. That thing you've been doing consistently? It compounds between the {peak_days[0]}th and {peak_days[2]}th. The results won't be dramatic, but they're building to something significant next month. Stay the course."
warning_en = f"The {peak_days[3]}th feels urgent but isn't. What feels like a deadline is actually just anxiety. Take your time."
elif score >= 50:
advice_en = f"Hold your position this month. The itch to make changes around the {caution_days[0]}th is restlessness, not intuition. Use this time to observe, not act. Around the {peak_days[1]}th, you'll see why waiting was right. Trust the pause."
warning_en = f"Someone pushes you to decide quickly around the {caution_days[1]}th. Anyone rushing you doesn't have your best interests. Delay until you're sure."
elif score >= 35:
advice_en = f"This is a testing month. Between the {caution_days[0]}th and {caution_days[2]}th, protect your energy like it's money. Say no more than yes. The thing that looks like a failure around the {caution_days[1]}th? It's actually filtering out what wasn't meant for you. Trust that."
warning_en = f"Around the {caution_days[1]}th-{caution_days[2]}th: someone will gaslight you about a decision you made. You weren't wrong. They're projecting their regret."
else:
advice_en = f"This month is about survival, not success. Between the {caution_days[0]}th and {caution_days[2]}th, just keep breathing. The pressure you're feeling peaks around the {caution_days[1]}th - and then it breaks. Don't make permanent decisions in temporary darkness. The {peak_days[0]}th brings the first sign of relief."
warning_en = f"Someone offers a quick fix around the {caution_days[2]}th. It will create more problems than it solves. Decline politely, weather the storm alone."
# Format peak days string
peak_days_str = ", ".join([_get_ordinal(d) for d in peak_days[:3]])
if len(peak_days) > 3:
peak_days_str += f" (high energy)"
caution_days_str = ", ".join([_get_ordinal(d) for d in caution_days[:2]])
return {
'year': year,
'month': month,
'score': score,
'quality': quality_en,
'matters': matters_en,
'advice': advice_en,
'peak_days': peak_days_str,
'caution_days': caution_days_str,
'warning': warning_en,
'overall_energy': f"This month's energy is {quality_en}. Trust what you feel in your body, not what looks logical on paper."
}
@app.get("/")
def root():
"""Root endpoint"""
return {
"status": "online",
"service": "AstrologyBazi API",
"version": "2.1.1",
"documentation": "/docs",
"website": "https://astrologybazi.com"
}
@app.get("/health")
@app.head("/health")
def health_check():
"""Health check endpoint for monitoring services"""
return {
"status": "healthy",
"service": "AstrologyBazi API",
"version": "2.1.1",
"uptime": "ok"
}
@app.post("/calculate-bazi")
@app.post("/api/calculate-bazi") # Alternative endpoint
def calculate_bazi(req: BaZiRequest, api_key: str = Depends(verify_api_key)):
"""
Calculate BaZi (八字) chart from birth information
🔒 Requires API Key authentication via x-api-key header
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format (e.g., '1990-01-15')
- `birth_time`: Birth time in HH:MM format (e.g., '08:30')
- `gender`: 'male' or 'female' (default: 'male')
- `is_lunar`: True for lunar calendar, False for solar/Gregorian (default: False)
**Response:**
- `bazi_chart`: Four Pillars (Year, Month, Day, Hour) with Gan/Zhi
- `day_master`: Day stem (日主) - the most important element
- `five_elements`: Distribution of Wood, Fire, Earth, Metal, Water
- `ten_gods`: Ten Gods (十神) relationships
- `hidden_stems`: Hidden stems (藏干) in Earthly Branches
**Example:**
```json
{
"birth_date": "1990-01-15",
"birth_time": "08:30",
"gender": "male",
"is_lunar": false
}
```
"""
try:
logger.info(f"🔮 Calculating BaZi for: {req.birth_date} {req.birth_time}")
# Validate date format
if len(req.birth_date.split('-')) != 3:
raise HTTPException(
status_code=400,
detail="Invalid birth_date format. Use YYYY-MM-DD (e.g., '1990-01-15')"
)
# Validate time format
if len(req.birth_time.split(':')) != 2:
raise HTTPException(
status_code=400,
detail="Invalid birth_time format. Use HH:MM (e.g., '08:30')"
)
# Validate gender
if req.gender not in ['male', 'female']:
raise HTTPException(
status_code=400,
detail="Invalid gender. Use 'male' or 'female'"
)
# Calculate BaZi
result = BaZiCalculator.calculate_bazi(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
**req.time_options()
)
logger.info(f"✅ BaZi calculation successful")
return {
"success": True,
"data": result
}
except ValueError as e:
logger.error(f"❌ Validation Error: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Calculation Error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"BaZi calculation failed: {str(e)}")
@app.post("/calculate-bazi-simple")
def calculate_bazi_simple(req: BaZiRequest, api_key: str = Depends(verify_api_key)):
"""
Simplified BaZi calculation - returns only essential data
🔒 Requires API Key authentication via x-api-key header
Perfect for quick lookups and mobile apps.
Returns only: Four Pillars, Day Master, Five Elements
"""
try:
logger.info(f"🔮 Simple BaZi for: {req.birth_date} {req.birth_time}")
result = BaZiCalculator.calculate_bazi(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
**req.time_options()
)
# Return simplified response
simplified = {
"bazi_chart": result['bazi_chart'],
"day_master": result['day_master'],
"five_elements": result['five_elements'],
"time_context": result['time_context']
}
logger.info(f"✅ Simple BaZi calculation successful")
return {
"success": True,
"data": simplified
}
except ValueError as e:
logger.error(f"❌ Validation Error: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Calculation Error: {e}")
raise HTTPException(status_code=500, detail=f"BaZi calculation failed: {str(e)}")
@app.post("/calculate-bazi-complete")
@app.post("/api/calculate-bazi-complete") # Alternative endpoint
def calculate_bazi_complete(req: BaZiCompleteRequest, api_key: str = Depends(verify_api_key)):
"""
Calculate complete BaZi with 大运 (Major Cycles) and 流年 (Yearly Cycles)
🔒 Requires API Key authentication via x-api-key header
**Premium Feature** - Returns comprehensive 120-year fortune analysis:
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format (e.g., '1990-01-15')
- `birth_time`: Birth time in HH:MM format (e.g., '08:30')
- `gender`: 'male' or 'female' (default: 'male')
- `is_lunar`: True for lunar calendar, False for solar/Gregorian (default: False)
- `num_dayun`: Number of 大运 periods (default: 12, each 10 years = 120 years total)
**Response:**
- All basic BaZi data (四柱八字, 日主, 五行, 十神, 藏干)
- `dayun`: List of major cycles, each containing:
- `start_age`, `end_age`: Age range for this cycle
- `ganzhi`: Heavenly Stem + Earthly Branch (干支)
- `elements`: Five Elements distribution
- `ten_gods`: Ten Gods relationships
- `nayin`: 60 Jia Zi element (纳音)
- `shens`: Spiritual forces (神煞) active in this period
- `liunian`: 10 yearly cycles within this major cycle
- `yun_direction`: 'forward' (顺) or 'backward' (逆)
- `yun_start_age`: Age when cycles begin
**Perfect for:**
- 10-year fortune predictions
- Career timing analysis
- Wealth opportunity windows
- Investment decision support
- Life path planning
**Example:**
```json
{
"birth_date": "1990-01-15",
"birth_time": "08:30",
"gender": "male",
"is_lunar": false,
"num_dayun": 12
}
```
"""
try:
logger.info(f"🔮 Complete BaZi calculation for: {req.birth_date} {req.birth_time}")
# Validate date format
if len(req.birth_date.split('-')) != 3:
raise HTTPException(
status_code=400,
detail="Invalid birth_date format. Use YYYY-MM-DD (e.g., '1990-01-15')"
)
# Validate time format
if len(req.birth_time.split(':')) != 2:
raise HTTPException(
status_code=400,
detail="Invalid birth_time format. Use HH:MM (e.g., '08:30')"
)
# Validate gender
if req.gender not in ['male', 'female']:
raise HTTPException(
status_code=400,
detail="Invalid gender. Use 'male' or 'female'"
)
# Calculate complete BaZi with dayun/liunian
result = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=req.num_dayun,
**req.time_options()
)
# 2026-07-12 Phase 3: 追加 meta/strength/timing/evidence 四层字段,旧字段一个不改。
# 格局分析(genju)在这里只用来丰富evidence,失败不影响这个endpoint本身能不能用——
# 这是新加的能力,不能让它的问题拖垮已经在跑的老功能。
current_age = None
try:
birth_year = int(req.birth_date.split('-')[0])
current_age = datetime.now().year - birth_year
except (ValueError, IndexError):
pass
if current_age is not None:
for d in result.get('dayun', []):
d['is_current'] = d.get('start_age', 0) <= current_age <= d.get('end_age', -1)
genju_analysis = None
try:
genju_analysis = GenjuAnalyzer(result).analyze_genju()
except Exception as e:
logger.warning(f"⚠️ Genju analysis skipped for evidence block (non-fatal): {e}")
result['meta'] = build_meta()
result['strength'] = result.get('wuxing_strength')
result['timing'] = build_timing(
result, result['bazi_chart']['day']['zhi'], result['bazi_chart']['month']['zhi']
)
result['evidence'] = build_evidence(result, genju_analysis, current_age)
if genju_analysis:
result['genju_analysis'] = genju_analysis
# Deterministic, redacted teaser for the free trend chart. The
# selector prefers the broad support-aware trend_score; the full
# structure score and reasons/events/advice remain exclusive to the
# paid endpoint. No LLM call is involved here.
try:
preview_analysis = DayunAnalyzer(
result,
genju_analysis,
current_age=current_age,
).analyze_all_dayun()
result['dayun_score_preview'] = select_dayun_score_preview(
result.get('dayun', []),
preview_analysis.get('dayun_analysis', []),
)
except Exception as e:
logger.warning(f"⚠️ Dayun score preview skipped (non-fatal): {e}")
# New free, evidence-first assessment. It deliberately has no total
# score and remains available even when optional Genju enrichment fails.
try:
result['dayun_dynamics'] = build_dayun_dynamic_assessment(
result,
genju_analysis,
current_age=current_age,
)
except Exception as e:
logger.warning(f"⚠️ Dayun dynamic assessment skipped (non-fatal): {e}")
logger.info(f"✅ Complete BaZi calculation successful with {req.num_dayun} dayun periods")
return {
"success": True,
"data": result
}
except ValueError as e:
logger.error(f"❌ Validation Error: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Calculation Error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Complete BaZi calculation failed: {str(e)}")
# ==================== Shensha Analysis API (神煞分析) ====================
@app.post("/analyze-shensha")
@app.post("/api/analyze-shensha") # Alternative endpoint
def analyze_shensha(req: BaZiRequest, api_key: str = Depends(verify_api_key)):
"""
🔮 Analyze Spiritual Forces (神煞分析) - Advanced Feature
🔒 Requires API Key authentication via x-api-key header
**Premium Feature** - Comprehensive analysis of 47 spiritual forces:
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format (e.g., '1990-01-15')
- `birth_time`: Birth time in HH:MM format (e.g., '08:30')
- `gender`: 'male' or 'female' (default: 'male')
- `is_lunar`: True for lunar calendar, False for solar/Gregorian (default: False)
**Response:**
- `bazi_chart`: Basic BaZi chart
- `shensha_analysis`: Complete spiritual forces analysis:
- `auspicious_gods`: List of beneficial spiritual forces (吉神)
- Tianyi Nobleman (天乙贵人) - Most auspicious
- Red Phoenix (红鸾) - Marriage fortune
- Heavenly Joy (天喜) - Joyful events
- Wealth God (禄神) - Financial fortune
- Fortune Star (福星) - General luck
- Golden Chariot (金舆) - Wealth & status
- And more... (10 total)
- `inauspicious_gods`: List of challenging forces (凶煞)
- Sheep Blade (羊刃) - Strong character
- Emptiness (空亡) - Void & loss
- Disaster God (灾煞) - Accidents
- White Tiger (白虎) - Bloodshed
- Loneliness (孤辰) - Solitude
- Widowhood (寡宿) - Isolation
- And more... (15+ total)
- `peach_blossom_system`: Detailed romance analysis
- Inner peach blossom (墙内桃花) - Harmonious marriage
- Outer peach blossom (墙外桃花) - Affairs risk
- `traveling_horse_system`: Movement & career analysis
- Wealth horse (禄马同乡) - Fortune through travel
- Broken foot horse (折足马) - Travel obstacles
- And more... (12 types total)
- `overall_judgment`: Comprehensive fortune rating
- Auspicious/Inauspicious scores
- Fortune level (大吉/中吉/平/不利)
- Remedy suggestions
**Example:**
```json
{
"birth_date": "1990-01-15",
"birth_time": "08:30",
"gender": "male",
"is_lunar": false
}
```
**Response Example:**
```json
{
"success": true,
"data": {
"bazi_chart": {...},
"shensha_analysis": {
"auspicious_gods": [
{
"name": "天乙贵人",
"name_en": "Tianyi Nobleman",
"position": "year",
"strength": 10,
"level": 10,
"effect": "逢凶化吉、遇难呈祥、贵人相助",
"description": "命中最吉之神,主一生多得贵人扶持..."
}
],
"inauspicious_gods": [...],
"peach_blossom_system": {
"has_peach_blossom": true,
"count": 1,
"details": [...]
},
"traveling_horse_system": {...},
"overall_judgment": {
"overall_judgment": "moderately_auspicious",
"overall_judgment_cn": "中吉",
"auspicious_score": 85.2,
"inauspicious_score": 32.5,
"message": "吉多凶少,总体平顺,稳中向好"
}
}
}
}
```
**Perfect for:**
- Fortune telling services ($49-$99)
- Detailed life analysis
- Marriage compatibility
- Career guidance
- Timing decisions (when to start business, get married, etc.)
"""
try:
logger.info(f"🔮 Analyzing Shenshas for: {req.birth_date} {req.birth_time}")
# Validate inputs
if len(req.birth_date.split('-')) != 3:
raise HTTPException(
status_code=400,
detail="Invalid birth_date format. Use YYYY-MM-DD (e.g., '1990-01-15')"
)
if len(req.birth_time.split(':')) != 2:
raise HTTPException(
status_code=400,
detail="Invalid birth_time format. Use HH:MM (e.g., '08:30')"
)
if req.gender not in ['male', 'female']:
raise HTTPException(
status_code=400,
detail="Invalid gender. Use 'male' or 'female'"
)
# Step 1: Calculate basic BaZi chart
bazi_result = BaZiCalculator.calculate_bazi(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
**req.time_options()
)
# Step 2: Analyze spiritual forces
analyzer = ShenshaAnalyzer(bazi_result)
shensha_analysis = analyzer.analyze_all_shenshas()
logger.info(f"✅ Shensha analysis successful: {shensha_analysis['overall_judgment']['overall_judgment_cn']}")
return {
"success": True,
"data": {
"bazi_chart": bazi_result,
"shensha_analysis": shensha_analysis
}
}
except ValueError as e:
logger.error(f"❌ Validation Error: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Shensha Analysis Error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Shensha analysis failed: {str(e)}")
# ==================== Genju Pattern Analysis API (格局分析) ====================
@app.post("/analyze-genju")
@app.post("/api/analyze-genju") # Alternative endpoint
def analyze_genju(req: BaZiRequest, api_key: str = Depends(verify_api_key)):
"""
🎯 Analyze Life Pattern (格局分析) - Core Premium Feature
🔒 Requires API Key authentication via x-api-key header
**Premium Feature** - Comprehensive analysis of life patterns and destiny structure:
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format (e.g., '1990-01-15')
- `birth_time`: Birth time in HH:MM format (e.g., '08:30')
- `gender`: 'male' or 'female' (default: 'male')
- `is_lunar`: True for lunar calendar, False for solar/Gregorian (default: False)
**Response:**
- `bazi_chart`: Basic BaZi chart
- `genju_analysis`: Complete pattern analysis:
- `primary_pattern`: Main life pattern (格局)
- **Six Proper Patterns (六大正格)**:
- 正官格 (Proper Officer) - Government, authority
- 七杀格 (Seven Killings) - Military, leadership
- 正财格 (Proper Wealth) - Business, stable income
- 偏财格 (Indirect Wealth) - Investment, windfall
- 正印格 (Proper Seal) - Education, wisdom
- 偏印格 (Indirect Seal) - Arts, metaphysics
- **Special Patterns (特殊格局)**:
- 从财格/从杀格/从儿格 (Following patterns) - Resource-following structure; validate carrying capacity by phase
- 化气格 (Transformation) - Rare structural configuration; validate with current timing
- 专旺格 (Specialized prosperity) - Single element dominance
- `yongshen_analysis`: **Yongshen (用神) - MOST CRITICAL CONCEPT**
- 用神: The key element needed for balance
- 喜神: Supporting elements
- 忌神: Harmful elements
- 仇神: Enemy elements
- 闲神: Neutral elements
- `pattern_level`: Pattern quality and current-stage capacity reading
- level/score: structural strength indicators, not a lifetime wealth grade
- description: stage-aware explanation tied to the chart structure
- nobility: traditional pattern signal kept separate from financial claims
- `career_suggestions`: Career recommendations by pattern
- Primary careers (most suitable)
- Secondary careers (acceptable)
- Careers to avoid
- `wealth_analysis`: Current wealth-channel and carrying-capacity analysis
- Wealth star and pattern signals
- Current-cycle wealth opportunities and risks
- Actionable cash-flow and support checks
- `marriage_analysis`: Marriage quality analysis
- Marriage quality (excellent/good/moderate/challenging)
- Spouse characteristics
- Marriage timing (early/normal/late)
**Example:**
```json
{
"birth_date": "1990-01-15",
"birth_time": "08:30",
"gender": "male",
"is_lunar": false
}
```
**Response Example:**
```json
{
"success": true,
"data": {
"bazi_chart": {...},
"genju_analysis": {
"primary_pattern": {
"pattern_name": "正官格",
"pattern_type": "proper",
"is_established": true,
"strength": 85,
"description": "月令正官透干,为官贵之命...",
"favorable_elements": ["正印", "食神", "财星"],
"unfavorable_elements": ["七杀", "伤官"]
},
"yongshen_analysis": {
"用神": "正印",
"喜神": ["比劫", "正印"],
"忌神": ["伤官", "七杀"],
"仇神": ["偏财"],
"闲神": ["食神"],
"method": "扶抑法",
"explanation": "日主偏弱,需印比帮扶..."
},
"pattern_level": {
"level": "上等",
"score": 85,
"nobility": "high",
"explanation": "格局清纯,用神有力,当前阶段承载力较稳"
},
"career_suggestions": {
"primary": ["公务员", "政府官员", "企业管理层"],
"secondary": ["教师", "军官", "警官"],
"avoid": ["自由职业", "销售", "投资"],
"best_industry": "政府机构、国企、教育系统"
},
"wealth_analysis": {
"wealth_potential": "high",
"wealth_source": "salary",
"best_period": "35-55岁",
"investment_advice": "保守稳健型投资为主"
},
"marriage_analysis": {
"quality": "stable",
"spouse_characteristics": "贤惠、稳重、有责任心",
"marriage_timing": "normal",
"compatibility_advice": "适合门当户对,重视家庭"
}
}
}
}
```
**Perfect for:**
- Fortune telling services ($99-$299)
- Career consulting ($199-$499)
- Life planning guidance ($299-$999)
- Investment timing analysis
- Marriage compatibility consulting
"""
try:
logger.info(f"🎯 Analyzing Genju Pattern for: {req.birth_date} {req.birth_time}")
# Validate inputs
if len(req.birth_date.split('-')) != 3:
raise HTTPException(
status_code=400,
detail="Invalid birth_date format. Use YYYY-MM-DD (e.g., '1990-01-15')"
)
if len(req.birth_time.split(':')) != 2:
raise HTTPException(
status_code=400,
detail="Invalid birth_time format. Use HH:MM (e.g., '08:30')"
)
if req.gender not in ['male', 'female']:
raise HTTPException(
status_code=400,
detail="Invalid gender. Use 'male' or 'female'"
)
# Step 1: Calculate basic BaZi chart
bazi_result = BaZiCalculator.calculate_bazi(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
**req.time_options()
)
# Step 2: Analyze life pattern (格局)
analyzer = GenjuAnalyzer(bazi_result)
genju_analysis = analyzer.analyze_genju()
logger.info(f"✅ Genju analysis successful: {genju_analysis['primary_pattern']['pattern_name']} - {genju_analysis['pattern_level']['level']}")
return {
"success": True,
"data": {
"bazi_chart": bazi_result,
"genju_analysis": genju_analysis
}
}
except ValueError as e:
logger.error(f"❌ Validation Error: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Genju Analysis Error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Genju pattern analysis failed: {str(e)}")
# ==================== Evidence-first Dayun Dynamics API ====================
@app.post("/analyze-dayun-dynamics")
@app.post("/api/analyze-dayun-dynamics")
def analyze_dayun_dynamics(req: BaZiCompleteRequest, api_key: str = Depends(verify_api_key)):
"""
Return deterministic, role-aware Da Yun dynamics.
This endpoint separates the broad Five-Element climate from the way a
Da Yun interacts with the natal chart. It reports evidence, counter-
evidence, transformation gates, and uncertainty instead of a single
fortune score. It does not call an LLM.
"""
try:
if len(req.birth_date.split('-')) != 3:
raise HTTPException(
status_code=400,
detail="Invalid birth_date format. Use YYYY-MM-DD (e.g., '1990-01-15')"
)
if len(req.birth_time.split(':')) != 2:
raise HTTPException(
status_code=400,
detail="Invalid birth_time format. Use HH:MM (e.g., '08:30')"
)
if req.gender not in ['male', 'female']:
raise HTTPException(
status_code=400,
detail="Invalid gender. Use 'male' or 'female'"
)
if req.num_dayun < 1 or req.num_dayun > 12:
raise HTTPException(
status_code=400,
detail="num_dayun must be between 1 and 12"
)
bazi_complete = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=req.num_dayun,
**req.time_options()
)
genju_analysis = GenjuAnalyzer(bazi_complete).analyze_genju()
current_age = None
try:
current_age = datetime.now().year - int(req.birth_date.split('-')[0])
except (ValueError, IndexError):
pass
dynamics = build_dayun_dynamic_assessment(
bazi_complete,
genju_analysis,
current_age=current_age,
)
return {
"success": True,
"data": {
"bazi_chart": bazi_complete.get("bazi_chart"),
"day_master": bazi_complete.get("day_master"),
"yun_direction": bazi_complete.get("yun_direction"),
"yun_start_age": bazi_complete.get("yun_start_age"),
"dayun_dynamics": dynamics,
},
}
except HTTPException:
raise
except ValueError as e:
logger.error(f"❌ Dayun dynamics validation error: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Dayun dynamics calculation error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Dayun dynamics calculation failed: {str(e)}")
# ==================== Dayun (Major Cycles) Analysis API (大运分析) ====================
# ⚠️ DEPRECATED (2026-07): 2026-07-12全仓库排查确认astrologybazi前端零调用此endpoint
# (唯一出现处是tianji-oracle的proxy allowlist字符串,从未被实际发起请求)。
# 保留仅为兼容,不删除代码——见 API_UNIFICATION_PLAN.md §1.1/§6。
# 大运数据现在应从 /calculate-bazi-complete 的 timing/evidence 字段获取。
@app.post("/analyze-dayun")
@app.post("/api/analyze-dayun") # Alternative endpoint
def analyze_dayun(req: BaZiCompleteRequest, api_key: str = Depends(verify_api_key)):
"""
📅 Analyze Major Cycles (大运分析) - Premium Fortune Prediction
⚠️ DEPRECATED: 零前端调用,详见函数上方模块级注释。
🔒 Requires API Key authentication via x-api-key header
**Premium Feature** - Comprehensive 120-year fortune prediction and life planning:
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format (e.g., '1990-01-15')
- `birth_time`: Birth time in HH:MM format (e.g., '08:30')
- `gender`: 'male' or 'female' (default: 'male')
- `is_lunar`: True for lunar calendar, False for solar/Gregorian (default: False)
- `num_dayun`: Number of major cycles (default: 12, each 10 years = 120 years)
**Response:**
- `bazi_chart`: Complete BaZi chart with Dayun data
- `genju_analysis`: Pattern analysis with Yongshen (用神)
- `dayun_analysis`: Comprehensive Dayun fortune analysis:
- `dayun_analysis`: List of all major cycles with:
- Fortune quality (大吉/顺遂/平稳/波折/困难)
- Detailed score and reasons
- Event predictions (career/wealth/marriage/health/study)
- Life advice for each period
- Critical years identification
- `turning_points`: Major life turning points
- Age and Ganzhi of turning point
- Direction (转吉/转凶/重大变动)
- Score changes
- `life_stages`: Life stage analysis
- Early years (0-30): Average score and judgment
- Middle years (30-60): Average score and judgment
- Late years (60+): Average score and judgment
- Best life stage identification
- `overall_summary`: Complete life fortune summary
- Total average score
- Quality distribution
- Best and worst Dayun periods
- Overall judgment
**Fortune Prediction Features:**
1. **Yongshen-based Analysis** (用神判断法)
- The returned 0–100 value is an explainable heuristic signal.
- It is not a probability, percentile, or historically calibrated forecast.
- `judgment.score_method` and `judgment.score_confidence` identify its semantics.
2. **Special Combinations** (特殊组合)
- Relationship adjustments are calculated once by the central
XingChongHeHeCalculator.
- Sui Yun Bing Lin (岁运并临) is a year-level event only when the full
annual GanZhi equals the Dayun GanZhi; it is not a fixed decade penalty.
3. **Auspicious Spirits** (吉神)
- Tianyi Nobleman (天乙贵人), Yi Ma (驿马), and Peach Blossom (桃花)
remain bounded heuristic signals, not guaranteed outcomes.
4. **Event Predictions** (事件预测)
- Career: Promotion, job change, peak period
- Wealth: Investment timing, income increase, financial risks
- Marriage: Wedding timing, relationship quality, crisis periods
- Health: Peak health years, illness risks, prevention advice
- Study: Academic success, exam luck, learning opportunities
**Example:**
```json
{
"birth_date": "1990-01-15",
"birth_time": "08:30",
"gender": "male",
"is_lunar": false,
"num_dayun": 12
}
```
**Response Example:**
```json
{
"success": true,
"data": {
"bazi_chart": {...},
"genju_analysis": {...},
"dayun_analysis": {
"dayun_analysis": [
{
"ganzhi": "戊寅",
"age_range": "7-16岁",
"judgment": {
"quality": "good",
"quality_cn": "顺遂平安",
"label": "✅ 良好",
"score": 65,
"reasons": [
"✅ 大运为用神五行,有力助身",
"✅ 大运逢驿马,利于走动变动"
]
},
"events": {
"career": ["7-10岁:学业进步快"],
"wealth": ["..."],
"marriage": ["..."],
"health": ["..."],
"study": ["7-16岁:学业进步快,考试运佳"]
},
"advice": [
"✅ 7-16岁运势向好,适合稳健发展",
"抓住学习机会,提升知识能力"
],
"critical_years": [
{
"year": 2011,
"age": 22,
"ganzhi": "辛卯",
"importance": 3,
"reasons": ["流年与日柱天克地冲"]
}
]
}
],
"turning_points": [
{
"age": 37,
"from_dayun": "己卯",
"to_dayun": "庚辰",
"direction": "转吉",
"score_change": 35,
"description": "37岁进入庚辰大运,运势转吉"
}
],
"life_stages": {
"early_years": {
"age_range": "0-30岁(早年运)",
"avg_score": 58.5,
"judgment": "运势良好,平稳向上"
},
"middle_years": {
"age_range": "30-60岁(中年运)",
"avg_score": 72.3,
"judgment": "运势极佳,大吉大利"
},
"late_years": {
"age_range": "60岁以上阶段",
"avg_score": 45.2,
"judgment": "运势平稳,中规中矩"
},
"best_stage": "middle"
},
"overall_summary": {
"total_dayun_count": 12,
"avg_score": 58.7,
"overall_judgment": "已计算周期整体偏稳,仍需按当前窗口调整节奏",
"quality_distribution": {
"excellent": 2,
"good": 5,
"average": 3,
"challenging": 2,
"difficult": 0
},
"best_dayun": {
"ganzhi": "庚辰",
"age_range": "37-46岁",
"score": 85
},
"worst_dayun": {
"ganzhi": "丙子",
"age_range": "67-76岁",
"score": 32
}
}
}
}
}
```
**Perfect for:**
- 10-year fortune predictions ($199-$399)
- Life planning consultation ($299-$799)
- Career timing analysis ($399-$999)
- Investment decision support ($499-$1,499)
- Marriage timing consultation ($199-$499)
- Comprehensive destiny report ($499-$1,999)
**Business Value:**
- Subscription model: $29-49/month for ongoing Dayun updates
- One-time reports: $199-$1,999 depending on depth
- Monthly revenue potential: $2,500-$5,000
"""
try:
logger.info(f"📅 Analyzing Dayun for: {req.birth_date} {req.birth_time}")
# Validate inputs
if len(req.birth_date.split('-')) != 3:
raise HTTPException(
status_code=400,
detail="Invalid birth_date format. Use YYYY-MM-DD (e.g., '1990-01-15')"
)
if len(req.birth_time.split(':')) != 2:
raise HTTPException(
status_code=400,
detail="Invalid birth_time format. Use HH:MM (e.g., '08:30')"
)
if req.gender not in ['male', 'female']:
raise HTTPException(
status_code=400,
detail="Invalid gender. Use 'male' or 'female'"
)
# Step 1: Calculate complete BaZi with Dayun
bazi_complete = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=req.num_dayun,
**req.time_options()
)
# Step 2: Analyze life pattern (格局) to get Yongshen
genju_analyzer = GenjuAnalyzer(bazi_complete)
genju_analysis = genju_analyzer.analyze_genju()
# 🎯 计算当前年龄(用于标记当前大运)
from datetime import datetime
birth_year = int(req.birth_date.split('-')[0])
current_year = datetime.now().year
current_age = current_year - birth_year
# Step 3: Analyze Dayun (major cycles)
dayun_analyzer = DayunAnalyzer(bazi_complete, genju_analysis, current_age=current_age)
dayun_analysis = dayun_analyzer.analyze_all_dayun()
logger.info(f"✅ Dayun analysis successful: {len(dayun_analysis['dayun_analysis'])} major cycles analyzed")
logger.info(f" Overall judgment: {dayun_analysis['overall_summary']['overall_judgment']}")
return {
"success": True,
"data": {
"bazi_chart": bazi_complete,
"genju_analysis": genju_analysis,
"dayun_analysis": dayun_analysis
}
}
except ValueError as e:
logger.error(f"❌ Validation Error: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Dayun Analysis Error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Dayun analysis failed: {str(e)}")
# ==================== AI Interpretation API (AI解读引擎) ====================
@app.post("/ai-interpretation")
@app.post("/api/ai-interpretation") # Alternative endpoint
def ai_interpretation(req: BaZiInterpretationRequest, api_key: str = Depends(verify_api_key)):
"""
🤖 AI BaZi Interpretation - Transform Technical Analysis into Natural Language
🔒 Requires API Key authentication via x-api-key header
**Premium Feature** - Professional natural language interpretation of BaZi destiny analysis:
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format (e.g., '1990-01-15')
- `birth_time`: Birth time in HH:MM format (e.g., '08:30')
- `gender`: 'male' or 'female' (default: 'male')
- `is_lunar`: True for lunar calendar, False for solar/Gregorian (default: False)
- `interpretation_level`: Depth of interpretation
- **'basic'**: 3,000 words - Essential interpretation ($29)
- Personality, Career, Wealth, Marriage, Health, Overall
- **'professional'**: 8,000 words - Detailed analysis ($99)
- Basic sections + Dayun overview + Life stages analysis
- **'master'**: 15,000 words - Complete life planning ($199)
- Professional sections + Turning points + All Dayun details + Spiritual forces + Life planning
- `language`: Output language 'zh-CN' (Chinese), 'en' (English), 'zh-TW' (Traditional Chinese)
- `num_dayun`: Number of major cycles to analyze (default: 12 = 120 years)
**Response:**
- `bazi_chart`: Complete BaZi chart with all technical data
- `shensha_analysis`: Spiritual forces analysis (47 forces)
- `genju_analysis`: Pattern analysis with Yongshen (用神)
- `dayun_analysis`: Major cycles fortune prediction
- `ai_interpretation`: **Natural language interpretation** 🌟
- `level`: Interpretation depth ('basic'/'professional'/'master')
- `word_count`: Total word count
- `sections`: Natural language sections:
- **personality**: Who you are (based on Day Master + Pattern)
- Example: "你是甲木日主,就像大树一样,天性正直向上..."
- **career**: Career recommendations with specific industries
- Example: "你的八字显示你适合在有组织、有制度的环境中工作,如公务员..."
- **wealth**: Current wealth-channel and carrying-capacity signals
- **marriage**: Marriage quality and spouse characteristics
- **health**: Health advice based on Five Elements
- **overall**: Structural summary and current-phase suggestions
- **(Professional/Master only)** **dayun_overview**: Current and next cycle signals
- **(Professional/Master only)** **life_stages**: Current-window calibration
- **(Master only)** **turning_points**: Turning points near the current window
- **(Master only)** **ten_year_fortunes**: Current and next cycle details
- **(Master only)** **auspicious_spirits**: Beneficial forces interpretation
- **(Master only)** **inauspicious_spirits**: Challenging forces interpretation
- **(Master only)** **life_planning**: Comprehensive life roadmap
**Key Transformation Examples:**
| Technical Term | Natural Language |
|----------------|------------------|
| 甲木日主 + 正官格 | "你是一个正直向上、有责任心的人,适合在政府或大企业工作" |
| 用神为正印 | "学习是你的贵人,通过教育和知识提升可以改变命运" |
| 大运走喜神运 | "未来10年运势极佳,是创业或升职的黄金时期" |
| 羊刃 + 七杀 | "性格刚强果断,适合军警或竞争性行业" |
| 桃花 + 红鸾 | "异性缘好,容易遇到心仪对象" |
**Example:**
```json
{
"birth_date": "1990-01-15",
"birth_time": "08:30",
"gender": "male",
"is_lunar": false,
"interpretation_level": "professional",
"language": "zh-CN",
"num_dayun": 12
}
```
**Response Example:**
```json
{
"success": true,
"data": {
"bazi_chart": {...},
"shensha_analysis": {...},
"genju_analysis": {...},
"dayun_analysis": {...},
"ai_interpretation": {
"level": "professional",
"word_count": 8247,
"sections": {
"personality": "你是甲木日主,就像大树一样,天性正直向上、积极进取。你有很强的责任心...",
"career": "你的八字显示你适合在有组织、有制度的环境中工作,如公务员、政府官员...",
"wealth": "当前财路与承载力信号:先核现金流、支持条件和可重复交付...",
"marriage": "你的婚姻质量良好,配偶贤惠、稳重、有责任心...",
"health": "作为木性日主,你需要特别注意肝胆的健康...",
"overall": "当前结构提示资源与职责如何协同;请结合当前窗口的现实反馈...",
"dayun_overview": "当前与下一大运窗口的结构信号与可执行建议...",
"life_stages": "当前阶段校准:把机会当作工作假设,用现金流和结果复核..."
}
}
}
}
```
**Perfect for:**
- BaZi consulting services ($29-$199 per reading)
- Automated fortune-telling platforms
- Mobile apps with human-readable reports
- Life coaching and career consulting
- Subscription-based destiny analysis services
**Business Value:**
- **Basic tier**: $29 × 100 users/month = $2,900/month
- **Professional tier**: $99 × 50 users/month = $4,950/month
- **Master tier**: $199 × 25 users/month = $4,975/month
- **Total potential**: $12,825/month ($153,900/year)
**Monthly Revenue Projection:**
- Conservative (50 users): $2,500/month
- Moderate (200 users): $10,000/month
- Optimistic (500 users): $20,000+/month
"""
try:
logger.info(f"🤖 AI Interpretation for: {req.birth_date} {req.birth_time}, level: {req.interpretation_level}")
# Validate inputs
if len(req.birth_date.split('-')) != 3:
raise HTTPException(
status_code=400,
detail="Invalid birth_date format. Use YYYY-MM-DD (e.g., '1990-01-15')"
)
if len(req.birth_time.split(':')) != 2:
raise HTTPException(
status_code=400,
detail="Invalid birth_time format. Use HH:MM (e.g., '08:30')"
)
if req.gender not in ['male', 'female']:
raise HTTPException(
status_code=400,
detail="Invalid gender. Use 'male' or 'female'"
)
if req.interpretation_level not in ['basic', 'professional', 'master']:
raise HTTPException(
status_code=400,
detail="Invalid interpretation_level. Use 'basic', 'professional', or 'master'"
)
if req.language not in ['zh-CN', 'en', 'zh-TW']:
raise HTTPException(
status_code=400,
detail="Invalid language. Use 'zh-CN', 'en', or 'zh-TW'"
)
# Step 1: Calculate complete BaZi with Dayun
logger.info(" Step 1/5: Calculating complete BaZi with Dayun...")
bazi_complete = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=req.num_dayun,
**req.time_options()
)
logger.info(f" ✅ BaZi calculated: {bazi_complete['day_master']['gan']}日主")
# Step 2: Analyze spiritual forces (Shensha)
logger.info(" Step 2/5: Analyzing spiritual forces (Shensha)...")
shensha_analyzer = ShenshaAnalyzer(bazi_complete)
shensha_analysis = shensha_analyzer.analyze_all_shenshas()
logger.info(f" ✅ Shensha analyzed: {len(shensha_analysis['auspicious_gods'])} auspicious, {len(shensha_analysis['inauspicious_gods'])} inauspicious")
# Step 3: Analyze life pattern (Genju) to get Yongshen
logger.info(" Step 3/5: Analyzing life pattern (Genju) for Yongshen...")
genju_analyzer = GenjuAnalyzer(bazi_complete)
genju_analysis = genju_analyzer.analyze_genju()
logger.info(f" ✅ Genju analyzed: {genju_analysis['primary_pattern']['pattern_name']}, 用神: {genju_analysis['yongshen_analysis']['用神']}")
# Step 4: Analyze Dayun (major cycles)
logger.info(" Step 4/5: Analyzing Dayun (major cycles)...")
# 🎯 计算当前年龄(用于标记当前大运)
from datetime import datetime
birth_year = int(req.birth_date.split('-')[0])
current_year = datetime.now().year
current_age = current_year - birth_year
logger.info(f" 📍 Current age: {current_age} years (born {birth_year})")
dayun_analyzer = DayunAnalyzer(bazi_complete, genju_analysis, current_age=current_age)
dayun_analysis = dayun_analyzer.analyze_all_dayun()
logger.info(f" ✅ Dayun analyzed: {len(dayun_analysis['dayun_analysis'])} periods, avg score: {dayun_analysis['overall_summary']['avg_score']}")
# 🎯 Phase 3: Advanced Analysis Modules (2025-12-24)
logger.info(" Step 4.5/6: Running Phase 3 advanced analysis modules...")
# 🌐 Map API language to analyzer language ('zh-CN'/'zh-TW' → 'zh', 'en' → 'en')
analyzer_lang = 'en' if req.language == 'en' else 'zh'
# 婚姻分析
marriage_analyzer = MarriageAnalyzer(bazi_complete, dayun_analysis, language=analyzer_lang)
marriage_analysis = marriage_analyzer.analyze_marriage()
logger.info(f" ✅ Marriage analyzed: Quality {marriage_analysis['marriage_quality']['score']}/100")
# 事业分析
career_analyzer = CareerAnalyzer(bazi_complete, genju_analysis, dayun_analysis) # 🔧 Bug fix: 添加dayun_analysis
career_analysis = career_analyzer.analyze_career(language=analyzer_lang)
logger.info(f" ✅ Career analyzed: {len(career_analysis['recommended_careers'])} careers recommended")
# 🆕 财运分析
wealth_analyzer = WealthAnalyzer(bazi_complete, dayun_analysis)
wealth_analysis = wealth_analyzer.analyze_wealth(language=analyzer_lang)
logger.info(f" ✅ Wealth analyzed: Score {wealth_analysis['wealth_quality']['score']}/100")
# 健康分析
health_analyzer = HealthAnalyzer(bazi_complete)
health_analysis = health_analyzer.analyze_health(language=analyzer_lang)
logger.info(f" ✅ Health analyzed: {len(health_analysis['health_risks'])} risks identified")
# 🆕 官职分析 (2025-12-25)
official_analyzer = OfficialAnalyzer(bazi_complete, dayun_analysis)
official_analysis = official_analyzer.analyze_official()
logger.info(f" ✅ Official analyzed: Level {official_analysis.get('official_level', 'N/A')}")
# 🆕 法律风险分析 (2025-12-25)
legal_risk_analyzer = LegalRiskAnalyzer(bazi_complete, dayun_analysis)
legal_risk_analysis = legal_risk_analyzer.analyze_legal_risk()
logger.info(f" ✅ Legal risk analyzed: Score {legal_risk_analysis.get('risk_score', 0)}/100")
# Step 5: Generate AI interpretation
logger.info(f" Step 5/6: Generating {req.interpretation_level} level AI interpretation...")
interpreter = AIInterpreter(
bazi_chart=bazi_complete,
shensha_analysis=shensha_analysis,
genju_analysis=genju_analysis,
dayun_analysis=dayun_analysis
)
ai_interpretation = interpreter.generate_interpretation(
level=req.interpretation_level,
language=req.language
)
logger.info(f" ✅ Interpretation generated: {ai_interpretation['word_count']} words, {len(ai_interpretation['sections'])} sections")
logger.info(f"✅ Complete AI interpretation successful for {req.interpretation_level} level")
# 🎯 Extract life curve data (1-100 age fortune table with scores)
life_curve_data = None
if hasattr(interpreter, 'life_curve_data') and interpreter.life_curve_data:
life_curve_data = interpreter.life_curve_data
return {
"success": True,
"data": {
"bazi_chart": bazi_complete,
"shensha_analysis": shensha_analysis,
"genju_analysis": genju_analysis,
"dayun_analysis": dayun_analysis,
"ai_interpretation": ai_interpretation,
"life_curve_data": life_curve_data, # Phase 2: 1-100岁完整运势数据
# 🎯 Phase 3: Advanced Analysis (2025-12-24)
"marriage_analysis": marriage_analysis, # 婚姻分析
"career_analysis": career_analysis, # 事业分析
"health_analysis": health_analysis, # 健康分析
"wealth_analysis": wealth_analysis, # 财运分析
# 🆕 Phase 3.5: Official & Legal Analysis (2025-12-25)
"official_analysis": official_analysis, # 官职分析
"legal_risk_analysis": legal_risk_analysis # 法律风险分析
}
}
except ValueError as e:
logger.error(f"❌ Validation Error: {e}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ AI Interpretation Error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"AI interpretation failed: {str(e)}")
# ==================== Time Fortune Prediction API (时间预测API) ====================
@app.post("/monthly-fortune")
@app.post("/api/monthly-fortune") # Alternative endpoint
def analyze_monthly_fortune_api(req: MonthlyFortuneRequest, api_key: str = Depends(verify_api_key)):
"""
📅 Monthly Fortune Analysis (流月运势分析)
🔒 Requires API Key authentication
**Premium Feature** - Analyze fortune for a specific month ($9.9/month subscription)
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format
- `birth_time`: Birth time in HH:MM format
- `gender`: 'male' or 'female'
- `is_lunar`: True if lunar calendar
- `target_year`: Year to analyze (e.g., 2026)
- `target_month`: Month to analyze (1-12)
- `language`: 'zh' (Chinese, default) or 'en' (cold-reading English with Barnum Effect)
**Response:**
- Monthly fortune score (0-100)
- Quality assessment (Chinese: 大吉/顺遂/平稳/波折/困难, English: Peak Performance Window/Favorable Momentum/etc.)
- **English mode**: Cold-reading format with specific dates, emotional triggers, Barnum Effect
- **Chinese mode**: Traditional fortune analysis with reasons and advice
- Suitable/avoid activities (Chinese mode only)
**Business Value**: $9.9/month subscription × 200 users = $1,980/month
"""
try:
logger.info(f"📅 Analyzing monthly fortune for {req.target_year}-{req.target_month}")
# Calculate BaZi
bazi_chart = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=12
)
# Analyze Genju for Yongshen
genju_analyzer = GenjuAnalyzer(bazi_chart)
genju_analysis = genju_analyzer.analyze_genju()
# Analyze Monthly Fortune
fortune_analyzer = TimeFortuneAnalyzer(bazi_chart, genju_analysis, None)
monthly_fortune = fortune_analyzer.analyze_monthly_fortune(req.target_year, req.target_month)
logger.info(f"✅ Monthly fortune analyzed: {monthly_fortune['quality_cn']} ({monthly_fortune['score']}/100)")
# Apply cold-reading transformation for English
if req.language == "en":
monthly_fortune_output = apply_cold_reading_english(
monthly_fortune,
req.target_year,
req.target_month
)
logger.info(f"🔮 Applied cold-reading English transformation")
else:
monthly_fortune_output = monthly_fortune
return {
"success": True,
"data": {
"bazi_chart": bazi_chart,
"monthly_fortune": monthly_fortune_output
}
}
except Exception as e:
logger.error(f"❌ Monthly fortune analysis error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Monthly fortune analysis failed: {str(e)}")
@app.post("/career-monthly-fortune")
@app.post("/api/career-monthly-fortune") # Alternative endpoint
def analyze_career_monthly_fortune_api(req: CareerMonthlyFortuneRequest, api_key: str = Depends(verify_api_key)):
"""
💼 Career-Specific Monthly Fortune Analysis (事业月运分析)
🔒 Requires API Key authentication
**Career Forecast Feature** - Analyze 12 months of career fortune for a specific year
⚠️ **Product Differentiation**: This endpoint provides CAREER-ONLY monthly analysis.
It is different from Tianji Oracle's comprehensive monthly fortune (which includes health, relationships, wealth).
This prevents content duplication if users purchase both products.
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format
- `birth_time`: Birth time in HH:MM format
- `gender`: 'male' or 'female'
- `is_lunar`: True if lunar calendar
- `target_year`: Year to analyze (e.g., 2026)
**Response:**
- Array of 12 months with:
- `month`: "YYYY-MM" format
- `month_ganzhi`: Monthly celestial stem and branch (干支)
- `career_score`: Career fortune score (0-100)
- `quality_cn`: Quality assessment (大吉/顺遂/平稳/波折/困难)
- `opportunities`: List of career opportunities this month
- `risks`: List of career risks to watch out for
- `advice`: Actionable career advice for the month
**Business Value**: Part of Career Forecast product ($19.99)
"""
try:
logger.info(f"💼 Analyzing career monthly fortune for year {req.target_year}")
# Calculate BaZi
bazi_chart = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=False, # Don't need Dayun for this
num_dayun=0
)
# Initialize CareerAnalyzer
career_analyzer = CareerAnalyzer(bazi_chart)
# Analyze career monthly fortune
monthly_fortune = career_analyzer._analyze_career_monthly_fortune(req.target_year)
logger.info(f"✅ Career monthly fortune analyzed: {len(monthly_fortune)} months")
return {
"success": True,
"data": {
"target_year": req.target_year,
"monthly_fortune": monthly_fortune,
"note": "Career-only analysis. For comprehensive fortune including health/relationships/wealth, see Tianji Oracle product."
}
}
except Exception as e:
logger.error(f"❌ Career monthly fortune analysis error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Career monthly fortune analysis failed: {str(e)}")
@app.post("/yearly-fortune")
@app.post("/api/yearly-fortune") # Alternative endpoint
def analyze_yearly_fortune_api(req: YearlyFortuneRequest, api_key: str = Depends(verify_api_key)):
"""
🎯 Yearly Fortune Analysis - 12 Month Complete Forecast (流年12个月运势)
🔒 Requires API Key authentication
**Premium Feature** - Get complete yearly fortune prediction for all 12 months
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format
- `birth_time`: Birth time in HH:MM format
- `gender`: 'male' or 'female'
- `is_lunar`: True if lunar calendar
- `target_year`: Year to analyze (defaults to current year)
- `language`: 'zh' or 'en' for output language
**Response:**
- Complete BaZi chart with Five Elements analysis
- 12-month fortune array with scores, quality, reasons, advice
- Yearly overview with best/worst months
- Feng Shui recommendations per month
**Business Value**: Powers Tianji Oracle Premium ($19.99/reading)
"""
try:
# Default to current year if not specified
target_year = req.target_year or datetime.now().year
logger.info(f"🎯 Analyzing yearly fortune for {target_year}, birth: {req.birth_date}")
# 1. Calculate BaZi chart (only once!)
bazi_chart = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=12
)
# 2. Analyze Genju for Yongshen/Jishen
genju_analyzer = GenjuAnalyzer(bazi_chart)
genju_analysis = genju_analyzer.analyze_genju()
# 3. Initialize fortune analyzer once
fortune_analyzer = TimeFortuneAnalyzer(bazi_chart, genju_analysis, None)
# 4. Analyze all 12 months
monthly_fortunes = []
total_score = 0
best_month = {"month": 1, "score": 0}
worst_month = {"month": 1, "score": 100}
for month in range(1, 13):
monthly_fortune = fortune_analyzer.analyze_monthly_fortune(target_year, month)
# Track best/worst
score = monthly_fortune.get('score', 50)
total_score += score
if score > best_month["score"]:
best_month = {"month": month, "score": score, "quality": monthly_fortune.get('quality_cn', '平稳')}
if score < worst_month["score"]:
worst_month = {"month": month, "score": score, "quality": monthly_fortune.get('quality_cn', '平稳')}
monthly_fortunes.append({
"month": month,
"month_name_zh": f"{month}月",
"month_name_en": datetime(target_year, month, 1).strftime("%B"),
**monthly_fortune
})
# 5. Calculate yearly overview
avg_score = round(total_score / 12)
yearly_quality = (
"大吉" if avg_score >= 80 else
"顺遂" if avg_score >= 65 else
"平稳" if avg_score >= 50 else
"波折" if avg_score >= 35 else
"困难"
)
# 6. Extract key info for frontend
day_master = bazi_chart.get('day_master', {})
five_elements = bazi_chart.get('five_elements', {})
logger.info(f"✅ Yearly fortune analyzed: avg={avg_score}, best=M{best_month['month']}, worst=M{worst_month['month']}")
return {
"success": True,
"data": {
# BaZi summary (not full chart to reduce payload)
"bazi_summary": {
"day_master": day_master,
"five_elements": five_elements,
"year_pillar": bazi_chart.get('bazi_chart', {}).get('year', {}),
"month_pillar": bazi_chart.get('bazi_chart', {}).get('month', {}),
"day_pillar": bazi_chart.get('bazi_chart', {}).get('day', {}),
"hour_pillar": bazi_chart.get('bazi_chart', {}).get('hour', {}),
},
# Yongshen/Jishen
"genju_summary": {
"yongshen": genju_analysis.get('yongshen', ''),
"jishen": genju_analysis.get('jishen', []),
"pattern": genju_analysis.get('pattern', ''),
},
# Yearly overview
"yearly_overview": {
"year": target_year,
"average_score": avg_score,
"quality": yearly_quality,
"best_month": best_month,
"worst_month": worst_month,
},
# All 12 months
"monthly_fortunes": monthly_fortunes,
}
}
except Exception as e:
logger.error(f"❌ Yearly fortune analysis error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Yearly fortune analysis failed: {str(e)}")
@app.post("/daily-fortune")
@app.post("/api/daily-fortune") # Alternative endpoint
def analyze_daily_fortune_api(req: DailyFortuneRequest, api_key: str = Depends(verify_api_key)):
"""
🗓️ Daily Fortune & Date Selection (流日择日分析)
🔒 Requires API Key authentication
**Premium Feature** - Analyze if a specific date is suitable for certain activities ($19/reading)
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format
- `birth_time`: Birth time in HH:MM format
- `gender`: 'male' or 'female'
- `is_lunar`: True if lunar calendar
- `target_date`: Date to analyze in YYYY-MM-DD format
- `activities`: Activities to check (e.g., ['嫁娶', '开业', '搬家'])
**Response:**
- Daily fortune score (0-100)
- Overall verdict (大吉/吉/平/凶/大凶)
- Activity-specific analysis with scores
- Almanac (宜忌) from traditional calendar
**Common Activities:**
- 嫁娶 (Marriage), 开业 (Business Opening), 搬家 (Moving)
- 动土 (Construction), 出行 (Travel), 签约 (Contract Signing)
- 求财 (Wealth Seeking), 祭祀 (Worship), 安葬 (Burial)
**Business Value**: $19/reading × 100 users = $1,900/month
"""
try:
logger.info(f"🗓️ Analyzing daily fortune for {req.target_date}")
# Parse target date
try:
year, month, day = map(int, req.target_date.split('-'))
target_date_obj = date(year, month, day)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid target_date format. Use YYYY-MM-DD")
# Calculate BaZi
bazi_chart = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=12
)
# Analyze Genju for Yongshen
genju_analyzer = GenjuAnalyzer(bazi_chart)
genju_analysis = genju_analyzer.analyze_genju()
# Analyze Daily Fortune
fortune_analyzer = TimeFortuneAnalyzer(bazi_chart, genju_analysis, None)
daily_fortune = fortune_analyzer.analyze_daily_fortune(target_date_obj, req.activities)
logger.info(f"✅ Daily fortune analyzed: {daily_fortune['quality_cn']} ({daily_fortune['score']}/100)")
return {
"success": True,
"data": {
"bazi_chart": bazi_chart,
"daily_fortune": daily_fortune
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Daily fortune analysis error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Daily fortune analysis failed: {str(e)}")
@app.post("/hourly-fortune")
@app.post("/api/hourly-fortune") # Alternative endpoint
def analyze_hourly_fortune_api(req: DailyFortuneRequest, api_key: str = Depends(verify_api_key)):
"""
⏰ Hourly Fortune Analysis (时辰吉凶分析)
🔒 Requires API Key authentication
**Premium Feature** - Analyze fortune for all 12 hours of a day
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format
- `birth_time`: Birth time in HH:MM format
- `gender`: 'male' or 'female'
- `is_lunar`: True if lunar calendar
- `target_date`: Date to analyze in YYYY-MM-DD format
**Response:**
- 12 hourly fortunes with scores (0-100 each)
- Tian Shen (天神) for each hour
- Suitable activities for each hour
- Recommendations
**Use Cases:**
- Find best time for important meetings
- Schedule medical appointments
- Plan business negotiations
- Choose wedding ceremony time
"""
try:
logger.info(f"⏰ Analyzing hourly fortune for {req.target_date}")
# Parse target date
try:
year, month, day = map(int, req.target_date.split('-'))
target_date_obj = date(year, month, day)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid target_date format. Use YYYY-MM-DD")
# Calculate BaZi
bazi_chart = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=12
)
# Analyze Genju for Yongshen
genju_analyzer = GenjuAnalyzer(bazi_chart)
genju_analysis = genju_analyzer.analyze_genju()
# Analyze Hourly Fortune
fortune_analyzer = TimeFortuneAnalyzer(bazi_chart, genju_analysis, None)
hourly_fortunes = fortune_analyzer.analyze_hourly_fortune(target_date_obj)
logger.info(f"✅ Hourly fortune analyzed: 12 hours")
return {
"success": True,
"data": {
"date": str(target_date_obj),
"hourly_fortunes": hourly_fortunes
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Hourly fortune analysis error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Hourly fortune analysis failed: {str(e)}")
@app.post("/find-auspicious-dates")
@app.post("/api/find-auspicious-dates") # Alternative endpoint
def find_auspicious_dates_api(req: FindAuspiciousDatesRequest, api_key: str = Depends(verify_api_key)):
"""
🔍 Find Auspicious Dates (寻找吉日)
🔒 Requires API Key authentication
**Premium Feature** - Find best dates for specific activities within a date range
**Request Body:**
- `birth_date`: Birth date in YYYY-MM-DD format
- `birth_time`: Birth time in HH:MM format
- `gender`: 'male' or 'female'
- `is_lunar`: True if lunar calendar
- `start_date`: Range start date in YYYY-MM-DD format
- `end_date`: Range end date in YYYY-MM-DD format (max 90 days)
- `activity`: Activity type (e.g., '嫁娶', '开业', '搬家')
- `min_score`: Minimum score requirement (default: 70)
**Response:**
- List of auspicious dates sorted by score
- Each date includes:
- Overall score and activity-specific score
- Verdict (大吉/吉/平)
- Detailed reasons
- Weekday information
**Common Use Cases:**
- 🎊 Wedding date selection (嫁娶)
- 🏢 Business opening dates (开业)
- 🏠 Moving dates (搬家)
- ✍️ Contract signing dates (签约)
- ✈️ Travel start dates (出行)
**Business Value**: Part of $19 date selection service
"""
try:
logger.info(f"🔍 Finding auspicious dates for {req.activity} from {req.start_date} to {req.end_date}")
# Parse dates
try:
start_year, start_month, start_day = map(int, req.start_date.split('-'))
end_year, end_month, end_day = map(int, req.end_date.split('-'))
start_date_obj = date(start_year, start_month, start_day)
end_date_obj = date(end_year, end_month, end_day)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
# Validate date range
if end_date_obj <= start_date_obj:
raise HTTPException(status_code=400, detail="end_date must be after start_date")
if (end_date_obj - start_date_obj).days > 90:
raise HTTPException(status_code=400, detail="Date range cannot exceed 90 days")
# Calculate BaZi
bazi_chart = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
include_dayun=True,
num_dayun=12
)
# Analyze Genju for Yongshen
genju_analyzer = GenjuAnalyzer(bazi_chart)
genju_analysis = genju_analyzer.analyze_genju()
# Find Auspicious Dates
fortune_analyzer = TimeFortuneAnalyzer(bazi_chart, genju_analysis, None)
auspicious_dates = fortune_analyzer.find_auspicious_dates(
start_date_obj,
end_date_obj,
req.activity,
req.min_score
)
logger.info(f"✅ Found {len(auspicious_dates)} auspicious dates for {req.activity}")
return {
"success": True,
"data": {
"activity": req.activity,
"date_range": {
"start": req.start_date,
"end": req.end_date
},
"min_score": req.min_score,
"found_count": len(auspicious_dates),
"auspicious_dates": auspicious_dates[:30] # Limit to 30 results
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Find auspicious dates error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Find auspicious dates failed: {str(e)}")
# ==================== Chinese Calendar API ====================
@app.get("/api/calendar/today")
def get_today_calendar():
"""
Get today's Chinese lunar calendar information
📅 Public endpoint - No API key required
**Response:**
- Solar calendar date
- Lunar calendar date (农历)
- Gan Zhi (干支) for year, month, day
- Zodiac animal (生肖)
- Almanac data (宜忌)
- Solar term (节气) if applicable
- Festivals if applicable
**Example Response:**
```json
{
"success": true,
"date": "2025-12-10",
"weekday": "星期三",
"solar": {
"year": 2025,
"month": 12,
"day": 10,
"date_string": "2025年12月10日"
},
"lunar": {
"year": 2025,
"month": 11,
"day": 10,
"year_cn": "乙巳年",
"month_cn": "冬月",
"day_cn": "初十",
"date_string": "乙巳年冬月初十"
},
"ganzhi": {
"year": "乙巳",
"month": "戊子",
"day": "癸未"
},
"zodiac": {
"animal": "蛇"
},
"almanac": {
"appropriate": ["祭祀", "祈福", "求嗣"],
"avoid": ["嫁娶", "动土", "破土"]
}
}
```
"""
try:
logger.info("📅 Fetching today's Chinese calendar")
result = ChineseCalendarService.get_today_calendar()
logger.info(f"✅ Today's calendar retrieved: {result['date']}")
return result
except Exception as e:
logger.error(f"❌ Error getting today's calendar: {e}")
raise HTTPException(status_code=500, detail=f"Calendar service error: {str(e)}")
@app.get("/api/calendar/range")
def get_calendar_range(start: str, end: str):
"""
Get Chinese lunar calendar information for a date range
📅 Public endpoint - No API key required
**Query Parameters:**
- `start`: Start date in YYYY-MM-DD format
- `end`: End date in YYYY-MM-DD format
**Note:** Maximum range is 365 days
**Response:**
List of calendar objects for each date in the range
**Example:**
```
GET /api/calendar/range?start=2025-12-01&end=2025-12-31
```
"""
try:
logger.info(f"📅 Fetching calendar range: {start} to {end}")
# Parse dates
try:
start_year, start_month, start_day = map(int, start.split('-'))
end_year, end_month, end_day = map(int, end.split('-'))
start_date = date(start_year, start_month, start_day)
end_date = date(end_year, end_month, end_day)
except ValueError:
raise HTTPException(
status_code=400,
detail="Invalid date format. Use YYYY-MM-DD"
)
# Validate date range
if end_date < start_date:
raise HTTPException(
status_code=400,
detail="End date must be after start date"
)
result = ChineseCalendarService.get_calendar_range(start_date, end_date)
logger.info(f"✅ Calendar range retrieved: {len(result)} days")
return {
"success": True,
"start_date": start,
"end_date": end,
"total_days": len(result),
"calendars": result
}
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Error getting calendar range: {e}")
raise HTTPException(status_code=500, detail=f"Calendar service error: {str(e)}")
@app.get("/api/calendar/{date_str}")
def get_calendar_by_date(date_str: str):
"""
Get Chinese lunar calendar information for a specific date
📅 Public endpoint - No API key required
**Path Parameter:**
- `date_str`: Date in YYYY-MM-DD format (e.g., '2025-12-10')
**Response:**
Same as `/api/calendar/today` but for the specified date
**Example:**
```
GET /api/calendar/2025-12-10
```
"""
try:
logger.info(f"📅 Fetching calendar for: {date_str}")
# Parse date string
try:
year, month, day = map(int, date_str.split('-'))
target_date = date(year, month, day)
except ValueError:
raise HTTPException(
status_code=400,
detail="Invalid date format. Use YYYY-MM-DD (e.g., '2025-12-10')"
)
result = ChineseCalendarService.get_calendar_by_date(target_date)
logger.info(f"✅ Calendar retrieved for: {date_str}")
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error getting calendar for {date_str}: {e}")
raise HTTPException(status_code=500, detail=f"Calendar service error: {str(e)}")
class LunarToSolarRequest(BaseModel):
"""Request model for Lunar to Solar conversion"""
lunar_year: int = Field(..., description="Lunar year (e.g., 2025)", ge=1900, le=2100, example=2025)
lunar_month: int = Field(..., description="Lunar month (1-12)", ge=1, le=12, example=1)
lunar_day: int = Field(..., description="Lunar day (1-30)", ge=1, le=30, example=1)
is_leap_month: bool = Field(False, description="Whether it's a leap month (闰月)", example=False)
@app.post("/api/calendar/convert-lunar-to-solar")
def convert_lunar_to_solar(request: LunarToSolarRequest):
"""
Convert Chinese Lunar calendar date to Gregorian (Solar) calendar date
🌙 农历转公历 | Lunar to Solar Conversion
📅 Public endpoint - No API key required
**Request Body:**
```json
{
"lunar_year": 2025,
"lunar_month": 1,
"lunar_day": 1,
"is_leap_month": false
}
```
**Response:**
```json
{
"success": true,
"solar_date": "2025-01-29",
"lunar_date": "2025-01-01",
"solar_date_string": "2025年1月29日",
"lunar_date_string": "2025年正月初一",
"weekday": "星期三",
"ganzhi": "己巳年 丁丑月 甲子日",
"zodiac": "蛇"
}
```
**Example:**
```bash
curl -X POST "https://sisithefox-astrologybazi-api.hf.space/api/calendar/convert-lunar-to-solar" \\
-H "Content-Type: application/json" \\
-d '{"lunar_year": 2025, "lunar_month": 1, "lunar_day": 1, "is_leap_month": false}'
```
"""
try:
logger.info(f"🌙 Converting lunar date: {request.lunar_year}-{request.lunar_month}-{request.lunar_day} (leap: {request.is_leap_month})")
# Create Lunar object
# Note: For leap month, use negative month value
month_value = -request.lunar_month if request.is_leap_month else request.lunar_month
lunar = Lunar.fromYmd(request.lunar_year, month_value, request.lunar_day)
# Convert to Solar
solar = lunar.getSolar()
# Get calendar info for the converted solar date
target_date = date(solar.getYear(), solar.getMonth(), solar.getDay())
calendar_info = ChineseCalendarService.get_calendar_by_date(target_date)
# Build response
response = {
"success": True,
"solar_date": solar.toYmd(),
"lunar_date": f"{request.lunar_year}-{request.lunar_month:02d}-{request.lunar_day:02d}",
"solar_date_string": f"{solar.getYear()}年{solar.getMonth()}月{solar.getDay()}日",
"lunar_date_string": calendar_info.get("lunar", {}).get("date_string", ""),
"weekday": calendar_info.get("weekday", ""),
"ganzhi": calendar_info.get("ganzhi", {}).get("full", ""),
"zodiac": calendar_info.get("zodiac", {}).get("animal", ""),
"is_leap_month": request.is_leap_month
}
logger.info(f"✅ Conversion successful: {response['lunar_date']} -> {response['solar_date']}")
return response
except Exception as e:
logger.error(f"❌ Lunar to solar conversion failed: {e}")
raise HTTPException(
status_code=400,
detail=f"Invalid lunar date or conversion error: {str(e)}"
)
# ==================== Nine Flying Stars API ====================
class NineStarsRequest(BaseModel):
"""Request model for Nine Flying Stars analysis"""
date: str = Field(..., description="Target date in YYYY-MM-DD format", example="2025-12-11")
hour: int = Field(12, description="Hour of the day (0-23), default 12 (noon)", ge=0, le=23)
@app.post("/api/nine-stars/analyze")
def analyze_nine_stars(
req: NineStarsRequest,
api_key: str = Depends(verify_api_key)
):
"""
🌟 Calculate and analyze Nine Flying Stars (九宫飞星)
🔒 Protected endpoint - Requires API key
Based on traditional Xuan Kong Feng Shui (玄空风水) theory:
- 《钦定协纪辨方书》
- 《紫白赋》
- 《沈氏玄空学》
**Parameters:**
- `date`: Target date in YYYY-MM-DD format (e.g., "2025-12-11")
- `hour`: Hour of the day (0-23), default 12 (noon)
**Response:**
- Yearly/Monthly/Daily/Hourly flying stars (年月日时飞星)
- Nine palace distribution (九宫分布)
- Fortune analysis (吉凶分析)
- Lucky/Unlucky directions (吉凶方位)
**Example Response:**
```json
{
"success": true,
"data": {
"date": "2025-12-11",
"hour": 12,
"yearly_star": {
"number": 2,
"name": "二黑巨门星",
"fortune": "凶"
},
"monthly_star": {
"number": 8,
"name": "八白左辅星",
"fortune": "大吉"
},
"daily_star": {
"number": 6,
"name": "六白武曲星",
"fortune": "吉"
},
"hourly_star": {
"number": 3,
"name": "三碧禄存星",
"fortune": "凶"
},
"palace_distribution": {
"1": 6, "2": 7, "3": 8, "4": 9, "5": 1,
"6": 2, "7": 3, "8": 4, "9": 5
},
"palace_details": {...},
"fortune_analysis": {
"overall_score": 6.5,
"overall_fortune": "吉",
"description": "运势平稳向上,谨慎行事可获成功。",
"lucky_directions": ["西北", "南", "东"],
"avoid_directions": ["西南", "西", "东北"]
}
}
}
```
"""
try:
logger.info(f"🌟 Analyzing Nine Flying Stars for: {req.date} at hour {req.hour}")
# Parse and validate date
try:
target_date = datetime.strptime(req.date, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(
status_code=400,
detail="Invalid date format. Use YYYY-MM-DD (e.g., '2025-12-11')"
)
# Validate hour
if not 0 <= req.hour <= 23:
raise HTTPException(
status_code=400,
detail="Invalid hour. Must be between 0 and 23"
)
# Calculate Nine Flying Stars
result = NineStarsCalculator.get_complete_analysis(target_date, req.hour)
logger.info(f"✅ Nine Stars analysis successful: {result['fortune_analysis']['overall_fortune']}")
return {
"success": True,
"data": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Nine Stars calculation error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Nine Stars analysis failed: {str(e)}")
@app.get("/api/nine-stars/today")
def get_today_nine_stars(
hour: int = 12,
api_key: str = Depends(verify_api_key)
):
"""
🌟 Get today's Nine Flying Stars analysis
🔒 Protected endpoint - Requires API key
**Query Parameters:**
- `hour`: Hour of the day (0-23), default 12 (noon)
**Returns:** Same as `/api/nine-stars/analyze` but for today's date
"""
try:
today = datetime.now().date()
logger.info(f"🌟 Fetching today's Nine Flying Stars at hour {hour}")
result = NineStarsCalculator.get_complete_analysis(today, hour)
logger.info(f"✅ Today's Nine Stars retrieved: {result['fortune_analysis']['overall_fortune']}")
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"❌ Error getting today's Nine Stars: {e}")
raise HTTPException(status_code=500, detail=f"Nine Stars service error: {str(e)}")
# ==================== 28 Lunar Mansions API ====================
class Constellation28Request(BaseModel):
"""Request model for 28 Constellations analysis"""
date: str = Field(..., description="Target date in YYYY-MM-DD format", example="2025-12-11")
activities: List[str] = Field(
default=[],
description="Activities to analyze (e.g., ['嫁娶', '开市', '安葬'])",
example=["嫁娶", "开市"]
)
@app.post("/api/28-constellations/analyze")
def analyze_28_constellations(
req: Constellation28Request,
api_key: str = Depends(verify_api_key)
):
"""
⭐ Analyze 28 Lunar Mansions (二十八星宿) for date selection
🔒 Protected endpoint - Requires API key
Based on traditional Chinese astronomy and almanac selection:
- 《钦定协纪辨方书》
- 《星宿择日法》
**Parameters:**
- `date`: Target date in YYYY-MM-DD format
- `activities`: List of activities to check (optional)
**Response:**
- Current constellation (当日星宿)
- Four Symbols classification (四象)
- Fortune level (吉凶等级 1-5)
- Suitable/Avoid activities (宜忌)
- Activity-specific recommendations (择日建议)
**Example Response:**
```json
{
"success": true,
"data": {
"date": "2025-12-11",
"constellation": {
"name": "角",
"pinyin": "Jiao",
"symbol": "东方青龙",
"animal": "蛟龙",
"element": "木",
"fortune": "吉",
"level": 4,
"description": "龙角,主功名、文书、升迁"
},
"suitable_activities": ["祭祀", "嫁娶", "纳采", "开市"],
"avoid_activities": ["安葬", "动土"],
"fortune_analysis": {
"recommendation": "今日角宿值日,大吉之日",
"activity_analysis": [
{
"activity": "嫁娶",
"verdict": "大吉",
"reason": "角宿宜嫁娶"
}
]
}
}
}
```
"""
try:
logger.info(f"⭐ Analyzing 28 Constellations for: {req.date}")
# Parse date
try:
target_date = datetime.strptime(req.date, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(
status_code=400,
detail="Invalid date format. Use YYYY-MM-DD"
)
# Get constellation for the date
constellation_data = Constellation28Calculator.get_constellation(target_date)
# Prepare response
result = {
"date": str(target_date),
"constellation": constellation_data,
"suitable_activities": constellation_data['suitable'],
"avoid_activities": constellation_data['avoid']
}
# Add fortune analysis if activities provided
if req.activities:
analysis = Constellation28Calculator.get_fortune_analysis(
target_date,
req.activities
)
result['fortune_analysis'] = analysis
logger.info(f"✅ 28 Constellations analysis successful: {constellation_data['name']}宿")
return {
"success": True,
"data": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ 28 Constellations error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"28 Constellations analysis failed: {str(e)}")
@app.get("/api/28-constellations/today")
def get_today_constellation(api_key: str = Depends(verify_api_key)):
"""
⭐ Get today's 28 Lunar Mansions constellation
🔒 Protected endpoint - Requires API key
**Returns:** Today's constellation info with suitable/avoid activities
"""
try:
today = datetime.now().date()
logger.info(f"⭐ Fetching today's constellation")
constellation_data = Constellation28Calculator.get_constellation(today)
result = {
"date": str(today),
"constellation": constellation_data,
"suitable_activities": constellation_data['suitable'],
"avoid_activities": constellation_data['avoid']
}
logger.info(f"✅ Today's constellation: {constellation_data['name']}宿")
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"❌ Error getting today's constellation: {e}")
raise HTTPException(status_code=500, detail=f"Constellation service error: {str(e)}")
@app.post("/api/28-constellations/find-auspicious")
def find_auspicious_dates(
activity: str,
start_date: str,
end_date: str,
min_level: int = 3,
api_key: str = Depends(verify_api_key)
):
"""
⭐ Find auspicious dates for specific activity (寻找吉日)
🔒 Protected endpoint - Requires API key
**Query Parameters:**
- `activity`: Activity name (e.g., "嫁娶", "开市", "安葬")
- `start_date`: Start date (YYYY-MM-DD)
- `end_date`: End date (YYYY-MM-DD)
- `min_level`: Minimum fortune level (1-5), default 3
**Returns:** List of auspicious dates ranked by fortune level
**Example:**
```
POST /api/28-constellations/find-auspicious
{
"activity": "嫁娶",
"start_date": "2025-12-11",
"end_date": "2026-01-11",
"min_level": 4
}
```
"""
try:
logger.info(f"⭐ Finding auspicious dates for: {activity}")
# Parse dates
try:
start = datetime.strptime(start_date, "%Y-%m-%d").date()
end = datetime.strptime(end_date, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(
status_code=400,
detail="Invalid date format. Use YYYY-MM-DD"
)
# Validate date range
if end <= start:
raise HTTPException(
status_code=400,
detail="end_date must be after start_date"
)
if (end - start).days > 365:
raise HTTPException(
status_code=400,
detail="Date range cannot exceed 1 year"
)
# Find auspicious dates
auspicious_dates = Constellation28Calculator.find_auspicious_dates(
start, end, activity, min_level
)
logger.info(f"✅ Found {len(auspicious_dates)} auspicious dates")
return {
"success": True,
"data": {
"activity": activity,
"date_range": {
"start": str(start),
"end": str(end)
},
"min_level": min_level,
"found_count": len(auspicious_dates),
"auspicious_dates": auspicious_dates[:30] # Limit to 30 results
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error finding auspicious dates: {e}")
raise HTTPException(status_code=500, detail=f"Auspicious date search failed: {str(e)}")
# ========================================
# 🐉 Zodiac Fortune Endpoints (生肖运程)
# ========================================
from zodiac_fortune_service import ZodiacFortuneService
@app.get("/api/zodiac-fortune/today/{zodiac}")
def get_today_zodiac_fortune(zodiac: str):
"""
🐉 Get today's fortune for a specific zodiac (今日生肖运程)
**Path Parameters:**
- `zodiac`: Chinese zodiac animal (鼠/牛/虎/兔/龙/蛇/马/羊/猴/鸡/狗/猪)
**Returns:** Complete fortune analysis for today
**Example:**
```bash
curl https://sisithefox-astrologybazi-api.hf.space/api/zodiac-fortune/today/鼠
```
**Response:**
```json
{
"success": true,
"date": "2025-12-11",
"zodiac": "鼠",
"overall": {
"score": 9,
"description": "今日与值日地支相合,贵人相助,万事顺遂"
},
"career": {"score": 9, "advice": "贵人相助,可开展新业务"},
"wealth": {"score": 8, "advice": "财运亨通,可适当投资"},
"love": {"score": 9, "advice": "桃花运旺,单身者可主动出击"},
"health": {"score": 8, "advice": "精力充沛,身体健康"},
"lucky_hours": [...],
"lucky_color": ["黑色", "蓝色"],
"lucky_number": [1, 2]
}
```
"""
try:
logger.info(f"🐉 Getting today's fortune for zodiac: {zodiac}")
today = datetime.now().date()
fortune = ZodiacFortuneService.generate_daily_fortune(zodiac, today)
return fortune
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Error getting zodiac fortune: {e}")
raise HTTPException(status_code=500, detail=f"Zodiac fortune service error: {str(e)}")
@app.get("/api/zodiac-fortune/{zodiac}/{date_str}")
def get_zodiac_fortune_by_date(zodiac: str, date_str: str):
"""
🐉 Get fortune for a specific zodiac on a specific date (指定日期生肖运程)
**Path Parameters:**
- `zodiac`: Chinese zodiac animal (鼠/牛/虎/兔/龙/蛇/马/羊/猴/鸡/狗/猪)
- `date_str`: Date in YYYY-MM-DD format
**Returns:** Complete fortune analysis for the specified date
**Example:**
```bash
curl https://sisithefox-astrologybazi-api.hf.space/api/zodiac-fortune/龙/2026-01-01
```
**Response:** Same format as `/api/zodiac-fortune/today/{zodiac}`
"""
try:
# The generic two-segment route is registered before the historical
# all-zodiac route. Delegate here so /all/today and /all/YYYY-MM-DD
# cannot be mistaken for a single zodiac named "all".
if zodiac == 'all':
return get_all_zodiac_fortune(date_str)
logger.info(f"🐉 Getting fortune for {zodiac} on {date_str}")
# Parse date
try:
target_date = datetime.strptime(date_str, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(
status_code=400,
detail="Invalid date format. Use YYYY-MM-DD"
)
fortune = ZodiacFortuneService.generate_daily_fortune(zodiac, target_date)
return fortune
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error getting zodiac fortune: {e}")
raise HTTPException(status_code=500, detail=f"Zodiac fortune service error: {str(e)}")
@app.get("/api/zodiac-fortune/all/{date_str}")
def get_all_zodiac_fortune(date_str: str):
"""
🐉 Get fortune for ALL 12 zodiacs on a specific date (12生肖运程)
**Path Parameters:**
- `date_str`: Date in YYYY-MM-DD format or "today"
**Returns:** Fortune for all 12 Chinese zodiacs
**Example:**
```bash
curl https://sisithefox-astrologybazi-api.hf.space/api/zodiac-fortune/all/2026-01-01
curl https://sisithefox-astrologybazi-api.hf.space/api/zodiac-fortune/all/today
```
**Response:**
```json
{
"success": true,
"date": "2026-01-01",
"fortunes": {
"鼠": {...},
"牛": {...},
...
}
}
```
"""
try:
logger.info(f"🐉 Getting fortune for all zodiacs on {date_str}")
# Parse date
if date_str.lower() == "today":
target_date = datetime.now().date()
else:
try:
target_date = datetime.strptime(date_str, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(
status_code=400,
detail="Invalid date format. Use YYYY-MM-DD or 'today'"
)
# Generate fortune for all 12 zodiacs
zodiacs = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪']
fortunes = {}
for zodiac in zodiacs:
try:
fortunes[zodiac] = ZodiacFortuneService.generate_daily_fortune(zodiac, target_date)
except Exception as e:
logger.error(f"Error generating fortune for {zodiac}: {e}")
fortunes[zodiac] = {"success": False, "error": str(e)}
return {
"success": True,
"date": str(target_date),
"fortunes": fortunes
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error getting all zodiac fortunes: {e}")
raise HTTPException(status_code=500, detail=f"Zodiac fortune service error: {str(e)}")
# ==================== 八字合婚 API ====================
class MarriageCompatibilityRequest(BaseModel):
"""Request model for marriage compatibility analysis (八字合婚)"""
male_birth_date: str = Field(
...,
description="Male birth date in YYYY-MM-DD format",
example="1990-01-15"
)
male_birth_time: str = Field(
...,
description="Male birth time in HH:MM format",
example="08:30"
)
male_is_lunar: bool = Field(
default=False,
description="True if male birth date is lunar calendar"
)
female_birth_date: str = Field(
...,
description="Female birth date in YYYY-MM-DD format",
example="1992-06-20"
)
female_birth_time: str = Field(
...,
description="Female birth time in HH:MM format",
example="14:00"
)
female_is_lunar: bool = Field(
default=False,
description="True if female birth date is lunar calendar"
)
@app.post("/api/marriage-compatibility")
@app.post("/marriage-compatibility")
def analyze_marriage_compatibility(
req: MarriageCompatibilityRequest,
api_key: str = Depends(verify_api_key)
):
"""
💍 八字合婚分析 (Marriage Compatibility Analysis)
基于《男女能結合的奧祕》五要则评分系统:
1. 八字相似 (25%): 两人八字中相同、相生、相合之字
2. 禍福相同 (30%): 夫妻命运走向是否一致
3. 子女同步 (15%): 头胎子女性别是否一致
4. 年支同氣 (15%): 年支是否同为一方之气
5. 日干舒配 (15%): 日干五行配合
🔒 Protected endpoint - Requires API key
**Request Body:**
- `male_birth_date`: 男方出生日期 (YYYY-MM-DD)
- `male_birth_time`: 男方出生时间 (HH:MM)
- `female_birth_date`: 女方出生日期 (YYYY-MM-DD)
- `female_birth_time`: 女方出生时间 (HH:MM)
**Returns:**
- 总分 (0-100)
- 评级 (天作之合/上等婚配/中上婚配/中等婚配/中下婚配/下等婚配/不宜婚配)
- 五要则详细分析
- 冲克分析
- 合婚建议
**Example:**
```json
{
"male_birth_date": "1990-01-15",
"male_birth_time": "08:30",
"female_birth_date": "1992-06-20",
"female_birth_time": "14:00"
}
```
"""
try:
logger.info(f"💍 Marriage compatibility analysis: Male={req.male_birth_date} Female={req.female_birth_date}")
# 计算男方八字
male_bazi = BaZiCalculator.calculate_bazi(
req.male_birth_date,
req.male_birth_time,
gender='male',
is_lunar=req.male_is_lunar
)
# 计算女方八字
female_bazi = BaZiCalculator.calculate_bazi(
req.female_birth_date,
req.female_birth_time,
gender='female',
is_lunar=req.female_is_lunar
)
# 合婚分析
analyzer = MarriageCompatibilityAnalyzer(male_bazi, female_bazi)
result = analyzer.analyze_compatibility()
# 添加八字信息到结果
result['male_bazi'] = {
'birth_date': req.male_birth_date,
'birth_time': req.male_birth_time,
'bazi_chart': male_bazi.get('bazi_chart', {})
}
result['female_bazi'] = {
'birth_date': req.female_birth_date,
'birth_time': req.female_birth_time,
'bazi_chart': female_bazi.get('bazi_chart', {})
}
logger.info(f"✅ Marriage compatibility analysis complete: {result['total_score']}分 ({result['rating']})")
return {
"success": True,
"data": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Marriage compatibility error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Marriage compatibility analysis failed: {str(e)}")
# ==================== Comprehensive Fortune Score API ====================
class FortuneScoreRequest(BaseModel):
"""Request model for comprehensive fortune score (0-120 scoring system)"""
birth_date: str = Field(
...,
description="Birth date in YYYY-MM-DD format",
example="1998-03-15"
)
birth_time: str = Field(
...,
description="Birth time in HH:MM format",
example="10:30"
)
gender: str = Field(
...,
description="Gender: 'male' or 'female'",
example="female"
)
is_lunar: bool = Field(
default=False,
description="True if birth date is lunar calendar"
)
target_date: Optional[str] = Field(
default=None,
description="Target date for fortune analysis in YYYY-MM-DD format (default: today)",
example="2026-06-15"
)
include_western: bool = Field(
default=True,
description="Include Western astrology (Saturn/Jupiter Return) in scoring"
)
@app.post("/api/fortune-score")
@app.post("/fortune-score")
def calculate_comprehensive_fortune_score(
req: FortuneScoreRequest,
api_key: str = Depends(verify_api_key)
):
"""
🔮 综合运势评分 (Comprehensive Fortune Score)
算法融合东西方命理系统:
- 东方 (70%): 八字身财关系 + 大运成败 + 流年吉凶 + 十神组合
- 西方 (30%): 土星回归 + 木星回归 + 行星过境 + 星体力量
评分体系:
- 0-120分综合评分
- 5级评级:excellent (90+), good (70-89), average (50-69), challenging (30-49), difficult (<30)
特色功能:
- ✨ 木星回归(12年周期):机会期加分
- ⚠️ 土星回归(29.5年周期):压力期减分
- 🎯 十神组合检测:10种关键吉凶组合
- 📊 精确身财关系:4步量化法(0-607分)
"""
try:
logger.info(f"📊 Fortune score request: {req.birth_date} {req.birth_time} ({req.gender})")
# 1. Calculate BaZi
bazi_result = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
num_dayun=12
)
# 2. Analyze Genju (格局)
genju_analyzer = GenjuAnalyzer(bazi_result)
genju_analysis = genju_analyzer.analyze_genju()
# 3. Analyze Dayun (大运)
dayun_analyzer = DayunAnalyzer(bazi_result, genju_analysis)
dayun_analysis = dayun_analyzer.analyze_all_dayun()
# 4. Calculate comprehensive fortune score
calculator = FortuneScoreCalculator(
bazi_chart=bazi_result,
genju_analysis=genju_analysis,
dayun_analysis=dayun_analysis
)
# Parse target date
if req.target_date:
target = datetime.strptime(req.target_date, '%Y-%m-%d').date()
else:
target = datetime.now().date()
# Calculate score
result = calculator.calculate_comprehensive_score(
target_date=target,
include_western=req.include_western
)
logger.info(f"✅ Fortune score complete: {result['total_score']}/120 ({result['rating']})")
return {
"success": True,
"data": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Fortune score error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Fortune score calculation failed: {str(e)}")
# ============================================================================
# 🌟 RITUALS API - Energy Rituals & Spiritual Practices (2026-01-25)
# ============================================================================
class RitualSearchRequest(BaseModel):
"""仪式搜索请求"""
query: Optional[str] = Field(None, description="关键词搜索")
category: Optional[List[str]] = Field(None, description="类别筛选")
difficulty: Optional[int] = Field(None, ge=1, le=5, description="难度筛选 (1-5)")
ritual_type: Optional[str] = Field(None, description="类型筛选")
max_time: Optional[int] = Field(None, description="最大时长(分钟)")
include_premium: Optional[bool] = Field(True, description="是否包含Premium内容")
class RitualRecommendRequest(BaseModel):
"""仪式推荐请求"""
user_need: str = Field(..., description="用户需求")
limit: Optional[int] = Field(5, ge=1, le=20, description="返回数量")
birth_date: Optional[str] = Field(None, description="出生日期(可选,用于更精准推荐)")
birth_time: Optional[str] = Field(None, description="出生时间")
gender: Optional[str] = Field(None, description="性别")
@app.get("/rituals")
@app.get("/api/rituals") # 备用路由
def get_all_rituals(api_key: str = Depends(verify_api_key)):
"""
获取所有能量仪式
Usage:
GET /rituals
Headers: X-API-Key: your_api_key
Returns:
{
"success": true,
"data": {
"rituals": [...],
"total": 100,
"categories": [...],
"statistics": {...}
}
}
"""
try:
logger.info("🌟 Fetching all rituals")
rituals = rituals_service.get_all_rituals()
logger.info(f"✅ Retrieved {len(rituals)} rituals")
return {
"success": True,
"data": {
"rituals": rituals,
"total": len(rituals),
"categories": rituals_service.get_categories(),
"statistics": rituals_service.get_statistics()
}
}
except Exception as e:
logger.error(f"❌ Get all rituals error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to fetch rituals: {str(e)}")
@app.get("/rituals/statistics")
@app.get("/api/rituals/statistics") # 备用路由
def get_rituals_statistics(api_key: str = Depends(verify_api_key)):
"""
获取仪式统计信息
Usage:
GET /rituals/statistics
Headers: X-API-Key: your_api_key
Returns:
{
"success": true,
"data": {
"total_rituals": 100,
"by_category": {...},
"by_difficulty": {...},
"by_type": {...},
"average_duration": 30,
"premium_count": 20,
"free_count": 80
}
}
"""
try:
logger.info("🌟 Fetching rituals statistics")
stats = rituals_service.get_statistics()
logger.info(f"✅ Retrieved statistics for {stats['totalRituals']} rituals")
return {
"success": True,
"data": stats
}
except Exception as e:
logger.error(f"❌ Get statistics error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to fetch statistics: {str(e)}")
# NOTE: /rituals/statistics is registered above this dynamic route on purpose —
# FastAPI matches routes in registration order, and /rituals/{ritual_id} would
# otherwise greedily swallow "statistics" as a ritual_id and 404.
@app.get("/rituals/{ritual_id}")
@app.get("/api/rituals/{ritual_id}") # 备用路由
def get_ritual_by_id(ritual_id: str, api_key: str = Depends(verify_api_key)):
"""
获取单个仪式详情
Usage:
GET /rituals/new_moon_abundance
Headers: X-API-Key: your_api_key
Parameters:
ritual_id: 仪式ID
Returns:
{
"success": true,
"data": {
"ritual": {...},
"related_rituals": [...]
}
}
"""
try:
logger.info(f"🌟 Fetching ritual: {ritual_id}")
ritual = rituals_service.get_ritual_by_id(ritual_id)
if not ritual:
raise HTTPException(status_code=404, detail=f"Ritual not found: {ritual_id}")
# 获取相关仪式
related = rituals_service.get_related_rituals(ritual_id, limit=5)
logger.info(f"✅ Retrieved ritual: {ritual.get('title_en')}")
return {
"success": True,
"data": {
"ritual": ritual,
"related_rituals": related
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Get ritual error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to fetch ritual: {str(e)}")
@app.post("/rituals/search")
@app.post("/api/rituals/search") # 备用路由
def search_rituals(request: RitualSearchRequest, api_key: str = Depends(verify_api_key)):
"""
搜索仪式
Usage:
POST /rituals/search
Headers: X-API-Key: your_api_key
Body: {
"query": "abundance",
"category": ["prosperity", "healing"],
"difficulty": 3,
"max_time": 30,
"include_premium": true
}
Parameters:
query: 关键词搜索(可选)
category: 类别筛选(可选)
difficulty: 难度筛选 1-5(可选)
ritual_type: 类型筛选(可选)
max_time: 最大时长分钟数(可选)
include_premium: 是否包含Premium内容(默认true)
Returns:
{
"success": true,
"data": {
"rituals": [...],
"total": 25,
"filters_applied": {...}
}
}
"""
try:
logger.info(f"🌟 Searching rituals with query: {request.query}")
rituals = rituals_service.search_rituals(
query=request.query,
category=request.category,
difficulty=request.difficulty,
ritual_type=request.ritual_type,
max_time=request.max_time,
include_premium=request.include_premium
)
logger.info(f"✅ Found {len(rituals)} matching rituals")
return {
"success": True,
"data": {
"rituals": rituals,
"total": len(rituals),
"filters_applied": {
"query": request.query,
"category": request.category,
"difficulty": request.difficulty,
"ritual_type": request.ritual_type,
"max_time": request.max_time,
"include_premium": request.include_premium
}
}
}
except Exception as e:
logger.error(f"❌ Search rituals error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to search rituals: {str(e)}")
@app.get("/rituals/category/{category}")
@app.get("/api/rituals/category/{category}") # 备用路由
def get_rituals_by_category(category: str, api_key: str = Depends(verify_api_key)):
"""
按类别获取仪式
Usage:
GET /rituals/category/prosperity
Headers: X-API-Key: your_api_key
Parameters:
category: 类别名称(prosperity, healing, protection, love, career, spiritual_growth)
Returns:
{
"success": true,
"data": {
"category": "prosperity",
"rituals": [...],
"total": 15
}
}
"""
try:
logger.info(f"🌟 Fetching rituals for category: {category}")
rituals = rituals_service.search_rituals(category=[category])
logger.info(f"✅ Found {len(rituals)} rituals in category: {category}")
return {
"success": True,
"data": {
"category": category,
"rituals": rituals,
"total": len(rituals)
}
}
except Exception as e:
logger.error(f"❌ Get category rituals error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to fetch category rituals: {str(e)}")
@app.get("/rituals/moon-phase/current")
@app.get("/api/rituals/moon-phase/current") # 备用路由
def get_current_moon_phase_rituals(api_key: str = Depends(verify_api_key)):
"""
获取当前月相适合的仪式
Usage:
GET /rituals/moon-phase/current
Headers: X-API-Key: your_api_key
Returns:
{
"success": true,
"data": {
"moon_phase": "waxing_crescent",
"rituals": [...],
"total": 8,
"description": "适合显化和吸引的时期"
}
}
"""
try:
logger.info("🌟 Fetching current moon phase rituals")
result = rituals_service.get_current_moon_phase_rituals()
logger.info(f"✅ Retrieved moon phase rituals: {result['currentPhase']}")
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"❌ Get moon phase rituals error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to fetch moon phase rituals: {str(e)}")
@app.post("/rituals/recommend")
@app.post("/api/rituals/recommend") # 备用路由
def recommend_rituals(request: RitualRecommendRequest, api_key: str = Depends(verify_api_key)):
"""
个性化仪式推荐
Usage:
POST /rituals/recommend
Headers: X-API-Key: your_api_key
Body: {
"user_need": "I need more abundance in my life",
"limit": 5,
"birth_date": "1990-05-15",
"birth_time": "14:30",
"gender": "female"
}
Parameters:
user_need: 用户需求描述(必填)
limit: 返回数量 1-20(默认5)
birth_date: 出生日期(可选,用于更精准推荐)
birth_time: 出生时间(可选)
gender: 性别(可选)
Returns:
{
"success": true,
"data": {
"recommendations": [...],
"total": 5,
"user_need": "...",
"personalized": true
}
}
"""
try:
logger.info(f"🌟 Recommending rituals for need: {request.user_need}")
recommendations = rituals_service.recommend_rituals(
user_need=request.user_need,
limit=request.limit,
birth_date=request.birth_date,
birth_time=request.birth_time,
gender=request.gender
)
is_personalized = bool(request.birth_date)
logger.info(f"✅ Generated {len(recommendations)} recommendations (personalized: {is_personalized})")
return {
"success": True,
"data": {
"recommendations": recommendations,
"total": len(recommendations),
"user_need": request.user_need,
"personalized": is_personalized
}
}
except Exception as e:
logger.error(f"❌ Recommend rituals error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to recommend rituals: {str(e)}")
@app.get("/rituals/related/{ritual_id}")
@app.get("/api/rituals/related/{ritual_id}") # 备用路由
def get_related_rituals(ritual_id: str, limit: int = 5, api_key: str = Depends(verify_api_key)):
"""
获取相关仪式
Usage:
GET /rituals/related/new_moon_abundance?limit=5
Headers: X-API-Key: your_api_key
Parameters:
ritual_id: 仪式ID
limit: 返回数量(默认5)
Returns:
{
"success": true,
"data": {
"ritual_id": "new_moon_abundance",
"related_rituals": [...],
"total": 5
}
}
"""
try:
logger.info(f"🌟 Fetching related rituals for: {ritual_id}")
related = rituals_service.get_related_rituals(ritual_id, limit=limit)
logger.info(f"✅ Found {len(related)} related rituals")
return {
"success": True,
"data": {
"ritual_id": ritual_id,
"related_rituals": related,
"total": len(related)
}
}
except Exception as e:
logger.error(f"❌ Get related rituals error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Failed to fetch related rituals: {str(e)}")
# ============================================================================
# 🖐️ BODY READING API - Five Elements Body/Hand/Face Prediction (2026-02-16)
# ============================================================================
class BodyReadingRequest(BaseModel):
"""体相分析请求"""
birth_date: str = Field(..., description="出生日期 YYYY-MM-DD", example="1990-01-15")
birth_time: str = Field(default="12:00", description="出生时间 HH:MM", example="08:30")
gender: str = Field(default="male", description="性别 male/female")
is_lunar: bool = Field(default=False, description="是否农历")
@app.post("/analyze-body-reading")
@app.post("/api/analyze-body-reading")
def analyze_body_reading_endpoint(
req: BodyReadingRequest,
api_key: str = Depends(verify_api_key)
):
"""
🖐️ 五行体相预测 (Five Elements Body Reading Prediction)
基于BaZi五行规则系统,预测手型/面相/体型/性格/健康
纯规则匹配(免费),不调用AI
用途:作为手相/面相付费分析的免费钩子
- 免费:五行体相预测(本API)
- 付费:AI实拍掌纹/面相分析(vision AI)
"""
try:
logger.info(f"🖐️ Body reading request: {req.birth_date} {req.birth_time} ({req.gender})")
# 1. Calculate BaZi
bazi_result = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
num_dayun=3
)
# 2. Analyze body reading
result = analyze_body_reading(bazi_result)
# 3. Add BaZi summary info
chart = bazi_result.get('bazi_chart', {})
result['bazi_summary'] = {
'four_pillars': {
'year': {'stem': chart.get('year', {}).get('gan', ''), 'branch': chart.get('year', {}).get('zhi', '')},
'month': {'stem': chart.get('month', {}).get('gan', ''), 'branch': chart.get('month', {}).get('zhi', '')},
'day': {'stem': chart.get('day', {}).get('gan', ''), 'branch': chart.get('day', {}).get('zhi', '')},
'hour': {'stem': chart.get('hour', {}).get('gan', ''), 'branch': chart.get('hour', {}).get('zhi', '')},
},
'day_master': bazi_result.get('day_master', {}),
}
logger.info(f"✅ Body reading complete: dominant={result['dominant_element']}")
return {
"success": True,
"data": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Body reading error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Body reading analysis failed: {str(e)}")
# ============================================================================
# 💕 LOVE CLARITY - Marriage Destiny Analysis (2026-02-28)
# ============================================================================
class MarriageDestinyRequest(BaseModel):
"""Love Clarity 婚姻命运分析请求"""
birth_date: str = Field(
...,
description="Birth date in YYYY-MM-DD format",
example="1992-08-15"
)
birth_time: str = Field(
...,
description="Birth time in HH:MM format",
example="14:30"
)
gender: str = Field(
default="female",
description="Gender: 'male' or 'female'",
example="female"
)
is_lunar: bool = Field(
default=False,
description="True if birth date is lunar calendar"
)
relationship_status: Optional[str] = Field(
default=None,
description="Current relationship status for context",
example="cold_war"
)
children_status: Optional[str] = Field(
default=None,
description="Children status for enhanced analysis",
example="teenager"
)
@app.post("/analyze-marriage-destiny")
@app.post("/api/analyze-marriage-destiny")
def analyze_marriage_destiny(
req: MarriageDestinyRequest,
api_key: str = Depends(verify_api_key)
):
"""
💕 Love & Marriage Clarity 婚姻命运深度分析
为 Love Clarity 产品 ($39/$99) 提供规则化命理数据,包含:
1. 日刃格检测 — 6个极端风险日柱
2. 增强桃花分析 — 5种类型(正桃花/野桃花/桃花劫/桃花煞/滚浪桃花)
3. 暗克分析 — 婚姻宫隐性冲克
4. 子女星分析 — 食伤星对婚姻影响
5. 2026流年影响 — 丙午年婚姻冲击
6. 冷读钩子 — 3个情绪共鸣锚点
+ 完整的传统婚姻分析(配偶星、婚姻宫、100分评分、女命规则等)
🔒 Protected endpoint - Requires API key
"""
try:
logger.info(f"💕 Marriage destiny analysis: {req.birth_date} {req.birth_time} ({req.gender})")
# 1. Calculate complete BaZi with dayun
bazi_result = BaZiCalculator.calculate_bazi_complete(
birth_date=req.birth_date,
birth_time=req.birth_time,
gender=req.gender,
is_lunar=req.is_lunar,
num_dayun=8 # 80年大运
)
# 2. Traditional marriage analysis
marriage_analyzer = MarriageAnalyzer(bazi_result)
traditional_analysis = marriage_analyzer.analyze_marriage()
# 3. Love Clarity enhanced analysis
clarity_analyzer = LoveClarityAnalyzer(bazi_result)
enhanced_analysis = clarity_analyzer.analyze_all()
# 4. BaZi chart summary
chart = bazi_result.get('bazi_chart', {})
bazi_summary = {
'four_pillars': {
'year': {'stem': chart.get('year', {}).get('gan', ''), 'branch': chart.get('year', {}).get('zhi', '')},
'month': {'stem': chart.get('month', {}).get('gan', ''), 'branch': chart.get('month', {}).get('zhi', '')},
'day': {'stem': chart.get('day', {}).get('gan', ''), 'branch': chart.get('day', {}).get('zhi', '')},
'hour': {'stem': chart.get('hour', {}).get('gan', ''), 'branch': chart.get('hour', {}).get('zhi', '')},
},
'day_master': bazi_result.get('day_master', {}),
'ten_gods': bazi_result.get('ten_gods', {}),
}
# 5. Combine all data
result = {
# Enhanced (new)
'day_blade_check': enhanced_analysis['day_blade_check'],
'peach_blossom_analysis': enhanced_analysis['peach_blossom_analysis'],
'hidden_clash_analysis': enhanced_analysis['hidden_clash_analysis'],
'children_star_analysis': enhanced_analysis['children_star_analysis'],
'annual_impact_2026': enhanced_analysis['annual_impact_2026'],
'cold_reading_hooks': enhanced_analysis['cold_reading_hooks'],
# Traditional
'spouse_star_analysis': traditional_analysis['spouse_star_analysis'],
'marriage_palace_analysis': traditional_analysis['marriage_palace_analysis'],
'spouse_characteristics': traditional_analysis['spouse_characteristics'],
'marriage_quality': traditional_analysis['marriage_quality'],
'marriage_palace_score': traditional_analysis['marriage_palace_score'],
'marriage_fortune_years': traditional_analysis['marriage_fortune_years'],
'marriage_advice': traditional_analysis['marriage_advice'],
'female_marriage_rules': traditional_analysis.get('female_marriage_rules', []),
'spouse_direction': traditional_analysis.get('spouse_direction', {}),
# Context
'bazi_summary': bazi_summary,
'user_context': {
'relationship_status': req.relationship_status,
'children_status': req.children_status,
}
}
logger.info(
f"✅ Marriage destiny complete: "
f"palace_score={traditional_analysis['marriage_palace_score'].get('score', 'N/A')}, "
f"day_blade={enhanced_analysis['day_blade_check']['detected']}, "
f"peach_blossom={enhanced_analysis['peach_blossom_analysis']['has_peach_blossom']}"
)
return {
"success": True,
"data": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Marriage destiny analysis error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Marriage destiny analysis failed: {str(e)}")
# ==================== Qimen Dunjia (奇门遁甲) ====================
class QimenChartRequest(BaseModel):
"""Request model for Qimen Dunjia chart calculation"""
date: str = Field(..., description="Date in YYYY-MM-DD format", example="2026-04-14")
hour: int = Field(..., description="Hour of the day (0-23)", ge=0, le=23, example=10)
minute: int = Field(0, description="Minute (0-59)", ge=0, le=59, example=30)
purpose: str = Field(
"综合",
description="Purpose: 事业/财运/感情/健康/学业/诉讼/出行/综合",
example="事业"
)
@app.post("/api/qimen/chart")
@app.post("/qimen-chart")
def calculate_qimen_chart(
req: QimenChartRequest,
api_key: str = Depends(verify_api_key)
):
"""
🌀 Calculate Qimen Dunjia Chart (奇门遁甲排盘)
🔒 Protected endpoint - Requires API key
Time-based Rotating Plate Method (时家转盘奇门) with:
- Complete nine-palace grid (九宫格) with earth/sky stems
- Nine Stars (九星), Eight Doors (八门), Eight Gods (八神)
- Void palaces (旬空), Post Horse Star (驿马)
- Zhi Fu (值符) and Zhi Shi (值使) positions
- Fortune analysis per palace and overall recommendations
**Parameters:**
- `date`: Solar calendar date (YYYY-MM-DD)
- `hour`: Hour (0-23)
- `minute`: Minute (0-59), default 0
- `purpose`: 事业/财运/感情/健康/学业/诉讼/出行/综合
"""
try:
logger.info(f"🌀 Calculating Qimen chart for: {req.date} {req.hour}:{req.minute:02d}, purpose={req.purpose}")
# Parse date
try:
parts = req.date.split("-")
year, month, day = int(parts[0]), int(parts[1]), int(parts[2])
except (ValueError, IndexError):
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
result = QimenCalculator.calculate(
year=year, month=month, day=day,
hour=req.hour, minute=req.minute,
purpose=req.purpose
)
logger.info(
f"✅ Qimen chart: {result['chart_info']['full_name']}, "
f"值符={result['zhi_fu']['star']}→{result['zhi_fu']['palace_name']}, "
f"overall={result['analysis']['overall_fortune']}"
)
return {
"success": True,
"data": result,
"timestamp": datetime.now().isoformat()
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Qimen calculation error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Qimen chart calculation failed: {str(e)}")
@app.get("/api/qimen/now")
@app.get("/qimen-now")
def get_qimen_now(
purpose: str = "综合",
api_key: str = Depends(verify_api_key)
):
"""
🌀 Get Qimen Dunjia chart for current moment (奇门遁甲 - 当前时刻)
🔒 Protected endpoint - Requires API key
**Query Parameters:**
- `purpose`: 事业/财运/感情/健康/学业/诉讼/出行/综合
**Returns:** Same as `/api/qimen/chart` but for the current moment
"""
try:
logger.info(f"🌀 Calculating Qimen chart for NOW, purpose={purpose}")
result = QimenCalculator.calculate_now(purpose=purpose)
logger.info(f"✅ Qimen NOW: {result['chart_info']['full_name']}, overall={result['analysis']['overall_fortune']}")
return {
"success": True,
"data": result,
"timestamp": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"❌ Qimen NOW error: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Qimen calculation failed: {str(e)}")
class LiuYaoRequest(BaseModel):
question: str = Field("", description="占卜问题", example="我的事业发展如何?")
purpose: str = Field("综合", description="Purpose: 财运/感情/事业/健康/学业/出行/综合", example="事业")
coins: Optional[List[int]] = Field(
None,
description="手动起卦:6个整数(每爻阳面铜钱数,由下到上),每个值0-3。不传则自动起卦",
example=[3, 2, 1, 2, 3, 1]
)
@field_validator("coins")
@classmethod
def validate_coins(cls, v: Optional[List[int]]) -> Optional[List[int]]:
if v is None:
return v
if len(v) != 6:
raise ValueError("coins must have exactly 6 elements (one per line, bottom to top)")
if not all(0 <= c <= 3 for c in v):
raise ValueError("each coin value must be 0–3 (count of yang-face coins)")
return v
def _liuyao_response(coins, question, purpose) -> dict:
result = calculate_liuyao(coins=coins, question=question, purpose=purpose)
logger.info(
f"✅ Liu Yao: 本卦={result['origin_hexagram']['name']}, "
f"变卦={result['variant_hexagram']['name'] if result['variant_hexagram'] else '无'}, "
f"动爻={result['changed_positions']}"
)
return {"success": True, "data": result, "timestamp": datetime.now().isoformat()}
@app.post("/api/liuyao/divine")
@app.post("/liuyao-divine")
def divine_liuyao(
req: LiuYaoRequest,
api_key: str = Depends(verify_api_key)
):
"""
🔮 Liu Yao (六爻) Divination Chart — 🔒 Requires API key
Returns full chart: 本卦/变卦, 六爻(六亲/六神/地支/五行), 世应, 动爻, 空亡, 卦身/伏神.
"""
try:
logger.info(f"🔮 Liu Yao: purpose={req.purpose}, manual_coins={req.coins is not None}")
return _liuyao_response(req.coins, req.question, req.purpose)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Liu Yao error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Liu Yao divination failed: {str(e)}")
@app.get("/api/liuyao/now")
@app.get("/liuyao-now")
def get_liuyao_now(
purpose: str = "综合",
question: str = "",
api_key: str = Depends(verify_api_key)
):
"""🔮 Liu Yao for current moment — 🔒 Requires API key"""
try:
logger.info(f"🔮 Liu Yao NOW: purpose={purpose}")
return _liuyao_response(None, question, purpose)
except Exception as e:
logger.error(f"❌ Liu Yao NOW error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Liu Yao calculation failed: {str(e)}")
# ==================== Fengshui (八宅法) ====================
from fengshui_calculator import FengshuiCalculator
_fengshui = FengshuiCalculator()
class FengshuiHomeRequest(BaseModel):
facing_direction: str = Field(..., description="大门朝向,如:南/北/东/西/东南/东北/西南/西北")
birth_year: Optional[int] = Field(None, description="主要居住者出生年份(可选)")
gender: Optional[str] = Field(None, description="性别:male/female 或 男/女(可选)")
class FengshuiMingGuaRequest(BaseModel):
birth_year: int = Field(..., description="出生年份(公历)")
gender: str = Field(..., description="性别:male/female 或 男/女")
@app.post("/api/fengshui/home-analysis")
@app.post("/fengshui-home-analysis")
def fengshui_home_analysis(
req: FengshuiHomeRequest,
api_key: str = Depends(verify_api_key)
):
"""🏠 八宅法风水分析:宅卦+八方吉凶+房间建议+命宅匹配 — 🔒 Requires API key"""
try:
logger.info(f"🏠 Fengshui: facing={req.facing_direction}, birth_year={req.birth_year}")
result = _fengshui.full_analysis(
facing_direction=req.facing_direction,
occupant_birth_year=req.birth_year,
occupant_gender=req.gender,
)
return {"success": True, "data": result}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Fengshui home analysis error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Fengshui analysis failed: {str(e)}")
@app.post("/api/fengshui/ming-gua")
@app.post("/fengshui-ming-gua")
def fengshui_ming_gua(
req: FengshuiMingGuaRequest,
api_key: str = Depends(verify_api_key)
):
"""🔮 计算个人命卦(八宅法)— 🔒 Requires API key"""
try:
ming = _fengshui.calculate_ming_gua(req.birth_year, req.gender)
personal_dirs = _fengshui.get_personal_auspicious_directions(ming['gua_num'])
return {"success": True, "data": {"ming_gua": ming, "personal_directions": personal_dirs}}
except Exception as e:
logger.error(f"❌ Ming gua error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Ming gua calculation failed: {str(e)}")
@app.get("/api/fengshui/zhai-gua/{facing}")
def fengshui_zhai_gua(
facing: str,
api_key: str = Depends(verify_api_key)
):
"""🏡 根据大门朝向查询宅卦+八方吉凶 — 🔒 Requires API key"""
try:
zhai = _fengshui.get_zhai_gua(facing)
directions = _fengshui.get_eight_directions_fortune(zhai['gua_num'])
rooms = _fengshui.get_room_recommendations(zhai['gua_num'])
return {"success": True, "data": {"zhai_gua": zhai, "eight_directions": directions, "room_recommendations": rooms}}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"❌ Zhai gua error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Zhai gua lookup failed: {str(e)}")
if __name__ == "__main__":
import uvicorn
import sys
# Check for port argument
port = 7860 # Default for Hugging Face Spaces
if len(sys.argv) > 1 and sys.argv[1] == "--port" and len(sys.argv) > 2:
port = int(sys.argv[2])
print(f"🚀 Starting server on port {port}")
uvicorn.run(app, host="0.0.0.0", port=port)
"""
🔮 AstrologyBazi API - Complete Eastern Mysticism Platform
BaZi (八字) | Nine Flying Stars (九宫飞星) | 28 Lunar Mansions (二十八星宿) | Chinese Calendar (农历)
Powering https://astrologybazi.com
🔒 Protected with API Key Authentication
"""
from birth_time_context import BirthTimeRequest
from fastapi import FastAPI, HTTPException, Header, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, field_validator
from typing import Optional, Dict, List, Any
import logging
import os
from datetime import date, datetime, timedelta
from dotenv import load_dotenv
# 🔧 Load environment variables from .env file
load_dotenv()
from bazi_calculator import BaZiCalculator
from lunar_python import Solar, Lunar
from nine_stars import NineStarsCalculator
from constellation_28 import Constellation28Calculator
from shensha_analyzer import ShenshaAnalyzer
from genju_analyzer import GenjuAnalyzer
from dayun_analyzer import DayunAnalyzer
from evidence import build_evidence, build_timing, build_meta, select_dayun_score_preview
from dayun_dynamic_assessment import build_dayun_dynamic_assessment
from ai_interpreter import AIInterpreter
from time_fortune_analyzer import TimeFortuneAnalyzer
from fortune_score_calculator import FortuneScoreCalculator
# 🎯 Phase 3 新增模块 (2025-12-24)
from marriage_analyzer import MarriageAnalyzer
from career_analyzer import CareerAnalyzer
from wealth_analyzer import WealthAnalyzer
from health_analyzer import HealthAnalyzer
from official_analyzer import OfficialAnalyzer # 官职分析
from legal_risk_analyzer import LegalRiskAnalyzer # 法律风险分析
from marriage_compatibility_analyzer import MarriageCompatibilityAnalyzer # 八字合婚
# 🌟 Rituals Service (2026-01-25)
from rituals_service import rituals_service
# 🖐️ Body Reading Analyzer (2026-02-16)
from body_reading_analyzer import analyze_body_reading
# 💕 Love Clarity Analyzer (2026-02-28)
from love_clarity_analyzer import LoveClarityAnalyzer
# 🌀 Qimen Dunjia Calculator (2026-04-14)
from qimen_calculator import QimenCalculator
# 🔮 Liu Yao Calculator (2026-04-17)
from liuyao_calculator import calculate_liuyao
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 🔐 API Key Configuration
# Support both naming conventions:
# - API_KEY: For HuggingFace Space settings (simple, since only one API key)
# - SISI_HF_API_KEY: For Cloudflare (clear distinction among many API keys)
# Fail closed: no hardcoded fallback — if neither env var is set, all
# protected endpoints refuse service instead of accepting a default key.
API_KEY = os.environ.get("API_KEY") or os.environ.get("SISI_HF_API_KEY")
if not API_KEY:
logger.error(
"🔒 API key is NOT configured (set API_KEY or SISI_HF_API_KEY). "
"All protected endpoints will return 503 until a key is set."
)
def verify_api_key(x_api_key: Optional[str] = Header(None)):
"""Verify API key from request header (fail closed when unconfigured)"""
if not API_KEY:
raise HTTPException(
status_code=503,
detail="🔒 API key is not configured on the server. Service unavailable."
)
if x_api_key != API_KEY:
raise HTTPException(
status_code=403,
detail="🔒 Invalid or missing API key. Please contact SisiTheFox for access."
)
return x_api_key
# ==================== Chinese Calendar Service ====================
class ChineseCalendarService:
"""Service for Chinese lunar calendar operations"""
# 地支到生肖的映射
ZHI_TO_ZODIAC = {
'子': '鼠', '丑': '牛', '寅': '虎', '卯': '兔',
'辰': '龙', '巳': '蛇', '午': '马', '未': '羊',
'申': '猴', '酉': '鸡', '戌': '狗', '亥': '猪'