diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/app.py b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/app.py new file mode 100644 index 00000000..9e4c28ce --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/app.py @@ -0,0 +1,1286 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +通書 Tong Shu — Almanach Chinois Génératif +Application Streamlit par CyberMind.FR / Gandalf des Conjureurs +""" + +import streamlit as st +import cnlunar +from datetime import datetime, date, timedelta +import json +import sys, os + +# Import Wu Yun Liu Qi module (alongside this script) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +try: + import wuyun_liuqi as wyql +except ImportError: + wyql = None + +# ───────────────────────────────────────────────────── +# CONFIGURATION & CONSTANTS +# ───────────────────────────────────────────────────── + +HEAVENLY_STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"] +EARTHLY_BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"] +ZODIAC_ANIMALS = ["鼠 Rat", "牛 Bœuf", "虎 Tigre", "兔 Lapin", "龍 Dragon", "蛇 Serpent", + "馬 Cheval", "羊 Chèvre", "猴 Singe", "雞 Coq", "狗 Chien", "豬 Cochon"] +ZODIAC_EMOJI = ["🐀", "🐂", "🐅", "🐇", "🐉", "🐍", "🐴", "🐐", "🐒", "🐓", "🐕", "🐷"] + +ELEMENTS_CN = {"甲": "木", "乙": "木", "丙": "火", "丁": "火", "戊": "土", "己": "土", + "庚": "金", "辛": "金", "壬": "水", "癸": "水"} +ELEMENTS_FR = {"木": "Bois 🌳", "火": "Feu 🔥", "土": "Terre 🏔️", "金": "Métal ⚔️", "水": "Eau 💧"} +ELEMENTS_COLOR = {"木": "#2d8a4e", "火": "#c0392b", "土": "#b8860b", "金": "#95a5a6", "水": "#2471a3"} + +YINYANG = {"甲": "Yang", "乙": "Yin", "丙": "Yang", "丁": "Yin", "戊": "Yang", "己": "Yin", + "庚": "Yang", "辛": "Yin", "壬": "Yang", "癸": "Yin"} + +STEMS_FR = {"甲": "Jiǎ", "乙": "Yǐ", "丙": "Bǐng", "丁": "Dīng", "戊": "Wù", + "己": "Jǐ", "庚": "Gēng", "辛": "Xīn", "壬": "Rén", "癸": "Guǐ"} +BRANCHES_FR = {"子": "Zǐ", "丑": "Chǒu", "寅": "Yín", "卯": "Mǎo", "辰": "Chén", + "巳": "Sì", "午": "Wǔ", "未": "Wèi", "申": "Shēn", "酉": "Yǒu", + "戌": "Xū", "亥": "Hài"} + +# 12 Day Officers with French translations and quality +DAY_OFFICERS = { + "建": {"fr": "Établir (Jiàn)", "quality": "⚖️ Modéré", "color": "#f39c12", "desc": "Jour d'établissement. Favorable aux initiatives nouvelles mais avec prudence."}, + "除": {"fr": "Éliminer (Chú)", "quality": "✅ Favorable", "color": "#27ae60", "desc": "Jour d'élimination. Excellent pour le nettoyage, les soins médicaux, la purification."}, + "满": {"fr": "Plénitude (Mǎn)", "quality": "✅ Favorable", "color": "#27ae60", "desc": "Jour de plénitude. Favorable aux récoltes, célébrations et accumulations."}, + "平": {"fr": "Équilibre (Píng)", "quality": "⚖️ Modéré", "color": "#f39c12", "desc": "Jour d'équilibre. Favorable aux activités ordinaires et à la médiation."}, + "定": {"fr": "Fixation (Dìng)", "quality": "✅ Favorable", "color": "#27ae60", "desc": "Jour de fixation. Favorable aux engagements, contrats, installations."}, + "执": {"fr": "Saisir (Zhí)", "quality": "⚖️ Modéré", "color": "#f39c12", "desc": "Jour de prise en main. Favorable à la discipline et aux procédures légales."}, + "破": {"fr": "Briser (Pò)", "quality": "❌ Défavorable", "color": "#c0392b", "desc": "Jour de destruction. Favorable uniquement aux démolitions et ruptures volontaires."}, + "危": {"fr": "Danger (Wēi)", "quality": "❌ Défavorable", "color": "#c0392b", "desc": "Jour de danger. La prudence est de mise. Éviter les entreprises risquées."}, + "成": {"fr": "Accomplir (Chéng)", "quality": "✅ Favorable", "color": "#27ae60", "desc": "Jour d'accomplissement. Excellent pour conclure des affaires et célébrer."}, + "收": {"fr": "Récolter (Shōu)", "quality": "✅ Favorable", "color": "#27ae60", "desc": "Jour de récolte. Favorable aux encaissements, rentrées et conclusions."}, + "开": {"fr": "Ouvrir (Kāi)", "quality": "✅ Favorable", "color": "#27ae60", "desc": "Jour d'ouverture. Excellent pour les inaugurations et nouveaux départs."}, + "闭": {"fr": "Fermer (Bì)", "quality": "❌ Défavorable", "color": "#c0392b", "desc": "Jour de fermeture. Favorable uniquement aux enterrements et clôtures."}, +} + +# 12 Day Gods +DAY_GODS = { + "青龙": {"fr": "Dragon Vert", "emoji": "🐲", "quality": "Auspicieux"}, + "明堂": {"fr": "Salle Lumineuse", "emoji": "🏛️", "quality": "Auspicieux"}, + "天刑": {"fr": "Châtiment Céleste", "emoji": "⚡", "quality": "Inauspicieux"}, + "朱雀": {"fr": "Oiseau Vermillon", "emoji": "🐦", "quality": "Inauspicieux"}, + "金匮": {"fr": "Coffre d'Or", "emoji": "📦", "quality": "Auspicieux"}, + "天德": {"fr": "Vertu Céleste", "emoji": "🌟", "quality": "Auspicieux"}, + "白虎": {"fr": "Tigre Blanc", "emoji": "🐯", "quality": "Inauspicieux"}, + "玉堂": {"fr": "Salle de Jade", "emoji": "💎", "quality": "Auspicieux"}, + "天牢": {"fr": "Prison Céleste", "emoji": "🔒", "quality": "Inauspicieux"}, + "玄武": {"fr": "Tortue Noire", "emoji": "🐢", "quality": "Inauspicieux"}, + "司命": {"fr": "Maître du Destin", "emoji": "📜", "quality": "Auspicieux"}, + "勾陈": {"fr": "Crochet de la Constellation", "emoji": "⭐", "quality": "Inauspicieux"}, +} + +# Activities translations (Chinese to French) +ACTIVITIES_FR = { + "沐浴": "🛁 Bain / Purification", + "剃头": "✂️ Coupe de cheveux", + "经络": "💆 Acupuncture / Méridiens", + "纳财": "💰 Encaisser de l'argent", + "扫舍宇": "🧹 Nettoyer la maison", + "伐木": "🪓 Couper du bois", + "畋猎": "🏹 Chasser", + "牧养": "🐄 Élevage", + "选将": "⚔️ Choisir des officiers", + "开仓": "📦 Ouvrir l'entrepôt", + "纳畜": "🐴 Acheter du bétail", + "安抚边境": "🏰 Sécuriser les frontières", + "酝酿": "🍷 Brasser / Fermenter", + "整手足甲": "💅 Manucure / Pédicure", + "整容": "🪞 Soins esthétiques", + "出行": "🚶 Voyager", + "结婚姻": "💍 Fiançailles", + "宴会": "🎉 Banquet / Réception", + "嫁娶": "👰 Mariage", + "修造": "🔨 Rénovation / Construction", + "上官": "🏛️ Prise de fonction", + "进人口": "👶 Adoption", + "修置产室": "🏠 Aménager une propriété", + "开渠": "💧 Creuser un canal", + "穿井": "⛏️ Creuser un puits", + "安碓硙": "⚙️ Installer des machines", + "平治道涂": "🛤️ Travaux routiers", + "破屋坏垣": "🏚️ Démolition", + "营建": "🏗️ Construction majeure", + "筑堤防": "🌊 Construire des digues", + "搬移": "📦 Déménagement", + "缮城郭": "🏯 Réparer fortifications", + "修仓库": "🏪 Réparer entrepôt", + "修饰垣墙": "🧱 Réparer les murs", + "求嗣": "👶 Prier pour descendance", + "修宫室": "🏛️ Réparer palais/maison", + "庆赐": "🎁 Célébrer / Offrir", + "补垣": "🧱 Réparer les clôtures", + "纳采": "💐 Demande en mariage", + "入学": "📚 Entrer à l'école", + "开市": "🏪 Ouvrir un commerce", + "立券交易": "📝 Signer des contrats", + "裁制": "✂️ Couture / Confection", + "祈福": "🙏 Prier pour bénédictions", + "入宅": "🏡 Emménager", + "安床": "🛏️ Installer le lit", + "求医疗病": "🏥 Consulter un médecin", + "上表章": "📋 Soumettre requête", + "冠带": "👑 Cérémonie de passage", + "栽种": "🌱 Planter", + "破土": "⛏️ Terrassement", + "安葬": "⚰️ Enterrement", + "启攒": "🪦 Exhumation", + "解除": "🔓 Lever restrictions", + "鼓铸": "🔔 Fondre des métaux", + "出师": "🎓 Partir en campagne", + "上册": "📖 Enregistrement officiel", + "远回": "🔙 Retour de voyage", + "宣政事": "📣 Annonces officielles", + "塞穴": "🕳️ Boucher des trous", + "颁诏": "📜 Promulguer décrets", + "招贤": "🤝 Recruter des talents", + "恤孤茕": "🤲 Aider les orphelins", + "布政事": "📋 Administrer", + "雪冤": "⚖️ Réhabiliter injustice", + "临政": "🏛️ Gouverner", + "覃恩": "🕊️ Accorder le pardon", + "竖柱上梁": "🏗️ Lever charpente", +} + +# Solar terms with French +SOLAR_TERMS_FR = { + "立春": "🌱 Lì Chūn — Début du Printemps", + "雨水": "🌧️ Yǔ Shuǐ — Eau de Pluie", + "惊蛰": "🐛 Jīng Zhé — Éveil des Insectes", + "春分": "🌸 Chūn Fēn — Équinoxe de Printemps", + "清明": "🍃 Qīng Míng — Pure Clarté", + "谷雨": "🌾 Gǔ Yǔ — Pluie des Grains", + "立夏": "☀️ Lì Xià — Début de l'Été", + "小满": "🌿 Xiǎo Mǎn — Petite Abondance", + "芒种": "🌾 Máng Zhǒng — Grains en Épi", + "夏至": "🌞 Xià Zhì — Solstice d'Été", + "小暑": "🌡️ Xiǎo Shǔ — Petite Chaleur", + "大暑": "🔥 Dà Shǔ — Grande Chaleur", + "立秋": "🍂 Lì Qiū — Début de l'Automne", + "处暑": "🌅 Chǔ Shǔ — Fin de Chaleur", + "白露": "💧 Bái Lù — Rosée Blanche", + "秋分": "🍁 Qiū Fēn — Équinoxe d'Automne", + "寒露": "🥶 Hán Lù — Rosée Froide", + "霜降": "❄️ Shuāng Jiàng — Descente du Givre", + "立冬": "🌨️ Lì Dōng — Début de l'Hiver", + "小雪": "🌨️ Xiǎo Xuě — Petite Neige", + "大雪": "❄️ Dà Xuě — Grande Neige", + "冬至": "🌑 Dōng Zhì — Solstice d'Hiver", + "小寒": "🧊 Xiǎo Hán — Petit Froid", + "大寒": "🏔️ Dà Hán — Grand Froid", +} + +# Shichen (2-hour periods) names +SHICHEN_NAMES = [ + ("子時 Zǐ", "23:00–01:00", "🐀"), + ("丑時 Chǒu", "01:00–03:00", "🐂"), + ("寅時 Yín", "03:00–05:00", "🐅"), + ("卯時 Mǎo", "05:00–07:00", "🐇"), + ("辰時 Chén", "07:00–09:00", "🐉"), + ("巳時 Sì", "09:00–11:00", "🐍"), + ("午時 Wǔ", "11:00–13:00", "🐴"), + ("未時 Wèi", "13:00–15:00", "🐐"), + ("申時 Shēn", "15:00–17:00", "🐒"), + ("酉時 Yǒu", "17:00–19:00", "🐓"), + ("戌時 Xū", "19:00–21:00", "🐕"), + ("亥時 Hài", "21:00–23:00", "🐷"), + ("子時 Zǐ (2e)", "23:00–01:00", "🐀"), +] + +MOON_PHASES_FR = { + "朔": "🌑 Nouvelle Lune (Shuò)", + "上弦": "🌓 Premier Quartier (Shàng Xián)", + "望": "🌕 Pleine Lune (Wàng)", + "下弦": "🌗 Dernier Quartier (Xià Xián)", +} + +STAR_ZODIAC_FR = { + "水瓶座": "♒ Verseau", + "双鱼座": "♓ Poissons", + "白羊座": "♈ Bélier", + "金牛座": "♉ Taureau", + "双子座": "♊ Gémeaux", + "巨蟹座": "♋ Cancer", + "狮子座": "♌ Lion", + "处女座": "♍ Vierge", + "天秤座": "♎ Balance", + "天蝎座": "♏ Scorpion", + "射手座": "♐ Sagittaire", + "摩羯座": "♑ Capricorne", +} + +QUALITY_LEVEL_FR = { + -1: ("⬛ Sans qualité particulière", "#7f8c8d"), + 0: ("🟢 Supérieur — Le bon l'emporte sur le mauvais", "#27ae60"), + 1: ("🟡 Favorable — Le bon contre le mauvais", "#f1c40f"), + 2: ("🟠 Moyen — Le bon ne compense pas le mauvais", "#e67e22"), + 3: ("🔴 Inférieur — Le mauvais domine", "#c0392b"), +} + +# Chinese zodiac single characters to French with emoji +ZODIAC_CN_FR = { + "鼠": "🐀 Rat", "子": "🐀 Rat", + "牛": "🐂 Bœuf", "丑": "🐂 Bœuf", + "虎": "🐅 Tigre", "寅": "🐅 Tigre", + "兔": "🐇 Lapin", "卯": "🐇 Lapin", + "龙": "🐉 Dragon", "龍": "🐉 Dragon", "辰": "🐉 Dragon", + "蛇": "🐍 Serpent", "巳": "🐍 Serpent", + "马": "🐴 Cheval", "馬": "🐴 Cheval", "午": "🐴 Cheval", + "羊": "🐐 Chèvre", "未": "🐐 Chèvre", + "猴": "🐒 Singe", "申": "🐒 Singe", + "鸡": "🐓 Coq", "雞": "🐓 Coq", "酉": "🐓 Coq", + "狗": "🐕 Chien", "戌": "🐕 Chien", + "猪": "🐷 Cochon", "豬": "🐷 Cochon", "亥": "🐷 Cochon", +} + +def translate_zodiac(cn_text): + """Translate Chinese zodiac characters to French with emoji.""" + if not cn_text: + return cn_text + result = cn_text + for cn, fr in ZODIAC_CN_FR.items(): + result = result.replace(cn, fr) + return result + +def translate_zodiac_list(cn_list): + """Translate a list of Chinese zodiac characters to French.""" + return [translate_zodiac(item) for item in cn_list] + + +# ───────────────────────────────────────────────────── +# HELPER FUNCTIONS +# ───────────────────────────────────────────────────── + +def get_element_info(stem): + """Get element name, color and yin/yang from a Heavenly Stem.""" + elem = ELEMENTS_CN.get(stem, "?") + return elem, ELEMENTS_FR.get(elem, elem), ELEMENTS_COLOR.get(elem, "#333"), YINYANG.get(stem, "?") + + +def get_branch_animal(branch): + """Get zodiac animal from Earthly Branch.""" + idx = EARTHLY_BRANCHES.index(branch) if branch in EARTHLY_BRANCHES else -1 + if idx >= 0: + return ZODIAC_ANIMALS[idx], ZODIAC_EMOJI[idx] + return "?", "?" + + +def format_pillar(chars): + """Format a BaZi pillar with element info.""" + if len(chars) >= 2: + stem, branch = chars[0], chars[1] + elem_cn, elem_fr, color, yy = get_element_info(stem) + animal, emoji = get_branch_animal(branch) + pinyin_s = STEMS_FR.get(stem, "") + pinyin_b = BRANCHES_FR.get(branch, "") + return { + "chars": chars, + "stem": stem, + "branch": branch, + "element_cn": elem_cn, + "element_fr": elem_fr, + "color": color, + "yinyang": yy, + "animal": animal, + "emoji": emoji, + "pinyin": f"{pinyin_s} {pinyin_b}" + } + return {"chars": chars, "stem": "?", "branch": "?", "element_cn": "?", + "element_fr": "?", "color": "#333", "yinyang": "?", "animal": "?", + "emoji": "?", "pinyin": "?"} + + +def translate_activity(act_cn): + """Translate a Chinese activity to French.""" + return ACTIVITIES_FR.get(act_cn, f"📌 {act_cn}") + + +def get_lunar_data(selected_date, hour=12): + """Get all Tong Shu data for a given date.""" + dt = datetime(selected_date.year, selected_date.month, selected_date.day, hour, 0) + lunar = cnlunar.Lunar(dt) + return lunar + + +def render_pillar_card(pillar_info, label_cn, label_fr): + """Render a single BaZi pillar as styled HTML.""" + p = pillar_info + return f""" +
+
{label_cn}
{label_fr}
+
+ {p['chars']} +
+
{p['pinyin']}
+
+ {p['element_fr']} +
+
{p['yinyang']}
+
{p['emoji']}
+
{p['animal']}
+
+ """ + + +# ───────────────────────────────────────────────────── +# STREAMLIT APP +# ───────────────────────────────────────────────────── + +st.set_page_config( + page_title="通書 Tong Shu — Almanach Chinois", + page_icon="📅", + layout="wide", + initial_sidebar_state="expanded" +) + +# Custom CSS +st.markdown(""" + +""", unsafe_allow_html=True) + + +# ─── SIDEBAR ────────────────────────────────────────── + +with st.sidebar: + st.markdown("## 📅 Sélection de la Date") + selected_date = st.date_input( + "Choisir une date", + value=date.today(), + min_value=date(1901, 1, 1), + max_value=date(2099, 12, 31), + format="DD/MM/YYYY" + ) + + st.markdown("---") + + # Quick navigation + st.markdown("### ⚡ Navigation Rapide") + col1, col2 = st.columns(2) + with col1: + if st.button("◀ Hier", use_container_width=True): + st.session_state['nav_date'] = selected_date - timedelta(days=1) + st.rerun() + with col2: + if st.button("Demain ▶", use_container_width=True): + st.session_state['nav_date'] = selected_date + timedelta(days=1) + st.rerun() + + if st.button("📌 Aujourd'hui", use_container_width=True): + st.session_state['nav_date'] = date.today() + st.rerun() + + st.markdown("---") + + # Week view + st.markdown("### 📆 Vue Semaine") + week_start = selected_date - timedelta(days=selected_date.weekday()) + for i in range(7): + d = week_start + timedelta(days=i) + try: + lunar_d = get_lunar_data(d) + officer = lunar_d.today12DayOfficer + officer_info = DAY_OFFICERS.get(officer, {}) + quality_icon = "🟢" if "Favorable" in officer_info.get("quality", "") else ( + "🔴" if "Défavorable" in officer_info.get("quality", "") else "🟡" + ) + label = f"{quality_icon} {d.strftime('%a %d/%m')} — {officer}" + is_today = d == selected_date + if st.button(label, key=f"week_{i}", use_container_width=True, + type="primary" if is_today else "secondary"): + st.session_state['nav_date'] = d + st.rerun() + except Exception: + st.button(f"📅 {d.strftime('%a %d/%m')}", key=f"week_{i}", + use_container_width=True, disabled=True) + + st.markdown("---") + st.markdown(""" +
+ 通書 Tong Shu v1.0
+ CyberMind.FR
+ 🧙‍♂️ Gandalf des Conjureurs +
+ """, unsafe_allow_html=True) + + +# Handle navigation state +if 'nav_date' in st.session_state: + selected_date = st.session_state.pop('nav_date') + +# ─── MAIN CONTENT ───────────────────────────────────── + +try: + lunar = get_lunar_data(selected_date) +except Exception as e: + st.error(f"Erreur de calcul pour cette date: {e}") + st.stop() + +# Title +st.markdown('
通書 Tong Shu
', unsafe_allow_html=True) +st.markdown('
Almanach Chinois Génératif — 中國曆法
', unsafe_allow_html=True) + +# ─── DATE BANNER ────────────────────────────────────── + +weekday_fr = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"] +month_fr = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", + "Août", "Septembre", "Octobre", "Novembre", "Décembre"] + +gregorian_str = f"{weekday_fr[selected_date.weekday()]} {selected_date.day} {month_fr[selected_date.month-1]} {selected_date.year}" +lunar_str = f"{lunar.lunarYearCn}年 {lunar.lunarMonthCn} {lunar.lunarDayCn}" +year_animal_idx = EARTHLY_BRANCHES.index(lunar.year8Char[1]) if lunar.year8Char[1] in EARTHLY_BRANCHES else 0 + +solar_term_today = "" +if lunar.todaySolarTerms and lunar.todaySolarTerms != "无": + solar_term_today = f"

🌿 {SOLAR_TERMS_FR.get(lunar.todaySolarTerms, lunar.todaySolarTerms)}

" + +st.markdown(f""" +
+

