Removing the English-to-Chinese back-translation left every BVR semantic note and every yogas_doshas raj/dhana "H*_Lord(...) in H*" sentence as 未判定 in the Chinese yoga table and pages. - yoga_engine: semantic conditions return BiText(Chinese, old note). The Chinese comes from references/yoga_semantic_notes_zh.json, one sentence per note, so dedup keeps the same equality classes. English-only custom templates get the same lookup. combination_en is unchanged. - yogas_doshas: raj/dhana rows add combination_zh; combination is unchanged. - chinese_combination prefers a clean combination_zh. Which yogas fire, their strength and category are unchanged (fictional charts 0-19, same rows as staging; only set-iteration order differs). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HwMNZEzdzmrbPhuRNmZe3C
729 lines
26 KiB
Python
729 lines
26 KiB
Python
"""Chinese appendix wording and hygiene gates.
|
||
|
||
Chinese sentences are written here. They are not produced by stripping Han
|
||
from the English appendix or by adding words to the sanitize replacement table.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
try:
|
||
from scripts.report_render.text import PLANET_LABEL_CN, SIGNS, SIGNS_CN
|
||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||
from report_render.text import PLANET_LABEL_CN, SIGNS, SIGNS_CN
|
||
|
||
PLANET_ZH = {
|
||
**PLANET_LABEL_CN,
|
||
"Rahu": "北交点/罗睺",
|
||
"Ketu": "南交点/计都",
|
||
"Lagna": "上升",
|
||
"Ascendant": "上升",
|
||
}
|
||
PLANET_EN = {
|
||
"太阳": "Sun",
|
||
"月亮": "Moon",
|
||
"火星": "Mars",
|
||
"水星": "Mercury",
|
||
"木星": "Jupiter",
|
||
"金星": "Venus",
|
||
"土星": "Saturn",
|
||
"北交点": "Rahu",
|
||
"罗睺": "Rahu",
|
||
"南交点": "Ketu",
|
||
"计都": "Ketu",
|
||
}
|
||
|
||
SIGN_EN_RE = re.compile(
|
||
r"\b(Sagittarius|Capricorn|Aquarius|Aries|Taurus|Gemini|Cancer|Leo|Virgo|Libra|Scorpio|Pisces)\b"
|
||
)
|
||
PLANET_EN_RE = re.compile(r"\b(Mercury|Jupiter|Saturn|Venus|Mars|Rahu|Ketu|Moon|Sun)\b")
|
||
_CHART_FENCE_RE = re.compile(
|
||
r"```(?:jyotish-chart|jyotish-focus)\n.*?\n```",
|
||
re.DOTALL,
|
||
)
|
||
_HAN_FUNCTION_RE = re.compile(
|
||
r"(?:[\u4e00-\u9fff][ \t]*\b(?:in|and|or|from|planet|lord)\b"
|
||
r"|\b(?:in|and|or|from|planet|lord)\b[ \t]*[\u4e00-\u9fff])",
|
||
re.IGNORECASE,
|
||
)
|
||
_LITERAL_GLUE_RE = re.compile(r", :|in \)")
|
||
|
||
ZH_HEADINGS = {
|
||
"Birth Data and Calculation Profile": "出生资料与计算口径",
|
||
"Core Chart Raw Positions": "本命盘原始位置",
|
||
"Sudarshana Chakra": "Sudarshana Chakra(三参考点盘)",
|
||
"Sudarshana Chakra Three-Reference Placements": "三参考点落宫表",
|
||
"Bhavesh House-Lord Interpretations": "宫主逐宫解释",
|
||
"Moon Chart": "月亮参考盘",
|
||
"Divisional Charts Numeric Tables": "分盘数值位置表",
|
||
"Special Lagnas": "特殊上升点",
|
||
"Sensitive Points": "敏感点",
|
||
"Upagraha and Sub-Planet Points": "Upagraha 与副行星点",
|
||
"Shadbala Components": "六维力量细项",
|
||
"Functional Benefic / Malefic Layer": "功能性吉凶星层",
|
||
"Bhava Bala House Strength": "十二宫力量",
|
||
"Planetary Friendship Matrix": "行星友敌关系矩阵",
|
||
"Sarvashtakavarga": "Sarvashtakavarga 总分",
|
||
"Bhinnashtakavarga": "Bhinnashtakavarga 单星八分表",
|
||
"KP Planet Significators": "KP 行星征象星",
|
||
"KP House Cusps and Significators": "KP 宫头与征象星",
|
||
"Dasha Tables": "Dasha 表格",
|
||
"Annual Varshaphala Core Fields": "年度 Varshaphala 核心字段",
|
||
"Mudda Dasha": "Mudda 大运",
|
||
"Applicable Yogas": "可用瑜伽",
|
||
"Dosha and Transit Summary": "Dosha 与行运摘要",
|
||
"Annual Mudda Dasha Rows": "年度 Mudda 大运行",
|
||
"Annual Patyayini / Patyamsha Rows": "年度 Patyayini / Patyamsha 行",
|
||
"Annual Saham Rows": "年度 Saham 行",
|
||
"Annual Tajika Yoga Rows": "年度 Tajika 瑜伽行",
|
||
"Annual Planet Extended Fields": "年度行星扩展字段",
|
||
"Annual Anchor Points": "年度锚点",
|
||
"Birth-Place Eight-Year Annual Series": "出生地八年年度序列",
|
||
"Local Eight-Year Annual Series": "现居地八年年度序列",
|
||
}
|
||
|
||
ZH_HEADERS = {
|
||
"Field": "项目",
|
||
"Value": "内容",
|
||
"Name": "名称",
|
||
"Birth date": "出生日期",
|
||
"Birth time": "出生时间",
|
||
"Latitude": "纬度",
|
||
"Longitude": "黄经",
|
||
"Time zone": "时区",
|
||
"Ayanamsa": "岁差",
|
||
"Node mode": "交点模式",
|
||
"Object": "天体",
|
||
"Sign": "星座",
|
||
"Degree in sign": "落座度数",
|
||
"House": "宫位",
|
||
"Nakshatra": "星宿",
|
||
"Pada": "星宿四分",
|
||
"Status": "状态",
|
||
"Speed": "速度",
|
||
"Reference point": "参考点",
|
||
"Role": "角色",
|
||
"Reference chart": "参考盘",
|
||
"Planet": "行星",
|
||
"Lord": "宫主",
|
||
"Lord placement": "宫主落点",
|
||
"Lord dignity": "宫主尊贵",
|
||
"Dasha activation": "大运层",
|
||
"Point": "点",
|
||
"Sthana": "Sthana",
|
||
"Dig": "Dig",
|
||
"Kala": "Kala",
|
||
"Chesta": "Chesta",
|
||
"Naisargika": "Naisargika",
|
||
"Drik": "Drik",
|
||
"Total": "合计",
|
||
"Score": "分数",
|
||
"Strength": "强度",
|
||
"Notes": "说明",
|
||
"Natural friends": "天然友",
|
||
"Natural enemies": "天然敌",
|
||
"Natural neutral": "天然中性",
|
||
"Temporary friends": "临时友",
|
||
"Temporary enemies": "临时敌",
|
||
"Compound relationship": "复合关系",
|
||
"SAV": "SAV",
|
||
"Rasi lord": "星座主星",
|
||
"Star lord": "星宿主星",
|
||
"Sub lord": "Sub 主星",
|
||
"Sub-sub lord": "Sub-sub 主星",
|
||
"Cusp longitude": "宫头黄经",
|
||
"Sign/Rashi": "星座",
|
||
"Start": "开始",
|
||
"End": "结束",
|
||
"Years": "年数",
|
||
"Order": "顺序",
|
||
"Months": "月数",
|
||
"Yoga": "瑜伽",
|
||
"Category": "类别",
|
||
"Combination": "组合条件",
|
||
"Effects / notes": "作用说明",
|
||
"Topic": "项目",
|
||
"Detected / phase": "是否成立 / 阶段",
|
||
"Summary": "摘要",
|
||
"Month": "月份",
|
||
"Local time": "本地时间",
|
||
"Annual ascendant": "年盘上升",
|
||
"Ascendant degree": "上升度数",
|
||
"Age": "岁数",
|
||
"Year": "年份",
|
||
"Solar return time": "太阳返照时刻",
|
||
"Ar": "白羊",
|
||
"Ta": "金牛",
|
||
"Ge": "双子",
|
||
"Cn": "巨蟹",
|
||
"Le": "狮子",
|
||
"Vi": "处女",
|
||
"Li": "天秤",
|
||
"Sc": "天蝎",
|
||
"Sg": "射手",
|
||
"Cp": "摩羯",
|
||
"Aq": "水瓶",
|
||
"Pi": "双鱼",
|
||
}
|
||
|
||
HOUSE_TOPICS_ZH = {
|
||
1: "自身、身体与人生方向",
|
||
2: "资源、言语与家庭价值",
|
||
3: "行动、学习与沟通",
|
||
4: "居所、家宅与内在安定",
|
||
5: "创造、智力与判断",
|
||
6: "事务、压力与日常责任",
|
||
7: "合作、婚姻与公开往来",
|
||
8: "共有资源、风险、研究与转变",
|
||
9: "信念、师长、高等教育与远行",
|
||
10: "事业、责任与公开角色",
|
||
11: "收获、人际与长期目标",
|
||
12: "退隐、支出、睡眠、异地与放下",
|
||
}
|
||
DESTINATION_TOPICS_ZH = {
|
||
1: "身份与身体表达",
|
||
2: "资源、言语与家庭延续",
|
||
3: "努力、技能与沟通",
|
||
4: "家宅、土地与情感根基",
|
||
5: "学习、创造与功德",
|
||
6: "事务、服务、竞争与修复",
|
||
7: "合作、对方与公开事务",
|
||
8: "共有资源、研究、脆弱与深层变化",
|
||
9: "信念、学业、师长与远行",
|
||
10: "事业、权威与可见职责",
|
||
11: "收入、人际与目标达成",
|
||
12: "支出、退隐、睡眠、异地联系与放下",
|
||
}
|
||
|
||
YOGA_CATEGORY_ZH = {
|
||
"solar_yoga": "太阳瑜伽",
|
||
"lunar_yoga": "月亮瑜伽",
|
||
"raja_yoga": "王瑜伽",
|
||
"dhana_yoga": "财富瑜伽",
|
||
"mahapurusha": "大人格瑜伽",
|
||
"nabhasa": "天空瑜伽",
|
||
"lunar": "月亮瑜伽",
|
||
"solar": "太阳瑜伽",
|
||
"raja": "王瑜伽",
|
||
"dhana": "财富瑜伽",
|
||
"kalatra": "婚恋",
|
||
"durbhaga": "不利组合",
|
||
"common": "常见",
|
||
"rare": "罕见",
|
||
}
|
||
|
||
REFERENCE_POINT_ZH = {
|
||
"ascendant_lagna": "出生上升参考点",
|
||
"moon_lagna": "月亮参考点",
|
||
"sun_lagna": "太阳参考点",
|
||
}
|
||
REFERENCE_CHART_ZH = {
|
||
"ascendant_lagna": "从出生上升起算",
|
||
"moon_lagna": "从月亮起算",
|
||
"sun_lagna": "从太阳起算",
|
||
}
|
||
ROLE_ZH = {
|
||
"primary": "主参考",
|
||
"secondary": "次参考",
|
||
"lagna": "上升",
|
||
"moon": "月亮",
|
||
"sun": "太阳",
|
||
}
|
||
|
||
COMPOUND_ORDER = (
|
||
("great_friends", "极友", "Great friend"),
|
||
("friends", "友", "Friend"),
|
||
("neutral", "中性", "Neutral"),
|
||
("enemies", "敌", "Enemy"),
|
||
("great_enemies", "极敌", "Great enemy"),
|
||
)
|
||
|
||
DOSHA_TOPICS = {
|
||
"mangal_dosha": ("火星婚姻煞(Mangala / Kuja Dosha)", "Mangala / Kuja Dosha"),
|
||
"kaal_sarp_dosha": ("时蛇煞(Kaal Sarp Dosha)", "Kaal Sarp Dosha"),
|
||
"pitra_dosha": ("祖业/父系业力煞(Pitra Dosha)", "Pitra Dosha"),
|
||
"sade_sati": ("Sade Sati(土星七年半)", "Sade Sati"),
|
||
}
|
||
|
||
|
||
def undetermined(language: str) -> str:
|
||
return "未判定" if language == "zh" else "Not determined"
|
||
|
||
|
||
def yes_no(value: bool, language: str) -> str:
|
||
if language == "zh":
|
||
return "是" if value else "否"
|
||
return "Yes" if value else "No"
|
||
|
||
|
||
def present_label(value: bool, language: str) -> str:
|
||
if language == "zh":
|
||
return "有" if value else "无"
|
||
return "Yes" if value else "No"
|
||
|
||
|
||
def sign_name(value: Any, language: str) -> str:
|
||
if value is None or value == "":
|
||
return undetermined(language)
|
||
if isinstance(value, int) and 0 <= value < len(SIGNS):
|
||
value = SIGNS[value]
|
||
text = str(value).strip()
|
||
if language == "zh":
|
||
return SIGNS_CN.get(text, text)
|
||
if text in SIGNS_CN.values():
|
||
for english, chinese in SIGNS_CN.items():
|
||
if chinese == text:
|
||
return english
|
||
return text
|
||
|
||
|
||
def planet_name(value: Any, language: str) -> str:
|
||
if value is None or value == "":
|
||
return undetermined(language)
|
||
text = str(value).strip()
|
||
if language == "zh":
|
||
return PLANET_ZH.get(text, text)
|
||
if text in PLANET_EN:
|
||
return PLANET_EN[text]
|
||
return text
|
||
|
||
|
||
def planet_list(values: Any, language: str) -> str:
|
||
if not isinstance(values, (list, tuple)) or not values:
|
||
return ""
|
||
separator = "、" if language == "zh" else ", "
|
||
return separator.join(planet_name(item, language) for item in values)
|
||
|
||
|
||
_STATUS_ZH = {
|
||
"exalted": "擢升",
|
||
"debilitated": "落陷",
|
||
"moolatrikona": "根本三分",
|
||
"mooltrikona": "根本三分",
|
||
"own": "入庙",
|
||
"friendly": "友好",
|
||
"enemy": "敌对",
|
||
"neutral": "中性",
|
||
"friend": "友好",
|
||
}
|
||
_STATUS_ZH_RE = re.compile(
|
||
r"\b(moolatrikona|mooltrikona|exalted|debilitated|friendly|neutral|enemy|friend|own)\b",
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
|
||
def public_zh(value: Any) -> str:
|
||
"""Keep Chinese and swap known English sign, planet, and dignity tokens."""
|
||
if value is None:
|
||
return "-"
|
||
if isinstance(value, bool):
|
||
return "是" if value else "否"
|
||
if isinstance(value, float):
|
||
return f"{value:.6f}".rstrip("0").rstrip(".")
|
||
if isinstance(value, int):
|
||
return str(value)
|
||
text = str(value).strip()
|
||
if not text:
|
||
return "-"
|
||
if text.lower() == "mean":
|
||
return "平交点"
|
||
if text.lower() == "true":
|
||
return "真交点"
|
||
text = re.sub(r"\bOwn Sign\b", "入庙", text, flags=re.IGNORECASE)
|
||
text = SIGN_EN_RE.sub(lambda match: SIGNS_CN[match.group(1)], text)
|
||
text = PLANET_EN_RE.sub(lambda match: PLANET_ZH[match.group(1)], text)
|
||
text = _STATUS_ZH_RE.sub(lambda match: _STATUS_ZH[match.group(1).lower()], text)
|
||
text = text.replace("北交点/罗睺/罗睺", "北交点/罗睺")
|
||
text = text.replace("南交点/计都/计都", "南交点/计都")
|
||
return text
|
||
|
||
|
||
FIELD_ZH = {
|
||
"balance_profile": "余额口径",
|
||
"lord": "主星",
|
||
"start": "开始",
|
||
"end": "结束",
|
||
"boundary_status": "边界状态",
|
||
"main": "主段",
|
||
"sub": "子段",
|
||
"boundary": "边界",
|
||
"name": "名称",
|
||
"saham": "Saham",
|
||
"sign": "星座",
|
||
"degree_in_sign": "落座度数",
|
||
"yoga": "瑜伽",
|
||
"present": "是否出现",
|
||
"forming_planets": "构成行星",
|
||
"planet": "行星",
|
||
"degree": "度数",
|
||
"longitude": "黄经",
|
||
"nakshatra": "星宿",
|
||
"pada": "星宿四分",
|
||
"label": "标签",
|
||
"value": "内容",
|
||
"house": "宫位",
|
||
"order": "顺序",
|
||
"months": "月数",
|
||
}
|
||
|
||
# Names that already had a Chinese header before the #91 fix.
|
||
# The annual-table filter used to drop every other column. Counts use this set
|
||
# so a later Chinese name still shows up as a column the old filter removed.
|
||
ANNUAL_COLUMN_NAMES_BEFORE_FIX = frozenset(FIELD_ZH)
|
||
|
||
_APPENDIX_DROPS: dict[str, dict[str, int]] = {
|
||
"annual_columns": {},
|
||
"annual_tables": {},
|
||
"effects": {},
|
||
"combinations_undetermined": {},
|
||
"combinations_replaced": {},
|
||
}
|
||
|
||
|
||
def reset_appendix_drop_log() -> None:
|
||
"""Clear the per-render drop counts. One Chinese render, then snapshot."""
|
||
for bucket in _APPENDIX_DROPS.values():
|
||
bucket.clear()
|
||
|
||
|
||
def snapshot_appendix_drop_log() -> dict[str, dict[str, int]]:
|
||
return {name: dict(bucket) for name, bucket in _APPENDIX_DROPS.items()}
|
||
|
||
|
||
def note_dropped_annual_column(field: str) -> None:
|
||
key = str(field)
|
||
bucket = _APPENDIX_DROPS["annual_columns"]
|
||
bucket[key] = bucket.get(key, 0) + 1
|
||
|
||
|
||
def note_annual_table() -> None:
|
||
bucket = _APPENDIX_DROPS["annual_tables"]
|
||
bucket["seen"] = bucket.get("seen", 0) + 1
|
||
|
||
|
||
def _drop_identity(item: dict, text: str) -> str:
|
||
parts = [
|
||
str(item.get("rule_id") or ""),
|
||
str(item.get("source") or ""),
|
||
str(item.get("bvr_id") or ""),
|
||
str(item.get("name") or ""),
|
||
" ".join(str(text).split()),
|
||
]
|
||
return " | ".join(parts)[:240]
|
||
|
||
|
||
def _note_effect(item: dict, text: str) -> None:
|
||
key = _drop_identity(item, text)
|
||
bucket = _APPENDIX_DROPS["effects"]
|
||
bucket[key] = bucket.get(key, 0) + 1
|
||
|
||
|
||
def _note_combination(item: dict, text: str, kind: str) -> None:
|
||
key = _drop_identity(item, text)
|
||
bucket = _APPENDIX_DROPS[kind]
|
||
bucket[key] = bucket.get(key, 0) + 1
|
||
|
||
|
||
def column_header(field: str, language: str) -> str:
|
||
if language != "zh":
|
||
return str(field).replace("_", " ").title()
|
||
label = FIELD_ZH.get(str(field))
|
||
if not label:
|
||
raise KeyError(field)
|
||
return label
|
||
if language != "zh":
|
||
return english
|
||
return ZH_HEADINGS.get(english, english)
|
||
|
||
|
||
def header(english: str, language: str) -> str:
|
||
if language != "zh":
|
||
return english
|
||
return ZH_HEADERS.get(english, english)
|
||
|
||
|
||
def _join_compound(groups: dict, language: str) -> str:
|
||
parts = []
|
||
for key, zh, en in COMPOUND_ORDER:
|
||
names = planet_list(groups.get(key) if isinstance(groups, dict) else None, language)
|
||
if not names:
|
||
continue
|
||
label = zh if language == "zh" else en
|
||
parts.append(f"{label}:{names}" if language == "zh" else f"{label}: {names}")
|
||
return ";".join(parts) if language == "zh" else "; ".join(parts)
|
||
|
||
|
||
def friendship_table(snapshot: Any, language: str) -> tuple[list[str], list[list[str]]]:
|
||
headers = [
|
||
header("Planet", language),
|
||
header("Natural friends", language),
|
||
header("Natural enemies", language),
|
||
header("Natural neutral", language),
|
||
header("Temporary friends", language),
|
||
header("Temporary enemies", language),
|
||
header("Compound relationship", language),
|
||
]
|
||
rows_src = snapshot.get("rows") if isinstance(snapshot, dict) else None
|
||
rows: list[list[str]] = []
|
||
if not isinstance(rows_src, list):
|
||
return headers, rows
|
||
for item in rows_src:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
natural = item.get("natural") if isinstance(item.get("natural"), dict) else {}
|
||
temporary = item.get("temporary") if isinstance(item.get("temporary"), dict) else {}
|
||
compound = item.get("compound") if isinstance(item.get("compound"), dict) else {}
|
||
rows.append([
|
||
planet_name(item.get("planet"), language),
|
||
planet_list(natural.get("friends"), language) or "-",
|
||
planet_list(natural.get("enemies"), language) or "-",
|
||
planet_list(natural.get("neutral"), language) or "-",
|
||
planet_list(temporary.get("friends"), language) or "-",
|
||
planet_list(temporary.get("enemies"), language) or "-",
|
||
_join_compound(compound, language) or "-",
|
||
])
|
||
return headers, rows
|
||
|
||
|
||
def _severity(value: Any, language: str) -> str:
|
||
mapping = {
|
||
"high": ("高", "High"),
|
||
"moderate": ("中", "Moderate"),
|
||
"low": ("低", "Low"),
|
||
"none": ("无", "None"),
|
||
}
|
||
pair = mapping.get(str(value or "").lower())
|
||
if not pair:
|
||
return undetermined(language)
|
||
return pair[0] if language == "zh" else pair[1]
|
||
|
||
|
||
def dosha_row(key: str, item: Any, language: str) -> list[str] | None:
|
||
topic_zh, topic_en = DOSHA_TOPICS.get(key, (None, None))
|
||
if topic_zh is None:
|
||
return None
|
||
topic = topic_zh if language == "zh" else topic_en
|
||
if not isinstance(item, dict):
|
||
return [topic, undetermined(language), undetermined(language)]
|
||
if key == "sade_sati":
|
||
return [topic, *_sade_sati_cells(item, language)]
|
||
if "has_dosha" not in item:
|
||
return [topic, undetermined(language), undetermined(language)]
|
||
has_dosha = bool(item.get("has_dosha"))
|
||
detected = present_label(has_dosha, language)
|
||
if key == "mangal_dosha":
|
||
summary = _mangal_summary(item, has_dosha, language)
|
||
elif key == "kaal_sarp_dosha":
|
||
detected, summary = _kaal_cells(item, has_dosha, language)
|
||
else:
|
||
summary = _pitra_summary(item, has_dosha, language)
|
||
return [topic, detected, summary]
|
||
|
||
|
||
def _mangal_summary(item: dict, has_dosha: bool, language: str) -> str:
|
||
house = item.get("mars_house")
|
||
sign = sign_name(item.get("mars_sign"), language) if item.get("mars_sign") else ""
|
||
severity = _severity(item.get("severity"), language) if item.get("severity") else ""
|
||
if not has_dosha and house is None:
|
||
if language == "zh":
|
||
return "火星不在煞宫。"
|
||
return "Mars is not in a dosha house."
|
||
if not has_dosha and not item.get("mars_sign"):
|
||
if language == "zh":
|
||
return "火星不在煞宫。"
|
||
return "Mars is not in a dosha house."
|
||
if language == "zh":
|
||
place = f"火星在第 {house} 宫" if house is not None else "火星落点未给出"
|
||
if sign and sign != undetermined(language):
|
||
place += f",星座为{sign}"
|
||
if severity and severity != undetermined(language):
|
||
place += f",严重程度{severity}"
|
||
return ("有火星婚姻煞。" if has_dosha else "无火星婚姻煞。") + place + "。"
|
||
place = f"Mars in house {house}" if house is not None else "Mars house is not listed"
|
||
if sign:
|
||
place += f", {sign}"
|
||
if severity and severity != undetermined(language):
|
||
place += f"; severity {severity}"
|
||
prefix = "Present. " if has_dosha else "No. "
|
||
return prefix + place + "."
|
||
|
||
|
||
def _kaal_cells(item: dict, has_dosha: bool, language: str) -> tuple[str, str]:
|
||
kind = str(item.get("type") or "none")
|
||
inside = item.get("planets_in_axis") if isinstance(item.get("planets_in_axis"), list) else []
|
||
outside = item.get("planets_outside") if isinstance(item.get("planets_outside"), list) else []
|
||
total = len(inside) + len(outside)
|
||
count = len(inside)
|
||
rahu = item.get("rahu_house")
|
||
ketu = item.get("ketu_house")
|
||
if kind == "full":
|
||
detected = "完全" if language == "zh" else "Full"
|
||
elif kind == "partial":
|
||
detected = "部分" if language == "zh" else "Partial"
|
||
elif kind == "none" and "has_dosha" in item:
|
||
detected = present_label(False, language)
|
||
else:
|
||
detected = present_label(has_dosha, language)
|
||
if total <= 0:
|
||
summary = undetermined(language)
|
||
elif language == "zh":
|
||
summary = f"{total} 颗中 {count} 颗"
|
||
if rahu is not None and ketu is not None:
|
||
summary += f"。北交点/罗睺在第 {rahu} 宫,南交点/计都在第 {ketu} 宫"
|
||
else:
|
||
summary = f"{count} of {total}"
|
||
if rahu is not None and ketu is not None:
|
||
summary += f". Rahu in house {rahu}, Ketu in house {ketu}"
|
||
return detected, summary
|
||
|
||
|
||
def _pitra_summary(item: dict, has_dosha: bool, language: str) -> str:
|
||
indicators = item.get("indicators") if isinstance(item.get("indicators"), list) else []
|
||
if not has_dosha:
|
||
if language == "zh":
|
||
return "未见明确迹象。"
|
||
return "No clear indication."
|
||
count = len(indicators) or 1
|
||
if language == "zh":
|
||
return f"有迹象,共 {count} 条。不把迹象原文当作定论。"
|
||
return f"Indications present, {count} item(s). The indication text is not a settled conclusion."
|
||
|
||
|
||
def _sade_sati_cells(item: dict, language: str) -> tuple[str, str]:
|
||
if "is_active" not in item:
|
||
return undetermined(language), undetermined(language)
|
||
active = bool(item.get("is_active"))
|
||
phase = str(item.get("phase") or "none")
|
||
phases = item.get("phases") if isinstance(item.get("phases"), dict) else {}
|
||
if language == "zh":
|
||
detected = "进行中" if active else "未进行"
|
||
if phase and phase != "none":
|
||
detected = f"{detected},{phase}"
|
||
else:
|
||
detected = "Active" if active else "Not active"
|
||
if phase and phase != "none":
|
||
detected = f"{detected}, {phase}"
|
||
if not phases:
|
||
return detected, undetermined(language)
|
||
rising = sign_name(phases.get("rising"), language)
|
||
peak = sign_name(phases.get("peak"), language)
|
||
setting = sign_name(phases.get("setting"), language)
|
||
if language == "zh":
|
||
summary = f"上升期{rising},高峰期{peak},下降期{setting}。是否进行要看土星实际位置。"
|
||
else:
|
||
summary = f"Rising {rising}, peak {peak}, setting {setting}. Activity follows Saturn's actual position."
|
||
return detected, summary
|
||
|
||
|
||
def _has_prose_glue(text: str) -> bool:
|
||
return bool(_HAN_FUNCTION_RE.search(text) or _LITERAL_GLUE_RE.search(text) or SIGN_EN_RE.search(text))
|
||
|
||
|
||
def chinese_plain(text: Any, fallback: str = "未判定") -> str:
|
||
value = str(text or "").strip()
|
||
if not value or _has_prose_glue(value):
|
||
return fallback
|
||
return value
|
||
|
||
|
||
def chinese_yoga_label(item: dict) -> str:
|
||
"""Prefer a Chinese name that does not glue English function words onto Han."""
|
||
for key in ("name_cn", "name", "type"):
|
||
text = str(item.get(key) or "").strip()
|
||
if not text:
|
||
continue
|
||
if not _has_prose_glue(text):
|
||
return text
|
||
prefix = re.split(r"[((]", text, maxsplit=1)[0].strip()
|
||
if prefix and prefix != text and not _has_prose_glue(prefix):
|
||
return prefix
|
||
category = str(item.get("category") or "")
|
||
if category in YOGA_CATEGORY_ZH:
|
||
return YOGA_CATEGORY_ZH[category]
|
||
return "瑜伽"
|
||
|
||
|
||
def chinese_combination(item: dict) -> str:
|
||
"""Use the Chinese combination sentence. An English sentence is not translated word by word."""
|
||
authored = str(item.get("combination_zh") or "").strip()
|
||
if authored and re.search(r"[一-鿿]", authored) and not _has_prose_glue(authored):
|
||
# Written in Chinese by the detector itself (yogas_doshas raj / dhana rows).
|
||
return authored
|
||
combination = str(item.get("combination") or "").strip()
|
||
han = bool(re.search(r"[\u4e00-\u9fff]", combination))
|
||
function = bool(_HAN_FUNCTION_RE.search(combination)) if combination else False
|
||
sign = bool(SIGN_EN_RE.search(combination)) if combination else False
|
||
if combination and han and not function and not sign:
|
||
return combination
|
||
planets = item.get("planets") if isinstance(item.get("planets"), list) else []
|
||
names = [planet_name(planet, "zh") for planet in planets if planet]
|
||
if names:
|
||
if combination:
|
||
_note_combination(item, combination, "combinations_replaced")
|
||
return "与".join(names)
|
||
if combination:
|
||
_note_combination(item, combination, "combinations_undetermined")
|
||
return "未判定"
|
||
|
||
|
||
def yoga_cells(item: dict, language: str) -> list[Any]:
|
||
"""Chinese cells come from the Chinese fields. English uses the English twins."""
|
||
if language != "zh":
|
||
return None
|
||
category = item.get("category")
|
||
category_text = YOGA_CATEGORY_ZH.get(str(category or ""), public_zh(category) if category else "-")
|
||
if _has_prose_glue(category_text):
|
||
category_text = "未判定"
|
||
strength = item.get("strength") or "-"
|
||
effects = item.get("effects") if isinstance(item.get("effects"), list) else []
|
||
kept = []
|
||
for effect in effects:
|
||
text = str(effect).strip()
|
||
if not text:
|
||
continue
|
||
if _has_prose_glue(text):
|
||
_note_effect(item, text)
|
||
continue
|
||
kept.append(text)
|
||
effects_text = ";".join(kept) or "-"
|
||
return [chinese_yoga_label(item), category_text, strength, chinese_combination(item), effects_text]
|
||
|
||
|
||
def house_lord_narrative_zh(house_index: int, sign: Any, lord: Any, lord_sign: Any, lord_house: Any, dignity: Any, activation_active: bool) -> list[str]:
|
||
sign_text = sign_name(sign, "zh")
|
||
lord_text = planet_name(lord, "zh")
|
||
lord_sign_text = sign_name(lord_sign, "zh") if lord_sign else undetermined("zh")
|
||
topic = HOUSE_TOPICS_ZH.get(house_index, f"第 {house_index} 宫主题")
|
||
try:
|
||
destination_house = int(lord_house)
|
||
except (TypeError, ValueError):
|
||
destination_house = None
|
||
destination = DESTINATION_TOPICS_ZH.get(destination_house, "其落点") if destination_house else "其落点"
|
||
dignity_text = public_zh(dignity) if dignity else undetermined("zh")
|
||
activation = "当前大运层" if activation_active else "本命背景"
|
||
if destination_house:
|
||
placement = f"{lord_sign_text},第 {destination_house} 宫"
|
||
else:
|
||
placement = lord_sign_text
|
||
return [
|
||
f"### 第 {house_index} 宫宫主:{lord_text}",
|
||
"",
|
||
f"第 {house_index} 宫涉及{topic}。宫头星座是{sign_text},宫主是{lord_text}。",
|
||
"",
|
||
f"宫主落在{placement},尊贵状态为{dignity_text}。第 {house_index} 宫的议题因此带到{destination}。",
|
||
"",
|
||
f"阅读时同时看尊贵、合相、相位、六维力量、分盘重复与大运应期。当前时间层:{activation}。",
|
||
"",
|
||
]
|
||
|
||
|
||
def chinese_prose_gates(markdown: str) -> dict[str, int]:
|
||
"""Count English zodiac names and Han-adjacent function words outside chart fences."""
|
||
text = _CHART_FENCE_RE.sub("", markdown or "")
|
||
return {
|
||
"english_signs": len(SIGN_EN_RE.findall(text)),
|
||
"han_function_glue": len(_HAN_FUNCTION_RE.findall(text)) + len(_LITERAL_GLUE_RE.findall(text)),
|
||
}
|