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('', 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('', 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('', 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('', 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('', 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('', unsafe_allow_html=True)
+
+hour_pillars = lunar.twohour8CharList or []
+now_hour = datetime.now().hour
+is_today = selected_date == date.today()
+
+table_html = ''
+table_html += '| 時辰 | Période | Pilier 柱 | Élément | Animal | '
+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'| {emoji} {name} | '
+ table_html += f'{period} | '
+ table_html += f'{p["chars"]} | '
+ table_html += f'{p["element_fr"]} | '
+ table_html += f'{p["emoji"]} {p["animal"]}{current_marker} | '
+ table_html += '
'
+
+table_html += '
'
+st.html(table_html)
+
+
+# ─── SOLAR TERMS ──────────────────────────────────────
+
+st.markdown('', 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('', 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('',
+ 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 = """
+
+
+
+ | Étape |
+ Période |
+ 主气 Hôte |
+ 客气 Invité |
+ Relation |
+
+
+
+ """
+
+ # 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"""
+
+
+ {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}
+ |
+
+ """
+
+ liu_bu_html += "
"
+ 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('', 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']}")