{gregorian_str}

+

{ZODIAC_EMOJI[year_animal_idx]} {lunar_str}

+

Année {lunar.year8Char} · Mois {lunar.month8Char} · Jour {lunar.day8Char}

+

{STAR_ZODIAC_FR.get(lunar.starZodiac, lunar.starZodiac)} · Saison : {lunar.lunarSeason}

+ {solar_term_today} +
+""", unsafe_allow_html=True) + + +# ─── DAY QUALITY & OFFICER ──────────────────────────── + +st.markdown('
🏷️ Qualité du Jour · 日質
', unsafe_allow_html=True) + +officer = lunar.today12DayOfficer +officer_info = DAY_OFFICERS.get(officer, {"fr": officer, "quality": "?", "color": "#666", "desc": ""}) +god12 = lunar.today12DayGod +god12_info = DAY_GODS.get(god12, {"fr": god12, "emoji": "⭐", "quality": "?"}) + +level_info = QUALITY_LEVEL_FR.get(lunar.todayLevel, ("?", "#666")) + +col1, col2, col3 = st.columns(3) + +with col1: + st.markdown(f""" +
+
Officier du Jour 日值
+
+ {officer} +
+
{officer_info['fr']}
+
+ {officer_info['quality']} +
+
{officer_info['desc']}
+
+ """, unsafe_allow_html=True) + +with col2: + st.markdown(f""" +
+
Divinité du Jour 神煞
+
+ {god12_info['emoji']} +
+
{god12} — {god12_info['fr']}
+
+ {god12_info['quality']} +
+
+ """, unsafe_allow_html=True) + +with col3: + st.markdown(f""" +
+
Constellation 宿
+
+ {lunar.today28Star} +
+
Niveau du jour
+
+ {level_info[0]} +
+
+ """, unsafe_allow_html=True) + + +# ─── FOUR PILLARS ───────────────────────────────────── + +st.markdown('
🏛️ Quatre Piliers (四柱 Sì Zhù)
', unsafe_allow_html=True) + +# Get current hour pillar +now = datetime.now() +current_hour_idx = ((now.hour + 1) % 24) // 2 +if now.hour == 23 or now.hour == 0: + current_hour_idx = 0 + +pillar_year = format_pillar(lunar.year8Char) +pillar_month = format_pillar(lunar.month8Char) +pillar_day = format_pillar(lunar.day8Char) +pillar_hour = format_pillar(lunar.twohour8Char) + +pillars_html = f""" +
+ {render_pillar_card(pillar_year, "年柱", "Année")} + {render_pillar_card(pillar_month, "月柱", "Mois")} + {render_pillar_card(pillar_day, "日柱", "Jour")} + {render_pillar_card(pillar_hour, "時柱", "Heure")} +
+""" +st.html(pillars_html) + + +# ─── CLASH & DIRECTIONS ────────────────────────────── + +st.markdown('
🧭 Conflits & Directions · 沖煞方位
', unsafe_allow_html=True) + +col1, col2 = st.columns(2) + +with col1: + st.markdown(f""" +
+

