feat(streamlit): Add Tong Shu Chinese Almanac app (wuyun_liuqi)

- Add 通書 Tong Shu almanac with Wu Yun Liu Qi calculations
- Dark theme compatible styling with transparent backgrounds
- French translations for zodiac animals and Chinese terms
- Uses st.html() for proper HTML rendering in Streamlit 1.33+
- Includes: Four Pillars, Day Quality, Clash/Directions, Activities

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-12 16:00:01 +01:00
parent a472e755ea
commit 029e0112fb
3 changed files with 1544 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
streamlit>=1.30.0
cnlunar>=0.3.0

View File

@ -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ǎ","":"","":"Bǐng","":"Dīng","":"",
"":"","":"Gēng","":"Xīn","":"Rén","":"Guǐ"}
BRANCH_PINYIN = {"":"","":"Chǒu","":"Yín","":"Mǎo","":"Chén",
"":"","":"","":"Wèi","":"Shēn","":"Yǒu",
"":"","":"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']}")