Files
Jyotisha/scripts/report_render/appendix_language.py
T
Jesse_ChenandClaude Opus 5.5 ac3dccbac8 fix(yoga): Chinese combination text from a Chinese source, not 未判定 (#91 fix2 F3, BUG-1418)
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
2026-10-22 21:55:02 +08:00

729 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)),
}