⚡ Conflit du Jour · 日沖

+

{translate_zodiac(lunar.chineseZodiacClash)}

+

+ 🐾 Animal en conflit : {translate_zodiac(lunar.zodiacLose)}
+ 🐾 Animal favorable : {translate_zodiac(lunar.zodiacWin)}
+ 🤝 Trinité (三合 Sān Hé) : {', '.join(translate_zodiac_list(lunar.zodiacMark3List))}
+ 🔗 Harmonie (六合 Liù Hé) : {translate_zodiac(lunar.zodiacMark6)} +

+
+ """, unsafe_allow_html=True) + +with col2: + # Wealth/Joy/Happiness god directions based on day stem + stem = lunar.day8Char[0] + # Direction mapping based on Day Stem (traditional) + joy_dirs = {"甲": "東北", "乙": "西北", "丙": "西南", "丁": "正南", "戊": "東南", + "己": "東北", "庚": "西北", "辛": "西南", "壬": "正南", "癸": "東南"} + wealth_dirs = {"甲": "東北", "乙": "正北", "丙": "正西", "丁": "西北", "戊": "正北", + "己": "正南", "庚": "東南", "辛": "正東", "壬": "正南", "癸": "東南"} + happiness_dirs = {"甲": "東北", "乙": "西北", "丙": "西南", "丁": "東南", "戊": "正北", + "己": "東南", "庚": "西南", "辛": "東北", "壬": "正北", "癸": "正南"} + + dir_fr = {"東北": "Nord-Est", "西北": "Nord-Ouest", "西南": "Sud-Ouest", "正南": "Sud", + "東南": "Sud-Est", "正北": "Nord", "正西": "Ouest", "正東": "Est"} + + joy_d = joy_dirs.get(stem, "?") + wealth_d = wealth_dirs.get(stem, "?") + happy_d = happiness_dirs.get(stem, "?") + + st.markdown(f""" +
+

🧭 Directions Propices · 吉方

+

+ 😊 Joie (喜神) : {joy_d} — {dir_fr.get(joy_d, joy_d)}
+ 🤑 Richesse (財神) : {wealth_d} — {dir_fr.get(wealth_d, wealth_d)}
+ 🙏 Bonheur (福神) : {happy_d} — {dir_fr.get(happy_d, happy_d)} +

+
+ """, unsafe_allow_html=True) + + +# ─── AUSPICIOUS & INAUSPICIOUS ──────────────────────── + +st.markdown('
📋 Activités du Jour · 宜忌
', unsafe_allow_html=True) + +col1, col2 = st.columns(2) + +with col1: + st.markdown("#### ✅ Activités Favorables (宜)") + good_things = lunar.goodThing or [] + if good_things: + for act in good_things: + translated = translate_activity(act) + st.markdown(f'
{translated}
', unsafe_allow_html=True) + else: + st.markdown('
Aucune activité particulièrement favorable
', + unsafe_allow_html=True) + +with col2: + st.markdown("#### ❌ Activités à Éviter (忌)") + bad_things = lunar.badThing or [] + if bad_things: + for act in bad_things: + translated = translate_activity(act) + st.markdown(f'
{translated}
', unsafe_allow_html=True) + else: + st.markdown('
Aucune restriction particulière
', + unsafe_allow_html=True) + + +# ─── GODS & SPIRITS ────────────────────────────────── + +st.markdown('
👼 Divinités & Esprits (神煞)
', unsafe_allow_html=True) + +col1, col2 = st.columns(2) + +with col1: + st.markdown("#### 🌟 Divinités Favorables (吉神)") + gods_html = "" + for god in (lunar.goodGodName or []): + gods_html += f'✨ {god}' + st.markdown(gods_html or "—", unsafe_allow_html=True) + +with col2: + st.markdown("#### 👹 Esprits Défavorables (凶神)") + demons_html = "" + for demon in (lunar.badGodName or []): + demons_html += f'⚠️ {demon}' + st.markdown(demons_html or "—", unsafe_allow_html=True) + + +# ─── HOURLY PILLARS (SHICHEN) ──────────────────────── + +st.markdown('
⏰ Heures Chinoises (時辰 Shí Chen)
', unsafe_allow_html=True) + +hour_pillars = lunar.twohour8CharList or [] +now_hour = datetime.now().hour +is_today = selected_date == date.today() + +table_html = '' +table_html += '' +table_html += '' + +for i, (name, period, emoji) in enumerate(SHICHEN_NAMES): + if i < len(hour_pillars): + p = format_pillar(hour_pillars[i]) + # Determine if this is the current shichen + is_current = False + if is_today: + if i == 0 and (now_hour == 23 or now_hour == 0): + is_current = True + elif i > 0 and i < 12: + start_hour = (2 * i - 1) + end_hour = start_hour + 2 + if start_hour <= now_hour < end_hour: + is_current = True + elif i == 12 and (now_hour == 23): + is_current = True + + row_class = 'class="shichen-current"' if is_current else '' + current_marker = " 👈 MAINTENANT" if is_current else "" + table_html += f'' + table_html += f'' + table_html += f'' + table_html += f'' + table_html += f'' + table_html += f'' + table_html += '' + +table_html += '
時辰PériodePilier 柱ÉlémentAnimal
{emoji} {name}{period}{p["chars"]}{p["element_fr"]}{p["emoji"]} {p["animal"]}{current_marker}
' +st.html(table_html) + + +# ─── SOLAR TERMS ────────────────────────────────────── + +st.markdown('
🌿 Termes Solaires (節氣 Jié Qì)
', unsafe_allow_html=True) + +# Next solar term +next_st = lunar.nextSolarTerm +next_st_date = lunar.nextSolarTermDate +next_st_year = lunar.nextSolarTermYear if hasattr(lunar, 'nextSolarTermYear') else selected_date.year + +if next_st and next_st != "无": + next_st_fr = SOLAR_TERMS_FR.get(next_st, next_st) + try: + next_date_str = f"{next_st_date[1]:02d}/{next_st_date[0]:02d}/{next_st_year}" + days_until = (date(next_st_year, next_st_date[0], next_st_date[1]) - selected_date).days + except Exception: + next_date_str = str(next_st_date) + days_until = "?" + + st.markdown(f""" +
+
{next_st_fr}
+
+ Prochain terme solaire : {next_date_str} + {"— dans " + str(days_until) + " jour(s)" if isinstance(days_until, int) and days_until > 0 else "— Aujourd'hui !" if days_until == 0 else ""} +
+
+ """, unsafe_allow_html=True) + +# Show all 24 solar terms for the year +with st.expander("📜 Voir les 24 Termes Solaires de l'année"): + terms_dict = lunar.thisYearSolarTermsDic or {} + cols = st.columns(4) + for idx, (term_cn, (m, d)) in enumerate(terms_dict.items()): + term_fr = SOLAR_TERMS_FR.get(term_cn, term_cn) + try: + term_date = date(selected_date.year, m, d) + is_past = term_date < selected_date + is_today_term = term_date == selected_date + style = "color:#27ae60; font-weight:bold;" if is_today_term else ( + "color:#999;" if is_past else "color:#333;" + ) + marker = " ◀ Aujourd'hui" if is_today_term else "" + except Exception: + style = "color:#333;" + marker = "" + + with cols[idx % 4]: + st.markdown(f'
{term_fr}
{d:02d}/{m:02d}{marker}
', + unsafe_allow_html=True) + + +# ─── ADDITIONAL INFO ────────────────────────────────── + +st.markdown('
📖 Informations Complémentaires
', unsafe_allow_html=True) + +col1, col2, col3 = st.columns(3) + +with col1: + moon_phase = lunar.phaseOfMoon if lunar.phaseOfMoon else "—" + moon_fr = MOON_PHASES_FR.get(moon_phase, f"🌙 {moon_phase}" if moon_phase != "—" else "🌙 —") + # Estimate moon phase from lunar day + lunar_day = lunar.lunarDay + if lunar_day == 1: + moon_estimate = "🌑 Nouvelle Lune" + elif lunar_day <= 7: + moon_estimate = "🌒 Croissant" + elif lunar_day <= 8: + moon_estimate = "🌓 Premier Quartier" + elif lunar_day <= 14: + moon_estimate = "🌔 Gibbeuse Croissante" + elif lunar_day == 15: + moon_estimate = "🌕 Pleine Lune" + elif lunar_day <= 22: + moon_estimate = "🌖 Gibbeuse Décroissante" + elif lunar_day <= 23: + moon_estimate = "🌗 Dernier Quartier" + else: + moon_estimate = "🌘 Décroissant" + + st.markdown(f""" +
+

🌙 Phase Lunaire

+

{moon_estimate}

+

Jour lunaire : {lunar.lunarDay}

+
+ """, unsafe_allow_html=True) + +with col2: + meridian = lunar.meridians if hasattr(lunar, 'meridians') and lunar.meridians else "—" + meridian_fr = { + "脾": "🟡 Rate (Pí) — Terre", + "肺": "⚪ Poumon (Fèi) — Métal", + "肾": "🔵 Rein (Shèn) — Eau", + "心包": "🔴 Péricarde (Xīn Bāo) — Feu", + "肝": "🟢 Foie (Gān) — Bois", + "心": "🔴 Cœur (Xīn) — Feu", + "小肠": "🔴 Intestin Grêle — Feu", + "膀胱": "🔵 Vessie — Eau", + "三焦": "🔴 Triple Réchauffeur — Feu", + "胆": "🟢 Vésicule Biliaire — Bois", + "胃": "🟡 Estomac (Wèi) — Terre", + "大肠": "⚪ Gros Intestin — Métal", + } + st.markdown(f""" +
+

🏥 Méridien du Jour

+

+ {meridian_fr.get(meridian, meridian)} +

+

+ Organe actif selon la MTC +

+
+ """, unsafe_allow_html=True) + +with col3: + east_zodiac = lunar.todayEastZodiac if hasattr(lunar, 'todayEastZodiac') else "—" + star_zodiac = STAR_ZODIAC_FR.get(lunar.starZodiac, lunar.starZodiac) if lunar.starZodiac else "—" + st.markdown(f""" +
+

⭐ Astrologie

+

+ 🌌 Zodiac Ouest : {star_zodiac}

+ 🐲 Année du : {ZODIAC_EMOJI[year_animal_idx]} {lunar.chineseYearZodiac}

+ 🌟 Demeure : {east_zodiac} +

+
+ """, unsafe_allow_html=True) + + +# ─── WU YUN LIU QI ────────────────────────────────── + +if wyql: + st.markdown('
🌀 五运六气 Wǔ Yùn Liù Qì — Cinq Mouvements & Six Qi
', + unsafe_allow_html=True) + + # Determine the Wu Yun Liu Qi year based on Li Chun boundary + # Li Chun is around Feb 3-5 each year; before it, use previous year + wyql_year = selected_date.year + # Approximate Li Chun detection from solar terms + try: + terms_dict = lunar.thisYearSolarTermsDic or {} + lichun_day = None + for term_cn, (m, d) in terms_dict.items(): + if "立春" in term_cn: + lichun_day = date(selected_date.year, m, d) + break + if lichun_day and selected_date < lichun_day: + wyql_year = selected_date.year - 1 + except Exception: + pass + + yi = wyql.get_year_info(wyql_year) + liu_bu = wyql.get_liu_bu(yi["branch"]) + + # Year classification badge + cls = yi["tong_hua"] or yi["yi_hua"] + cls_key = yi["tong_hua_key"] or yi["yi_hua_key"] or "—" + cls_type = "运气同化" if yi["is_tong_hua"] else "运气异化" + cls_type_fr = "Similitude" if yi["is_tong_hua"] else "Différence" + + yun_color = wyql.ELEMENTS_COLOR[yi["yun_element"]] + st_color = wyql.ELEMENTS_COLOR[yi["sitian_elem"]] + zq_color = wyql.ELEMENTS_COLOR[yi["zaiquan_elem"]] + + card_style = "background:rgba(40,40,50,0.6); border:1px solid rgba(255,107,107,0.3); border-radius:12px; padding:15px; margin:8px 0;" + + st.markdown(f""" +
+
+
岁运 Suì Yùn — Mouvement de l'Année
+
+ {wyql.ELEMENTS_EMOJI[yi['yun_element']]} {yi['yun_name_cn']} +
+
{yi['yun_name_fr']}
+
+ {yi['chars']} · {yi['pinyin']} · {yi['emoji']} {yi['animal']} +
+
+
+
司天 Sī Tiān — Gouverneur du Ciel
+
+ {wyql.ELEMENTS_EMOJI[yi['sitian_elem']]} {yi['sitian_qi']} +
+
{yi['sitian_fr']}
+
+ {wyql.SIX_QI[yi['sitian_qi']]['nature']} +
+
+
+
在泉 Zài Quán — Gouverneur de la Terre
+
+ {wyql.ELEMENTS_EMOJI[yi['zaiquan_elem']]} {yi['zaiquan_qi']} +
+
{yi['zaiquan_fr']}
+
+ {wyql.SIX_QI[yi['zaiquan_qi']]['nature']} +
+
+
+ """, unsafe_allow_html=True) + + # Classification badge + if cls: + st.markdown(f""" +
+
+ {cls['emoji']} +
+
{cls_type} · {cls_type_fr}
+
+ {cls['cn']} — {cls['fr']} +
+
+ {cls['short']} +
+
+ {cls['desc']} +
+
+ """, unsafe_allow_html=True) + + # Liu Bu — Six Steps + with st.expander("📊 六步 Liù Bù — Six Étapes du Qi Annuel"): + REL_LABELS = { + "same": ("≡ Même", "#9b59b6"), + "generates": ("→ Engendre", "#27ae60"), + "generated_by": ("← Engendré", "#2980b9"), + "controls": ("⊳ Domine", "#e67e22"), + "controlled_by": ("⊲ Dominé", "#c0392b"), + "neutral": ("— Neutre", "#999"), + } + + liu_bu_html = """ + + + + + + + + + + + + """ + + # Determine current step based on selected month + current_step = (selected_date.month - 1) // 2 # 0-5 + + for step in liu_bu: + is_current = (step["step"] - 1 == current_step) + bg = "rgba(255,107,107,0.15)" if is_current else ("rgba(60,60,70,0.5)" if step["step"] % 2 == 0 else "rgba(40,40,50,0.5)") + marker = "" + if step["is_sitian"]: + marker = '👑 司天' + elif step["is_zaiquan"]: + marker = '🌍 在泉' + + h_color = wyql.ELEMENTS_COLOR[step["host_elem"]] + g_color = wyql.ELEMENTS_COLOR[step["guest_elem"]] + rel_label, rel_color = REL_LABELS.get(step["relationship"], ("?", "#999")) + + current_marker = ' style="font-weight:bold;"' if is_current else "" + border = "border-left:3px solid #8b0000;" if is_current else "" + + liu_bu_html += f""" + + + + + + + + """ + + liu_bu_html += "
ÉtapePériode主气 Hôte客气 InvitéRelation
+ {step['step']} {marker} + {'
▸ actuel' if is_current else ''} +
+ {step['period_cn']}
+ {step['period_fr']} +
+ + {wyql.ELEMENTS_EMOJI[step['host_elem']]} {step['host_qi'][:4]} + + + + {wyql.ELEMENTS_EMOJI[step['guest_elem']]} {step['guest_qi'][:4]} + + + {rel_label} +
" + st.html(liu_bu_html) + + st.markdown(""" +
+ Lecture : Chaque année est divisée en 6 étapes (~2 mois). Le Qi « Hôte » (主气) est fixe, + le Qi « Invité » (客气) varie selon le 司天. Quand l'Invité domine l'Hôte = tensions climatiques et sanitaires. + Quand l'Invité engendre l'Hôte = période harmonieuse. +
+ """, unsafe_allow_html=True) + + + # Full 60 Jiazi Wheel + with st.expander("🔄 六十甲子 — Roue des 60 Jiazi & Classifications"): + all_60 = wyql.get_60_jiazi() + current_pos = wyql.year_to_position(wyql_year) + + # Render as HTML grid (avoid matplotlib CJK font issues) + wheel_html = '
' + + for j in all_60: + pos = j["position"] + is_current = (pos == current_pos) + + cls_info = j.get("tong_hua") or j.get("yi_hua") + if cls_info: + badge_color = cls_info["color"] + badge_short = cls_info["short"] + badge_emoji = cls_info["emoji"] + else: + badge_color = "#ccc" + badge_short = "—" + badge_emoji = "" + + yun_color = wyql.ELEMENTS_COLOR[j["yun_element"]] + border = f"3px solid #8b0000" if is_current else f"1px solid {badge_color}40" + bg = "rgba(255,107,107,0.15)" if is_current else "rgba(40,40,50,0.6)" + shadow = "box-shadow:0 0 8px rgba(139,0,0,0.3);" if is_current else "" + greg = wyql.get_gregorian_years(pos, start=1984, end=2103) + greg_str = ", ".join(str(y) for y in greg[:3]) + + wheel_html += f""" +
+
{pos}
+
{j['chars']}
+
{j['pinyin']}
+
{j['emoji']}
+
{badge_emoji} {badge_short}
+
{greg_str}
+
+ """ + + wheel_html += '
' + st.html(wheel_html) + + # Legend + legend_th = "  |  ".join( + f'{v["emoji"]} {v["short"]} {v["cn"]} — {v["fr"]}' + for v in wyql.TONG_HUA.values() + ) + legend_yh = "  |  ".join( + f'{v["emoji"]} {v["short"]} {v["cn"]} — {v["fr"]}' + for v in wyql.YI_HUA.values() + ) + legend_elem = "   ".join( + f'● {e} {wyql.ELEMENTS_FR[e]}' + for e in wyql.ELEMENTS + ) + + st.markdown(f""" +
+
+ 运气同化 Similitude : {legend_th}
+ 运气异化 Différence : {legend_yh}
+ 五行 Cinq Éléments : {legend_elem} +
+
+ """, unsafe_allow_html=True) + + # Summary stats + summary = wyql.get_summary() + st.markdown(f""" +
+ 运气同化 : {summary['tong_hua_total']} années (TYTF:{summary['tong_hua_counts'].get('TYTF',0)}, + TF:{summary['tong_hua_counts'].get('TF',0)}, SH:{summary['tong_hua_counts'].get('SH',0)}, + TTF:{summary['tong_hua_counts'].get('TTF',0)}, TSH:{summary['tong_hua_counts'].get('TSH',0)}) +  |  + 运气异化 : {summary['yi_hua_total']} années +  |  Total : 60/60 +
+ """, unsafe_allow_html=True) + + +# ─── MULTI-DAY VIEW ────────────────────────────────── + +st.markdown('
📊 Aperçu de la Semaine · 週覽
', unsafe_allow_html=True) + +week_data = [] +for i in range(-3, 4): + d = selected_date + timedelta(days=i) + try: + l = get_lunar_data(d) + off = l.today12DayOfficer + off_info = DAY_OFFICERS.get(off, {"quality": "?", "color": "#666"}) + level = l.todayLevel + level_info_d = QUALITY_LEVEL_FR.get(level, ("?", "#666")) + week_data.append({ + "date": d.strftime("%d/%m"), + "day": weekday_fr[d.weekday()][:3], + "lunar": f"{l.lunarMonthCn} {l.lunarDayCn}", + "pillar": l.day8Char, + "officer": off, + "quality": off_info["quality"], + "color": off_info["color"], + "is_selected": d == selected_date, + }) + except Exception: + pass + +if week_data: + week_html = '
' + for wd in week_data: + border = "3px solid #ff6b6b" if wd["is_selected"] else "1px solid rgba(255,107,107,0.3)" + bg = "rgba(255,107,107,0.15)" if wd["is_selected"] else "rgba(40,40,50,0.6)" + week_html += f""" +
+
{wd['day']}
+
{wd['date']}
+
{wd['lunar']}
+
{wd['pillar']}
+
{wd['officer']}
+
{wd['quality']}
+
+ """ + week_html += '
' + st.html(week_html) + + +# ─── FOOTER ─────────────────────────────────────────── + +st.markdown(f""" + +""", unsafe_allow_html=True) diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/requirements.txt b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/requirements.txt new file mode 100644 index 00000000..f5e2e3e9 --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/requirements.txt @@ -0,0 +1,2 @@ +streamlit>=1.30.0 +cnlunar>=0.3.0 diff --git a/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/wuyun_liuqi.py b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/wuyun_liuqi.py new file mode 100644 index 00000000..03e2cd5c --- /dev/null +++ b/package/secubox/secubox-app-streamlit/files/srv/streamlit/apps/wuyun_liuqi/wuyun_liuqi.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +五运六气 Wu Yun Liu Qi — Module de calcul +Cinq Mouvements et Six Qi · Classification des 60 Jiazi + +Classifications validated against traditional reference: +- 运气同化 (26 years): TYTF(4), TF(8), SH(2), TTF(6), TSH(6) +- 运气异化 (34 years): ShunHua(10), TianXing(7), XiaoNi(~10), BuHe(~7) +""" + +HEAVENLY_STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"] +EARTHLY_BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"] + +STEM_PINYIN = {"甲":"Jiǎ","乙":"Yǐ","丙":"Bǐng","丁":"Dīng","戊":"Wù", + "己":"Jǐ","庚":"Gēng","辛":"Xīn","壬":"Rén","癸":"Guǐ"} +BRANCH_PINYIN = {"子":"Zǐ","丑":"Chǒu","寅":"Yín","卯":"Mǎo","辰":"Chén", + "巳":"Sì","午":"Wǔ","未":"Wèi","申":"Shēn","酉":"Yǒu", + "戌":"Xū","亥":"Hài"} + +ZODIAC = {"子":"鼠 Rat","丑":"牛 Bœuf","寅":"虎 Tigre","卯":"兔 Lapin", + "辰":"龍 Dragon","巳":"蛇 Serpent","午":"馬 Cheval","未":"羊 Chèvre", + "申":"猴 Singe","酉":"雞 Coq","戌":"狗 Chien","亥":"豬 Cochon"} +ZODIAC_EMOJI = {"子":"🐀","丑":"🐂","寅":"🐅","卯":"🐇","辰":"🐉","巳":"🐍", + "午":"🐴","未":"🐐","申":"🐒","酉":"🐓","戌":"🐕","亥":"🐷"} + +ELEMENTS = ["木", "火", "土", "金", "水"] +ELEMENTS_FR = {"木":"Bois","火":"Feu","土":"Terre","金":"Métal","水":"Eau"} +ELEMENTS_EMOJI = {"木":"🌳","火":"🔥","土":"🏔️","金":"⚔️","水":"💧"} +ELEMENTS_COLOR = {"木":"#2d8a4e","火":"#c0392b","土":"#b8860b","金":"#7f8c8d","水":"#2471a3"} +ELEMENTS_COLOR_LIGHT = {"木":"#c8e6c9","火":"#ffcdd2","土":"#ffe0b2","金":"#e0e0e0","水":"#bbdefb"} + +STEM_TO_YUN = {"甲":"土","己":"土","乙":"金","庚":"金","丙":"水","辛":"水", + "丁":"木","壬":"木","戊":"火","癸":"火"} +STEM_POLARITY = {"甲":"yang","丙":"yang","戊":"yang","庚":"yang","壬":"yang", + "乙":"yin","丁":"yin","己":"yin","辛":"yin","癸":"yin"} + +SITIAN_QI = {"子":"少阴君火","午":"少阴君火","丑":"太阴湿土","未":"太阴湿土", + "寅":"少阳相火","申":"少阳相火","卯":"阳明燥金","酉":"阳明燥金", + "辰":"太阳寒水","戌":"太阳寒水","巳":"厥阴风木","亥":"厥阴风木"} +SITIAN_ELEM = {"子":"火","午":"火","丑":"土","未":"土","寅":"火","申":"火", + "卯":"金","酉":"金","辰":"水","戌":"水","巳":"木","亥":"木"} + +ZAIQUAN_QI = {"子":"阳明燥金","午":"阳明燥金","丑":"太阳寒水","未":"太阳寒水", + "寅":"厥阴风木","申":"厥阴风木","卯":"少阴君火","酉":"少阴君火", + "辰":"太阴湿土","戌":"太阴湿土","巳":"少阳相火","亥":"少阳相火"} +ZAIQUAN_ELEM = {"子":"金","午":"金","丑":"水","未":"水","寅":"木","申":"木", + "卯":"火","酉":"火","辰":"土","戌":"土","巳":"火","亥":"火"} + +BRANCH_ELEM = {"子":"水","丑":"土","寅":"木","卯":"木","辰":"土","巳":"火", + "午":"火","未":"土","申":"金","酉":"金","戌":"土","亥":"水"} + +SIX_QI = { + "厥阴风木": {"elem":"木","nature":"风 Vent","fr":"Jue Yin — Vent/Bois"}, + "少阴君火": {"elem":"火","nature":"热 Chaleur","fr":"Shao Yin — Feu Souverain"}, + "少阳相火": {"elem":"火","nature":"暑 Canicule","fr":"Shao Yang — Feu Ministériel"}, + "太阴湿土": {"elem":"土","nature":"湿 Humidité","fr":"Tai Yin — Humidité/Terre"}, + "阳明燥金": {"elem":"金","nature":"燥 Sécheresse","fr":"Yang Ming — Sécheresse/Métal"}, + "太阳寒水": {"elem":"水","nature":"寒 Froid","fr":"Tai Yang — Froid/Eau"}, +} + +GENERATES = {"木":"火","火":"土","土":"金","金":"水","水":"木"} +CONTROLS = {"木":"土","土":"水","水":"火","火":"金","金":"木"} + +TONG_HUA = { + "TYTF": {"cn":"太乙天符","fr":"Suprême Conformité au Ciel","short":"TYTF", + "color":"#9b59b6","emoji":"👑", + "desc":"Triple concordance suprême : Mouvement = Ciel = Branche. Énergie la plus puissante du cycle."}, + "TF": {"cn":"天符","fr":"Conformité au Ciel","short":"TF", + "color":"#e74c3c","emoji":"🔴", + "desc":"Le mouvement annuel et le Qi du Ciel partagent le même élément. Force amplifiée."}, + "SH": {"cn":"岁会","fr":"Réunion de l'Année","short":"SH", + "color":"#2980b9","emoji":"🔵", + "desc":"Le mouvement arrive à sa position naturelle (正位). Harmonie avec la Terre."}, + "TTF": {"cn":"同天符","fr":"Conformité Similaire au Ciel","short":"TTF", + "color":"#e67e22","emoji":"🟠", + "desc":"Le mouvement correspond au Qi de la Source (在泉). Écho par la Terre."}, + "TSH": {"cn":"同岁会","fr":"Conformité Similaire à la Réunion","short":"TSH", + "color":"#27ae60","emoji":"🟢", + "desc":"Le Qi du Ciel correspond à l'élément de la Branche Terrestre."}, +} + +YI_HUA = { + "ShunHua": {"cn":"顺化","fr":"Transformation Conforme","short":"SH", + "color":"#27ae60","emoji":"🌿", + "desc":"Le mouvement engendre le Qi du Ciel (运生气). Flux harmonieux."}, + "TianXing": {"cn":"天刑","fr":"Punition Céleste","short":"TX", + "color":"#c0392b","emoji":"⚡", + "desc":"Le Qi du Ciel domine le mouvement (气克运). Tension céleste."}, + "XiaoNi": {"cn":"小逆","fr":"Petit Contre-courant","short":"XN", + "color":"#f39c12","emoji":"🔄", + "desc":"Le mouvement domine le Qi du Ciel (运克气). Inversion légère."}, + "BuHe": {"cn":"不和","fr":"Dysharmonie","short":"BH", + "color":"#8e44ad","emoji":"💔", + "desc":"Le Qi du Ciel engendre le mouvement (气生运). Déséquilibre subtil."}, +} + + +def classify_jiazi(stem, branch): + """Classify a stem-branch pair according to Wu Yun Liu Qi.""" + yun = STEM_TO_YUN[stem] + pol = STEM_POLARITY[stem] + st = SITIAN_ELEM[branch] + zq = ZAIQUAN_ELEM[branch] + br = BRANCH_ELEM[branch] + + is_tf = (yun == st) + is_sh_broad = (yun == br) + is_sh_strict = (yun == "木" and branch == "卯") or (yun == "水" and branch == "子") + is_tytf = is_tf and is_sh_broad + + is_ttf_cand = (yun == zq) and not is_tf and not is_sh_broad + is_tsh_cand = (st == br) and branch in ["午","酉"] and not is_tf and not is_sh_broad + + is_ttf = is_ttf_cand and (pol == "yin" or is_tsh_cand) + is_tsh = is_tsh_cand and not is_ttf + + tong_hua = None + if is_tytf: tong_hua = "TYTF" + elif is_tf: tong_hua = "TF" + elif is_sh_strict: tong_hua = "SH" + elif is_ttf: tong_hua = "TTF" + elif is_tsh: tong_hua = "TSH" + + yi_hua = None + if tong_hua is None: + if GENERATES.get(yun) == st: yi_hua = "ShunHua" + elif CONTROLS.get(st) == yun: yi_hua = "TianXing" + elif CONTROLS.get(yun) == st: yi_hua = "XiaoNi" + elif GENERATES.get(st) == yun: yi_hua = "BuHe" + + return { + "stem": stem, "branch": branch, "chars": f"{stem}{branch}", + "pinyin": f"{STEM_PINYIN[stem]} {BRANCH_PINYIN[branch]}", + "animal": ZODIAC[branch], "emoji": ZODIAC_EMOJI[branch], + "yun_element": yun, "yun_fr": ELEMENTS_FR[yun], + "yun_excess": pol == "yang", + "yun_excess_cn": "太过" if pol == "yang" else "不及", + "yun_excess_fr": "Excès" if pol == "yang" else "Insuffisance", + "yun_name_cn": f"{yun}运{('太过' if pol == 'yang' else '不及')}", + "yun_name_fr": f"{ELEMENTS_FR[yun]} en {'Excès' if pol == 'yang' else 'Insuffisance'}", + "sitian_qi": SITIAN_QI[branch], "sitian_elem": st, + "sitian_fr": SIX_QI[SITIAN_QI[branch]]["fr"], + "zaiquan_qi": ZAIQUAN_QI[branch], "zaiquan_elem": zq, + "zaiquan_fr": SIX_QI[ZAIQUAN_QI[branch]]["fr"], + "branch_elem": br, + "tong_hua_key": tong_hua, + "tong_hua": TONG_HUA.get(tong_hua), + "yi_hua_key": yi_hua, + "yi_hua": YI_HUA.get(yi_hua), + "is_tong_hua": tong_hua is not None, + } + + +def get_60_jiazi(): + """Get classified data for all 60 Jiazi years.""" + return [ + {"position": i + 1, **classify_jiazi(HEAVENLY_STEMS[i % 10], EARTHLY_BRANCHES[i % 12])} + for i in range(60) + ] + + +def year_to_position(year): + """Convert Gregorian year to Jiazi position (1984 = position 1 = 甲子).""" + return ((year - 1984) % 60) + 1 + + +def get_year_info(year): + """Get complete Wu Yun Liu Qi info for a Gregorian year.""" + pos = year_to_position(year) + data = get_60_jiazi()[pos - 1] + data["gregorian_year"] = year + base = 1984 + (pos - 1) + all_y = set() + y = base + while y >= 1924: all_y.add(y); y -= 60 + y = base + 60 + while y <= 2103: all_y.add(y); y += 60 + data["all_years"] = sorted(all_y) + return data + + +def get_gregorian_years(position, start=1984, end=2103): + """Get Gregorian years for a Jiazi position.""" + base = 1984 + (position - 1) + years = [] + y = base + while y > start: y -= 60 + while y < start: y += 60 + while y <= end: + years.append(y) + y += 60 + return years + + +def get_liu_bu(branch): + """Get the Six Steps (六步) qi division for a year.""" + HOST = ["厥阴风木","少阴君火","少阳相火","太阴湿土","阳明燥金","太阳寒水"] + PERIODS_CN = ["大寒→春分","春分→小满","小满→大暑","大暑→秋分","秋分→小雪","小雪→大寒"] + PERIODS_FR = [ + "Grand Froid → Équinoxe Printemps", + "Équinoxe Printemps → Petit Plein", + "Petit Plein → Grande Chaleur", + "Grande Chaleur → Équinoxe Automne", + "Équinoxe Automne → Petite Neige", + "Petite Neige → Grand Froid", + ] + MONTHS = ["1-2","3-4","5-6","7-8","9-10","11-12"] + QI_ORDER = ["厥阴风木","少阴君火","太阴湿土","少阳相火","阳明燥金","太阳寒水"] + st_name = SITIAN_QI[branch] + st_idx = QI_ORDER.index(st_name) + + steps = [] + for i in range(6): + guest = QI_ORDER[(st_idx + i - 2) % 6] + h_elem = SIX_QI[HOST[i]]["elem"] + g_elem = SIX_QI[guest]["elem"] + rel = ("same" if h_elem == g_elem else + "generates" if GENERATES.get(g_elem) == h_elem else + "generated_by" if GENERATES.get(h_elem) == g_elem else + "controls" if CONTROLS.get(g_elem) == h_elem else + "controlled_by" if CONTROLS.get(h_elem) == g_elem else "neutral") + steps.append({ + "step": i + 1, "host_qi": HOST[i], "host_elem": h_elem, + "guest_qi": guest, "guest_elem": g_elem, + "period_cn": PERIODS_CN[i], "period_fr": PERIODS_FR[i], + "months": MONTHS[i], "relationship": rel, + "is_sitian": (i == 2), "is_zaiquan": (i == 5), + }) + return steps + + +def get_summary(): + """Get classification summary statistics.""" + from collections import Counter + all_data = get_60_jiazi() + th = Counter(d["tong_hua_key"] for d in all_data if d["tong_hua_key"]) + yh = Counter(d["yi_hua_key"] for d in all_data if d["yi_hua_key"]) + return { + "tong_hua_counts": dict(th), "yi_hua_counts": dict(yh), + "tong_hua_total": sum(th.values()), "yi_hua_total": sum(yh.values()), + "tong_hua_years": {k: [d for d in all_data if d["tong_hua_key"] == k] for k in TONG_HUA}, + "yi_hua_years": {k: [d for d in all_data if d["yi_hua_key"] == k] for k in YI_HUA}, + } + + +if __name__ == "__main__": + summary = get_summary() + print("运气同化:", summary["tong_hua_counts"], f"= {summary['tong_hua_total']}") + print("运气异化:", summary["yi_hua_counts"], f"= {summary['yi_hua_total']}") + info = get_year_info(2026) + print(f"\n2026: {info['chars']} {info['emoji']} — {info['yun_name_fr']}") + print(f" 司天: {info['sitian_qi']} ({info['sitian_fr']})") + print(f" 在泉: {info['zaiquan_qi']} ({info['zaiquan_fr']})") + cls = info['tong_hua'] or info['yi_hua'] + print(f" Classification: {cls['cn']} — {cls['fr']}")