fix(report): project annual sections and thicken dasha reading

完整数据版第二轮。年运章写出资料包里已有的节,没有数据的不新算。大运解释补回可能段落。瑜伽作用不再断言寿命。术语替换不进标识符。罗睺和计都不写守护第未列出宫。未合入 staging。
This commit is contained in:
jesse-ux
2026-10-08 22:30:32 +08:00
committed by Jesse_Chen
parent 25d067bf12
commit b8eef75871
14 changed files with 1148 additions and 165 deletions
+42 -1
View File
@@ -62,6 +62,7 @@ def build_annual_tajika_pack(
"muntha": muntha,
"year_lord": year_lord,
"tajika_yogas": tajika_yogas,
"tajika_strength": _tajika_strength_field(solar_report if isinstance(solar_report, dict) else {}),
"sahams": sahams,
"mudda_dasha": mudda_dasha,
"patyayini_dasha": patyayini_dasha,
@@ -194,11 +195,51 @@ def _field_from_report(report: dict[str, Any], key: str, label: str) -> dict[str
def _annual_chart_from_solar_report(report: dict[str, Any]) -> dict[str, Any]:
field = _field_from_report(report, "sr_chart_info", "Solar Return Annual Chart")
if field["status"] == "partial_verified":
if field["status"] == "partial_verified" and isinstance(field.get("data"), dict):
field["data_contract"] = "normalized_from_sr_chart_info"
longitudes = report.get("planet_lons") if isinstance(report, dict) else None
if isinstance(longitudes, dict) and longitudes:
data = dict(field["data"])
data["planet_longitudes"] = {
name: float(value)
for name, value in longitudes.items()
if isinstance(value, (int, float)) and not isinstance(value, bool)
}
field["data"] = data
return field
def _tajika_strength_field(report: dict[str, Any]) -> dict[str, Any]:
"""Copy numeric strength scores. Internal status words stay off this surface."""
raw = report.get("tajika_strength") if isinstance(report, dict) else None
if not isinstance(raw, dict):
return {"status": "not_applicable", "producer": "Tajika strength", "reason": "tajika_strength_missing"}
def scores(table: Any) -> dict[str, dict[str, float]]:
if not isinstance(table, dict):
return {}
kept = {}
for planet, item in table.items():
if not isinstance(item, dict):
continue
score = item.get("score")
if isinstance(score, bool) or not isinstance(score, (int, float)):
continue
kept[str(planet)] = {"score": float(score)}
return kept
harsha = scores(raw.get("harsha_bala"))
five_fold = scores(raw.get("panchavargiya_bala"))
if not harsha and not five_fold:
return {"status": "not_applicable", "producer": "Tajika strength", "reason": "tajika_strength_missing"}
return {
"status": "partial_verified",
"producer": "Tajika strength",
"data": {"harsha_bala": harsha, "panchavargiya_bala": five_fold},
}
def _tajika_yogas_field(solar_report: dict[str, Any]) -> dict[str, Any]:
if not isinstance(solar_report, dict) or solar_report.get("error"):
return _field_from_report(solar_report, "tajika_yogas", "Tajika Yogas")
+32 -12
View File
@@ -95,9 +95,17 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
"""Keep the parity volume's facts while removing internal-only labels."""
try:
from pl9_language_terms import normalize_zh_markdown_terms
from pl9_language_terms import (
normalize_zh_markdown_terms,
replace_natural_language_term,
strip_han_plural_s,
)
except ModuleNotFoundError: # pragma: no cover - package import compatibility
from scripts.pl9_language_terms import normalize_zh_markdown_terms
from scripts.pl9_language_terms import (
normalize_zh_markdown_terms,
replace_natural_language_term,
strip_han_plural_s,
)
text = _sanitize_pl9_public_markdown(markdown)
replacements = {
@@ -141,9 +149,6 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
"functional_role_not_returned": "功能属性未单独返回",
"natural_benefic": "自然吉星",
"natural_malefic": "自然凶星",
"functional_benefic": "功能吉星",
"functional_malefic": "功能凶星",
"functional_neutral": "功能中性",
"current_dasha": "当前 Dasha",
"birth_balance": "出生余额",
"Cycle / phase": "周期 / 阶段",
@@ -196,6 +201,12 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
}
for raw, public in replacements.items():
text = text.replace(raw, public)
for raw, public in (
("functional_benefic", "功能吉星"),
("functional_malefic", "功能凶星"),
("functional_neutral", "功能中性"),
):
text = replace_natural_language_term(text, raw, public)
text = re.sub(
r"\n## Bhavesh House-Lord Interpretations\n\n.*?(?=\n## |\Z)",
"\n",
@@ -218,7 +229,7 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
text = text.replace("Planetary", "行星")
text = text.replace("Planets", "行星")
text = text.replace("Planet", "行星")
text = text.replace("Sign", "星座")
text = replace_natural_language_term(text, "Sign", "星座")
text = text.replace("Degree in sign", "落座度数")
text = text.replace("Degree", "度数")
text = text.replace("Longitude", "黄经")
@@ -451,12 +462,12 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
"weak": "弱",
"medium": "中等",
"strong": "强",
"house condition": "宫位条件",
"house ": "第",
" condition": "宫条件",
}
text = replace_natural_language_term(text, "house condition", "宫位条件")
for raw, public in zh_heading_repairs.items():
text = text.replace(raw, public)
text = replace_natural_language_term(text, " condition", "宫条件")
zh_public_key_repairs = {
"mangal_dosha": "火星婚姻煞",
"kaal_sarp_dosha": "时蛇煞",
@@ -506,7 +517,7 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
]
)
text = re.sub(r"^#\s+.*$", summary, text, count=1, flags=re.MULTILINE)
return text + "\n"
return strip_han_plural_s(text) + "\n"
@@ -1001,7 +1012,8 @@ def _sanitize_pl9_ai_density_markdown_en(markdown: str, title: str = "Vedic Astr
"王权/事业": "authority/career",
"天象格局": "configuration",
"子女/创造": "children/creativity",
"寿命/健康": "longevity/health",
"体质参考": "constitution reference",
"寿命/健康": "constitution reference",
"愿望/收益": "desire/gains",
"解脱/内在": "moksha/inner life",
"贵人/助力": "patronage/support",
@@ -1494,10 +1506,18 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str:
stripped = value.strip()
translated = EFFECTS_EN.get(stripped)
if translated:
return translated
try:
from pl9_language_terms import soften_lifespan_wording
except ModuleNotFoundError: # pragma: no cover
from scripts.pl9_language_terms import soften_lifespan_wording
return soften_lifespan_wording(translated, "en")
# 已是英文短语(无 CJK)时原样透传,避免被误当占位删掉。
if re.search(r"[A-Za-z]", stripped) and not re.search(r"[\u4e00-\u9fff]", stripped):
return stripped
try:
from pl9_language_terms import soften_lifespan_wording
except ModuleNotFoundError: # pragma: no cover
from scripts.pl9_language_terms import soften_lifespan_wording
return soften_lifespan_wording(stripped, "en")
return ""
def table(headers: list[str], rows: list[list[Any]]) -> str:
+314 -22
View File
@@ -79,6 +79,49 @@ _THEMES: dict[str, dict[str, tuple[str, str]]] = {
},
}
# Traditional period topics, rewritten as possibilities. No diagnosis, death,
# lifespan, or certain reputation loss.
_PHALA: dict[str, dict[str, tuple[str, str, str]]] = {
"Sun": {
"zh": ("责任、可见职务、父亲或权威人物、土地,以及需要当场决定的公共事务", "被认可、得到行政上的协助,或把一件公共事务做完", "与权威的摩擦、离开熟悉环境办事、心里发热或坐不住,名声也可能受到议论"),
"en": ("responsibility, visible office, a father or authority figure, land, and public decisions", "recognition, administrative help, or finishing a public task", "friction with authority, leaving a familiar place for duty, heat or restlessness, and talk about reputation"),
},
"Moon": {
"zh": ("情绪节奏、母亲或照料、居住、出行、公众气氛和学习", "家里更安稳、交易或学习有进展,或换一个地方之后事情变顺", "判断摇摆、家人意见不一致、出行变密,旧的安全感变弱"),
"en": ("emotional pace, care, residence, travel, public mood, and study", "a steadier home, progress in trade or study, or a change of place that helps", "wavering judgment, family disagreement, denser travel, and a weaker sense of security"),
},
"Mars": {
"zh": ("行动、勇气、土地、兄弟姐妹、竞争和技术上的决断", "体力更够用、不动产或操作类工作有进展,竞争里更容易把事做完", "话说得硬、争执、火或器械方面的压力,冲动之下把资源花在搬迁或对抗上"),
"en": ("action, courage, land, siblings, competition, and technical decisions", "more stamina, progress in property or hands-on work, and finishing a contest", "harsh speech, quarrels, pressure around fire or tools, and spending resources on a move or a fight"),
},
"Mercury": {
"zh": ("学习、书写、交易、计算、合同和来回沟通", "文书更清楚、结盟更有用,或靠技能把问题拆开", "判断变散、文书带来额外开支,生意节奏忽快忽慢"),
"en": ("study, writing, trade, calculation, contracts, and back-and-forth talk", "clearer paperwork, more useful alliances, or taking a problem apart by skill", "scattered judgment, extra cost from paperwork, and an uneven business pace"),
},
"Jupiter": {
"zh": ("老师、判断、信仰、顾问、学习和体面的扩张", "得到尊重、顾问角色更清楚,或一件长期目标有着落", "建议不一致、目标推迟、家族义务变重"),
"en": ("teachers, judgment, belief, counsel, study, and a respectable expansion", "more respect, a clearer advisory role, or a long aim that finds a place", "conflicting advice, delayed aims, and heavier family obligations"),
},
"Venus": {
"zh": ("合作、审美、合意、资源交换、艺术和社交里的舒适", "协商更顺、作品或设计被接受,关系里的舒适感增加", "开销变大、享乐挤占正事,合作约定需要重新谈"),
"en": ("cooperation, taste, agreement, exchange, art, and social comfort", "smoother negotiation, accepted design, and more comfort in a relationship", "larger spending, pleasure crowding out work, and agreements that need to be discussed again"),
},
"Saturn": {
"zh": ("劳动、延迟、资历、界限、土地和长期功课", "工作更稳、责任被看见,后半段比开头更容易出成果", "拖延、负担变重、和熟人的距离拉开,事情要慢慢熬"),
"en": ("labor, delay, seniority, boundaries, land, and long-term work", "steadier work, visible duty, and more results in the later part than at the start", "delay, a heavier load, more distance from familiar people, and work that has to be endured"),
},
"Rahu": {
"zh": ("放大、外来机会、不按旧习惯走的路径,以及突然的转向", "非传统渠道带来开口,远方或圈层之外的事情更活跃", "想要得很急、信息前后不一致,名声可能受到议论"),
"en": ("amplification, outside opportunities, unfamiliar routes, and sudden turns", "an opening through a nontraditional channel, with distant or outside circles more active", "urgent wanting, inconsistent information, and talk about reputation"),
},
"Ketu": {
"zh": ("收束、分离、向内整理、专精和减少外界噪声", "看清一件旧目标可以放下,或在压力之后出现间歇的进展", "突然的停顿、来回奔波,或把注意力从外面收回来"),
"en": ("release, separation, inward sorting, specialization, and less outer noise", "seeing that an old aim can be set down, or intermittent progress after pressure", "a sudden pause, restless travel, or attention pulled back from the outside"),
},
}
_NODE_LORDS = {"Rahu", "Ketu"}
_HOUSE_TOPIC_ZH = {
1: "自我呈现与人生主轴",
2: "资源、表达与家庭积累",
@@ -295,6 +338,49 @@ def _lord_facts(packet: dict) -> dict[str, dict[str, Any]]:
return facts
def _period_beats(lord: str, language: str) -> str:
"""Four traditional beats. Wording stays possible, not certain."""
bank = _PHALA.get(lord) or _PHALA["Saturn"]
topic, good, pressure = bank["en" if language == "en" else "zh"]
name = _planet(lord, language)
if language == "zh":
return (
f"这段时间可能突出的主题:传统阅读里,{name}容易把{topic}推到眼前。"
"这只是可能被注意到的主题,不是已经发生的事。"
f"条件好时可能:若落宫、尊贵和当前大运同向,可能出现{good}。"
"仍要与宫主、分盘和星宿合看,不能单凭这一层下结论。"
f"受压时可能表现为:若该星受压或与当前节奏不一致,可能感到{pressure}。"
"这是压力下的可能表现,不是确定会发生的事。"
f"怎么细读:先核对{name}的落宫、守护宫、尊贵、星宿和功能吉凶,"
"再看它是否出现在主运、子运或次子运。只把它当成阶段线索。"
)
return (
f"Topics this period may bring forward: in the traditional reading, {name} may bring {topic} forward. "
"These are topics that may be noticed, not events that have already happened. "
f"When conditions are supportive, it may show {good}. "
"House lords, divisional charts, and the nakshatra still have to be read with it. "
f"Under pressure, it may show as {pressure}. "
"That is a possible expression under pressure, not a certain event. "
f"How to read it more closely: check the house, ownership, dignity, nakshatra, and functional nature of {name}, "
"then whether it is the Maha, Antar, or Pratyantar lord. Treat it as a period clue."
)
def _ruled_houses_clause(lord: str, owned: list, language: str) -> str:
"""Rahu and Ketu rule no houses. Other empty lists stay an explicit gap."""
if lord in _NODE_LORDS:
return "不守护宫位" if language == "zh" else "rules no house"
if language == "zh":
if owned:
return "守护第" + "、".join(str(item) for item in owned) + "宫"
return "守护宫未单独列出"
if owned:
return "owns houses " + ", ".join(str(item) for item in owned)
return "owned houses are not listed"
def _themes(lord: str, language: str) -> list[str]:
bank = _THEMES.get(lord) or {}
pair = bank.get(language) or bank.get("en") or ()
@@ -424,7 +510,11 @@ def _dasha_pages(master: dict, items: list[dict], language: str) -> list[dict[st
)
current_ad = next((row for row in antardashas if isinstance(row, dict) and row.get("is_current")), None)
if language == "zh":
general = f"{_planet(lord, language)} 主运期间,这段时间可能突出该星的自然主题。下面只按已算好的落宫、守护宫、尊贵、星宿和功能吉凶来读,不是确定会发生的事。"
general = (
f"{_planet(lord, language)} 主运期间,这段时间可能突出该星的自然主题。"
"下面只按已算好的落宫、守护宫、尊贵、星宿和功能吉凶来读,不是确定会发生的事。"
+ _period_beats(lord, language)
)
natal = _natal_zh(lord, facts, trigger)
current_text = (
f"当前子运(AD)是 {_planet(_dict(current_ad).get('lord'), language)},{current_ad.get('start') or '?'} 至 {current_ad.get('end') or '?'}。它只细化当前节奏。"
@@ -432,7 +522,12 @@ def _dasha_pages(master: dict, items: list[dict], language: str) -> list[dict[st
else "这一主运不是当前阶段,子运保留在时间表里。"
)
else:
general = f"During the Maha Dasha of {lord}, this period may bring the planet's natural topics forward. The reading uses only the calculated house, ownership, dignity, nakshatra, and functional nature. It is not a certain event."
general = (
f"During the Maha Dasha of {lord}, this period may bring the planet's natural topics forward. "
"The reading uses only the calculated house, ownership, dignity, nakshatra, and functional nature. "
"It is not a certain event. "
+ _period_beats(lord, language)
)
natal = _natal_en(lord, facts, trigger)
current_text = (
f"The current Antar Dasha (AD) is {current_ad.get('lord')}, {current_ad.get('start') or '?'} to {current_ad.get('end') or '?'} . It only refines the current pace."
@@ -494,8 +589,7 @@ def _natal_zh(lord: str, facts: dict, trigger: dict) -> str:
house = facts.get("house")
owned = facts.get("ownership") if isinstance(facts.get("ownership"), list) else []
bits = [f"{_planet(lord, 'zh')}在本命盘位于第{house}宫" if isinstance(house, int) else f"{_planet(lord, 'zh')}的落宫未列出"]
if owned:
bits.append("守护第" + "、".join(str(item) for item in owned) + "宫")
bits.append(_ruled_houses_clause(lord, owned, "zh"))
bits.append(f"尊贵为{_dignity(facts.get('dignity'), 'zh')}")
bits.append(f"星宿为{facts.get('nakshatra') or '未列出'}")
bits.append(f"功能吉凶为{_role(trigger.get('functional_classification'), 'zh')}")
@@ -505,12 +599,12 @@ def _natal_zh(lord: str, facts: dict, trigger: dict) -> str:
def _natal_en(lord: str, facts: dict, trigger: dict) -> str:
house = facts.get("house") if isinstance(facts.get("house"), int) else "not listed"
owned = facts.get("ownership") if isinstance(facts.get("ownership"), list) else []
owned_text = ", ".join(str(item) for item in owned) if owned else "not listed"
ownership = _ruled_houses_clause(lord, owned, "en")
nakshatra = facts.get("nakshatra") or "not listed"
if re.search(r"[\u4e00-\u9fff]", str(nakshatra)):
nakshatra = "not listed separately"
return (
f"Natal basis: {lord} is in house {house}, owns houses {owned_text}, "
f"Natal basis: {lord} is in house {house}, {ownership}, "
f"dignity {_dignity(facts.get('dignity'), 'en')}, nakshatra {nakshatra}, "
f"functional nature {_role(trigger.get('functional_classification'), 'en')}. "
"Functional nature comes from this site's functional benefic/malefic layer."
@@ -533,25 +627,26 @@ def _sub_paragraph(parent: str, lord: str, row: dict, facts: dict, language: str
owned = facts.get("ownership") if isinstance(facts.get("ownership"), list) else []
start = row.get("start") or "?"
end = row.get("end") or "?"
ruled = _ruled_houses_clause(lord, owned, language)
if language == "zh":
owned_text = "、".join(str(item) for item in owned) if owned else "未列出"
house_text = str(house) if house is not None else "未列出"
return (
f"{_planet(parent, language)} / {_planet(lord, language)}({level}):{start} 至 {end}。"
f"这段时间可能突出{_planet(lord, language)}的第{house_text}宫、守护第{owned_text}宫、"
f"这段时间可能突出{_planet(lord, language)}的第{house_text}宫、{ruled}、"
f"尊贵{_dignity(facts.get('dignity'), language)}与星宿{facts.get('nakshatra') or '未列出'}。"
"这是阶段线索,不是确定会发生的事。"
+ _period_beats(lord, language)
)
owned_text = ", ".join(str(item) for item in owned) if owned else "not listed"
house_text = str(house) if house is not None else "not listed"
nakshatra = facts.get("nakshatra") or "not listed"
if re.search(r"[\u4e00-\u9fff]", str(nakshatra)):
nakshatra = "not listed separately"
return (
f"{parent} / {lord} ({level}): {start} to {end}. "
f"This period may bring forward {lord}'s house {house_text}, owned houses {owned_text}, "
f"This period may bring forward {lord}'s house {house_text}, {ruled}, "
f"dignity {_dignity(facts.get('dignity'), language)}, and nakshatra {nakshatra}. "
"This is a period clue, not a certain event."
"This is a period clue, not a certain event. "
+ _period_beats(lord, language)
)
@@ -959,28 +1054,48 @@ def _field_usable(field: Any) -> bool:
return field.get("status") in {"partial_verified", "verified", "conflict", "parameter_sensitive"}
def project_annual_tables(annual: dict) -> None:
"""Copy existing annual fields onto the table names the reader already prints."""
def project_annual_tables(annual: dict, language: str = "zh") -> None:
"""Copy existing annual fields onto the table names the reader already prints.
Nothing here calculates a new chart. Empty producers stay empty.
"""
if not isinstance(annual, dict):
return
language = language if language in {"zh", "en"} else "zh"
normalized = annual.get("normalized_tables")
if not isinstance(normalized, dict):
normalized = {}
annual["normalized_tables"] = normalized
mudda = _dict(annual.get("mudda_dasha"))
mudda_data = _annual_data(mudda)
if not normalized.get("mudda_rows"):
periods = _list(mudda.get("periods") or _dict(mudda.get("data")).get("periods") or _dict(mudda.get("data")).get("dasha_sequence"))
periods = _list(mudda.get("periods") or mudda_data.get("periods") or mudda_data.get("dasha_sequence"))
rows = []
for period in periods:
if not isinstance(period, dict):
continue
lord = period.get("lord") or period.get("main_lord")
rows.append({
"balance_profile": period.get("balance_profile") or "native",
"lord": period.get("lord"),
"balance_profile": period.get("balance_profile") or mudda_data.get("balance_profile") or "native",
"lord": lord,
"start": period.get("start") or period.get("start_date"),
"end": period.get("end") or period.get("end_date"),
"boundary_status": mudda.get("status") or "partial_verified",
"boundary_status": "listed",
"reading": _mudda_reading(str(lord or ""), language),
})
for period in _list(mudda.get("antardasha_periods") or mudda_data.get("antardasha_periods")):
if not isinstance(period, dict):
continue
main_lord = period.get("main_lord")
sub_lord = period.get("sub_lord")
rows.append({
"balance_profile": "antardasha",
"lord": f"{main_lord}/{sub_lord}",
"start": period.get("start"),
"end": period.get("end"),
"boundary_status": "listed",
"reading": _mudda_reading(str(sub_lord or main_lord or ""), language),
})
if rows:
normalized["mudda_rows"] = rows
@@ -988,6 +1103,141 @@ def project_annual_tables(annual: dict) -> None:
normalized["saham_rows"] = _saham_rows(annual.get("sahams"))
if not normalized.get("tajika_yoga_rows"):
normalized["tajika_yoga_rows"] = _tajika_rows(annual.get("tajika_yogas"))
_project_annual_surfaces(annual, normalized, language)
def _project_annual_surfaces(annual: dict, normalized: dict, language: str) -> None:
"""Fill reader tables from fields the pack already holds."""
solar = _annual_data(annual.get("solar_return"))
chart = _annual_data(annual.get("annual_chart"))
year_lord = _annual_data(annual.get("year_lord"))
muntha = _annual_data(annual.get("muntha"))
if not normalized.get("true_solar_return_rows"):
rows = []
for key in ("dt_local", "dt_ut", "sun_lon"):
value = solar.get(key)
if value not in (None, ""):
rows.append({"field": key, "value": value, "dt_local": solar.get("dt_local")})
if rows:
normalized["true_solar_return_rows"] = rows
if not normalized.get("panchadhikari_rows"):
rows = []
for item in _list(year_lord.get("candidates")):
if not isinstance(item, dict):
continue
rows.append({
"planet": item.get("planet"),
"role": item.get("source") or item.get("role"),
"value": item.get("panchavargiya_score"),
"sign": item.get("planet_sign"),
"house": item.get("planet_house"),
})
if not rows and year_lord.get("year_lord"):
rows.append({
"planet": year_lord.get("year_lord"),
"role": year_lord.get("selection_basis"),
"value": year_lord.get("selection_basis_text_en") if language == "en" else year_lord.get("selection_basis_text"),
"sign": year_lord.get("year_lord_sign"),
"house": year_lord.get("year_lord_house"),
})
if rows:
normalized["panchadhikari_rows"] = rows
if not normalized.get("annual_anchor_rows"):
anchors = []
if solar.get("dt_local"):
anchors.append({"name": "Varshapravesha", "label": "solar return", "value": solar.get("dt_local")})
if chart.get("asc_sign"):
anchors.append({"name": "annual Lagna", "label": "ascendant", "sign": chart.get("asc_sign"), "value": chart.get("asc_sign")})
if muntha.get("muntha_sign"):
anchors.append({"name": "Muntha", "label": "muntha", "sign": muntha.get("muntha_sign"), "house": muntha.get("muntha_house"), "value": muntha.get("muntha_lord")})
if year_lord.get("year_lord"):
anchors.append({"name": "year lord", "label": "Panchadhikari", "value": year_lord.get("year_lord")})
if anchors:
normalized["annual_anchor_rows"] = anchors
longitudes = chart.get("planet_longitudes") if isinstance(chart.get("planet_longitudes"), dict) else {}
if longitudes and not normalized.get("annual_planet_rows"):
rows = []
for planet in PLANETS:
longitude = longitudes.get(planet)
if not isinstance(longitude, (int, float)) or isinstance(longitude, bool):
continue
rows.append({"planet": planet, "longitude": longitude, "degree": round(float(longitude) % 30, 2)})
if rows:
normalized["annual_planet_rows"] = rows
yoga_rows = _list(normalized.get("tajika_yoga_rows"))
if yoga_rows and not normalized.get("tajika_presence_rows"):
normalized["tajika_presence_rows"] = [
{"yoga": row.get("yoga"), "name": row.get("name"), "present": row.get("present"), "forming_planets": row.get("forming_planets")}
for row in yoga_rows
]
normalized["tajika_condition_status_rows"] = [
{"yoga": row.get("yoga"), "name": row.get("name"), "condition": row.get("condition"), "present": row.get("present")}
for row in yoga_rows
]
normalized["tajika_forming_pair_rows"] = [
{
"yoga": row.get("yoga"),
"name": row.get("name"),
"planet_a": row.get("planet_a"),
"planet_b": row.get("planet_b"),
"forming_planets": row.get("forming_planets"),
}
for row in yoga_rows
]
strength = [
{"planet": row.get("planet_a"), "yoga": row.get("yoga"), "strength": row.get("average_deeptamsa") or row.get("residual"), "value": row.get("average_deeptamsa") or row.get("residual")}
for row in yoga_rows
if row.get("average_deeptamsa") not in (None, "") or row.get("residual") not in (None, "")
]
if strength:
normalized["tajika_strength_rows"] = strength
if not normalized.get("tajika_strength_rows"):
scored = _strength_rows(annual.get("tajika_strength"))
if scored:
normalized["tajika_strength_rows"] = scored
if not annual.get("pl9_current_year_results"):
sections = _current_year_sections(solar, chart, year_lord, muntha, language)
if sections:
annual["pl9_current_year_results"] = {"status": "partial_verified", "sections": sections}
if not annual.get("pl9_annual_dasha_results"):
dasha_rows = []
for row in _list(normalized.get("mudda_rows")):
if row.get("balance_profile") == "antardasha":
continue
dasha_rows.append({
"order": len(dasha_rows) + 1,
"lord": row.get("lord"),
"start": row.get("start"),
"end": row.get("end"),
"themes": [row.get("reading")],
})
if dasha_rows:
annual["pl9_annual_dasha_results"] = {"status": "partial_verified", "rows": dasha_rows}
def _current_year_sections(solar: dict, chart: dict, year_lord: dict, muntha: dict, language: str) -> list[dict[str, Any]]:
sections = []
if solar.get("dt_local") or chart.get("asc_sign"):
if language == "zh":
theme = "真实太阳返照已经算出。这一年可能从年返上升和返照时刻看出环境变化,不是确定会发生的事。"
else:
theme = "The solar return is already calculated. The year may show a change of setting from the annual ascendant and the return time. This is not a certain event."
sections.append({"title": "solar return", "anchor": {"dt_local": solar.get("dt_local"), "asc_sign": chart.get("asc_sign")}, "themes": [theme]})
lord = year_lord.get("year_lord")
if lord:
if language == "zh":
theme = f"年主是{_planet(lord, 'zh')}。条件好时可能突出该星的年度主题;受压时可能表现为节奏需要重看。年主依据来自 Panchadhikari,不是另选一颗星。"
else:
theme = f"The year lord is {lord}. Supportive conditions may bring that planet's annual topics forward. Under pressure the pace may need another look. The basis is Panchadhikari."
sections.append({"title": "year lord", "anchor": {"year_lord": lord}, "themes": [theme]})
if muntha.get("muntha_sign") or muntha.get("muntha_lord"):
if language == "zh":
theme = "Muntha 标出这一年更容易被注意到的宫位。它可能加重该宫主题,不是单独的事件结论。"
else:
theme = "Muntha marks the house that may be easier to notice this year. It may emphasize that house. It is not a separate event conclusion."
sections.append({"title": "Muntha", "anchor": {"sign": muntha.get("muntha_sign"), "lord": muntha.get("muntha_lord")}, "themes": [theme]})
return sections
def _saham_rows(field: Any) -> list[dict[str, Any]]:
@@ -1028,15 +1278,57 @@ def _tajika_rows(field: Any) -> list[dict[str, Any]]:
for item in candidates:
if not isinstance(item, dict):
continue
planets = item.get("forming_planets") or item.get("planets")
pair = list(planets) if isinstance(planets, list) else []
rows.append({
"yoga": item.get("yoga") or item.get("name") or item.get("id"),
"name": item.get("name") or item.get("yoga"),
"present": item.get("present") if item.get("present") is not None else item.get("status") or item.get("formed"),
"forming_planets": item.get("forming_planets") or item.get("planets"),
"yoga": item.get("yoga") or item.get("named_yoga") or item.get("name") or item.get("candidate_name") or item.get("id"),
"name": item.get("name") or item.get("named_yoga") or item.get("candidate_name") or item.get("yoga"),
"present": item.get("present") if item.get("present") is not None else item.get("motion") or item.get("formed"),
"forming_planets": planets,
"motion": item.get("motion"),
"aspect": item.get("aspect"),
"average_deeptamsa": item.get("average_deeptamsa"),
"residual": item.get("residual"),
"planet_a": pair[0] if pair else None,
"planet_b": pair[1] if len(pair) > 1 else None,
"condition": item.get("condition") or item.get("motion") or item.get("aspect"),
})
return [row for row in rows if row.get("yoga") or row.get("name")]
def _annual_data(field: Any) -> dict:
if not isinstance(field, dict):
return {}
data = field.get("data")
return data if isinstance(data, dict) else field
def _mudda_reading(lord: str, language: str) -> str:
name = _planet(lord, "zh" if language == "zh" else "en")
if language == "zh":
return f"{name}这一段可能突出该星的年度主题。条件好时可能事情更顺,受压时可能表现为节奏变慢或需要重谈。这不是确定会发生的事。"
return f"The {lord} span may bring that planet's annual topics forward. Supportive conditions may make the work easier. Under pressure the pace may slow or an agreement may need another look. This is not a certain event."
def _strength_rows(field: Any) -> list[dict[str, Any]]:
"""Numeric Harsha and five-fold scores only. Do not print the internal status word."""
data = _annual_data(field)
rows = []
for layer, key in (("Harsha", "harsha_bala"), ("five-fold", "panchavargiya_bala")):
table = data.get(key)
if not isinstance(table, dict):
continue
for planet, item in table.items():
if not isinstance(item, dict):
continue
score = item.get("score")
if not isinstance(score, (int, float)) or isinstance(score, bool):
continue
rows.append({"planet": planet, "yoga": layer, "strength": score, "value": score})
return rows
def annual_coverage(annual: dict | None) -> list[dict[str, str]]:
"""Each upstream annual chapter is present or named with the missing field."""
@@ -1224,5 +1516,5 @@ def attach_full_data_interpretation(packet: dict) -> dict:
)
annual = timing.get("annual_tajika_pack")
if isinstance(annual, dict):
project_annual_tables(annual)
project_annual_tables(annual, language)
return packet
+74 -8
View File
@@ -11,6 +11,48 @@ import re
from typing import Any
def replace_natural_language_term(text: str, raw: str, public: str) -> str:
"""Replace one glossary term in prose only.
Identifiers and backtick spans stay as written. A trailing English plural
``s`` is consumed with the English word, so it is not left after Chinese.
"""
if not text or not raw:
return text
plural = "s?" if re.search(r"[A-Za-z]$", raw) and not raw.endswith("s") else ""
pattern = re.compile(rf"(?<![A-Za-z0-9_]){re.escape(raw)}{plural}(?![A-Za-z0-9_])")
parts = re.split(r"(`[^`]*`)", text)
return "".join(
part if index % 2 else pattern.sub(public, part)
for index, part in enumerate(parts)
)
_LIFESPAN_ASSERTION = re.compile(
r"长寿|寿命长|短寿|寿命|longevity|lifespan|long life|short life",
re.IGNORECASE,
)
_LIFESPAN_EFFECT_ZH = "传统上与体质和恢复力有关,仅作参考"
_LIFESPAN_EFFECT_EN = "Traditionally associated with constitution and recovery. Reference only."
def strip_han_plural_s(text: str) -> str:
"""Remove a Latin plural s left on Chinese after a glossary replace."""
return re.sub(r"([一-鿿])s\b", r"\1", text)
def soften_lifespan_wording(text: str, language: str = "zh") -> str:
"""Drop lifespan assertions from an effect cell. Yoga names are not passed here."""
if not text or not _LIFESPAN_ASSERTION.search(str(text)):
return text
if language == "en":
return _LIFESPAN_EFFECT_EN
return _LIFESPAN_EFFECT_ZH
ZH_TERM_REPLACEMENTS: tuple[tuple[str, str], ...] = (
("Natural friends", "天然友星"),
("Natural enemies", "天然敌星"),
@@ -221,10 +263,22 @@ _ZH_LITERAL_REPLACEMENTS = tuple(
if not re.fullmatch(r"[A-Za-z0-9_ /:-]+", raw)
)
_ZH_REGEX_REPLACEMENT_MAP = {raw: public for raw, public in _ZH_REGEX_REPLACEMENTS}
def _zh_regex_body(raw: str) -> str:
body = re.escape(raw)
if re.search(r"[A-Za-z]$", raw) and not raw.endswith("s"):
body += "s?"
return body
_ZH_REGEX_REPLACEMENT_PATTERN = re.compile(
r"(?<![A-Za-z])("
+ "|".join(re.escape(raw) for raw, _public in sorted(_ZH_REGEX_REPLACEMENTS, key=lambda item: len(item[0]), reverse=True))
+ r")(?![A-Za-z])"
r"(?<![A-Za-z0-9_])("
+ "|".join(
_zh_regex_body(raw)
for raw, _public in sorted(_ZH_REGEX_REPLACEMENTS, key=lambda item: len(item[0]), reverse=True)
)
+ r")(?![A-Za-z0-9_])"
)
_ZH_NAKSHATRA_GLOSSES = {
@@ -262,11 +316,23 @@ _ZH_NAKSHATRA_GLOSS_PATTERN = re.compile(
)
def _zh_regex_public(token: str) -> str:
if token in _ZH_REGEX_REPLACEMENT_MAP:
return _ZH_REGEX_REPLACEMENT_MAP[token]
if token.endswith("s") and token[:-1] in _ZH_REGEX_REPLACEMENT_MAP:
return _ZH_REGEX_REPLACEMENT_MAP[token[:-1]]
return token
def _replace_zh_regex_terms(markdown: str) -> str:
return _ZH_REGEX_REPLACEMENT_PATTERN.sub(
lambda match: _ZH_REGEX_REPLACEMENT_MAP[match.group(1)],
markdown,
)
def apply(chunk: str) -> str:
return _ZH_REGEX_REPLACEMENT_PATTERN.sub(
lambda match: _zh_regex_public(match.group(1)),
chunk,
)
parts = re.split(r"(`[^`]*`)", markdown)
return "".join(part if index % 2 else apply(part) for index, part in enumerate(parts))
def normalize_zh_markdown_terms(markdown: str) -> str:
@@ -321,7 +387,7 @@ def normalize_zh_markdown_terms(markdown: str) -> str:
text = _normalize_zh_dasha_prose(text)
text = _thicken_zh_dasha_density(text)
text = _normalize_mula_gloss(_restore_zh_nakshatra_glosses(_collapse_duplicate_zh_parentheses(text)))
return text
return strip_han_plural_s(text)
def _normalize_zh_bhava_factor_text(markdown: str) -> str:
+334 -112
View File
@@ -226,7 +226,7 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str:
"neecha_bhanga": "落陷取消",
"dhana": "财富",
"putra": "子女/创造",
"ayur": "寿命/健康",
"ayur": "体质参考",
"auspicious": "吉性",
"nabhasa": "天象格局",
"surya": "太阳类",
@@ -258,8 +258,12 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str:
"Aquarius": "水瓶座",
"Pisces": "双鱼座",
}
try:
from pl9_language_terms import replace_natural_language_term as _replace_term
except ModuleNotFoundError: # pragma: no cover
from scripts.pl9_language_terms import replace_natural_language_term as _replace_term
for raw, public in sorted(glossary_terms.items(), key=lambda item: len(item[0]), reverse=True):
text = re.sub(rf"(?<![A-Za-z]){re.escape(raw)}(?![A-Za-z])", public, text)
text = _replace_term(text, raw, public)
text = re.sub(r"\b[a-zA-Z_]+_profile\b", "计算口径", text)
text = re.sub(r"\b[a-zA-Z_]+_status\b", "状态", text)
text = re.sub(r"\b[a-zA-Z_]+_producer\b", "计算项", text)
@@ -306,7 +310,6 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str:
"太阳 and 水星": "太阳与水星",
"金星 and 水星": "金星与水星",
"Lagna": "上升",
"Kendra": "角宫",
"Trikona": "三方宫",
"Trine": "三方宫",
"Navamsa": "九分盘",
@@ -331,6 +334,7 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str:
}
for raw, public in natural_replacements.items():
text = text.replace(raw, public)
text = _replace_term(text, "Kendra", "角宫")
cleanups = {
"Special 上升s and Points": "特殊上升与派生点",
"Special 上升s": "特殊上升",
@@ -353,7 +357,11 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str:
text = re.sub(r"(?<=[\u4e00-\u9fff])\s+大运", "大运", text)
text = re.sub(r"大运\s+(?=[\u4e00-\u9fff])", "大运", text)
text = re.sub(r"(太阳|月亮|火星|水星|木星|金星|土星|北交点/罗睺|南交点/计都)\1", r"\1", text)
return text
try:
from pl9_language_terms import strip_han_plural_s
except ModuleNotFoundError: # pragma: no cover
from scripts.pl9_language_terms import strip_han_plural_s
return strip_han_plural_s(text)
def _sanitize_pl9_public_markdown(markdown: str) -> str:
@@ -410,9 +418,6 @@ def _render_pl9_user_markdown(packet: dict) -> str:
"{planets}": "相关行星",
"natural_benefic": "自然吉星",
"natural_malefic": "自然凶星",
"functional_benefic": "功能吉星",
"functional_malefic": "功能凶星",
"functional_neutral": "功能中性",
"functional_role_not_returned": "功能属性未单独返回",
"absolute Virupas; precise Sthana/Dig/Kala/Drik; bounded BPHS Chesta": "以 Virupa 为单位,综合 Sthana、Dig、Kala、Drik 与 BPHS Chesta",
"Chesta remains cross-engine method-conflicted; totals inherit that boundary.": "Chesta Bala 的算法口径会影响总分,阅读时以同一口径内比较为主。",
@@ -428,6 +433,16 @@ def _render_pl9_user_markdown(packet: dict) -> str:
}
for raw, public in replacements.items():
text = text.replace(raw, public)
try:
from pl9_language_terms import replace_natural_language_term
except ModuleNotFoundError: # pragma: no cover
from scripts.pl9_language_terms import replace_natural_language_term
for raw, public in (
("functional_benefic", "功能吉星"),
("functional_malefic", "功能凶星"),
("functional_neutral", "功能中性"),
):
text = replace_natural_language_term(text, raw, public)
text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE)
drop_fragments = (
"本册只编排原版",
@@ -559,15 +574,25 @@ def render_pl9_parity_markdown(packet: dict) -> str:
if field.get('status') in {'blocked', 'not_applicable'}:
return '-'
if field.get('status') == 'conflict':
labels = {'solar_return': '太阳返照', 'tajika': 'Tajika'}
labels = (
{'solar_return': 'solar return', 'tajika': 'Tajika'}
if _lang == 'en'
else {'solar_return': '太阳返照', 'tajika': 'Tajika'}
)
other = 'another method' if _lang == 'en' else '另一口径'
values = []
for item in _list(field.get('values')):
item = _dict(item)
data = _dict(item.get('value'))
value = next((data.get(key) for key in keys if data.get(key)), None)
if value is not None:
values.append(f"{labels.get(item.get('producer'), '另一口径')}:{_token(value)}")
return ';'.join(values) + '(两者不同)' if values else '-'
sep = ': ' if _lang == 'en' else ':'
values.append(f"{labels.get(item.get('producer'), other)}{sep}{_token(value)}")
if not values:
return '-'
if _lang == 'en':
return '; '.join(values) + ' (the two differ)'
return ';'.join(values) + '(两者不同)'
data = _dict(field.get('data') or field)
return _token(next((data.get(key) for key in keys if data.get(key)), None))
@@ -575,13 +600,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
year_lord = _dict(year_lord)
blocked_detail = _dict(year_lord.get('blocked_detail'))
if year_lord.get('status') == 'blocked' and blocked_detail.get('selection_basis_text'):
# BUG-1223: no stand-in year lord; 「年主暂无法确定(原因)」.
# BUG-1223: no stand-in year lord. Chinese keeps 「年主暂无法确定(原因)」.
# English uses the twin sentence and does not keep the Chinese source line.
if _lang == 'en' and blocked_detail.get('selection_basis_text_en'):
return _cell(blocked_detail['selection_basis_text_en'])
return _cell(blocked_detail['selection_basis_text'])
if year_lord.get('status') == 'conflict':
if _lang == 'en':
return 'The two calculations name different year lords, so they are not merged'
return '两种计算口径给出的年主不同,暂不合并'
data = _dict(year_lord.get('data') or year_lord)
# BUG-1214: the producer's one-sentence Panchadhikari basis (replaces
# the BUG-1212 "Muntha lord for now" stand-in).
if _lang == 'en' and data.get('selection_basis_text_en'):
return _cell(data['selection_basis_text_en'])
if data.get('selection_basis_text'):
return _cell(data['selection_basis_text'])
if data.get('selection_basis'):
@@ -666,18 +698,31 @@ def render_pl9_parity_markdown(packet: dict) -> str:
def _public_report_text(value) -> str:
text = str(value or '').strip()
replacements = {
'parameter_sensitive': '参考',
'blocked': '未列',
'observed_relationships': '已形成',
'observed_empty': '未形成',
'external_replay_pending': '待补充',
'Evidence label': '证据口径',
'证据标签': '证据口径',
}
text = text.replace('blocked_external_callable', '外部回放待校对')
text = text.replace('pyjhora_behavior_only / not_multiengine_parity', '外部行为参考')
text = text.replace('pyjhora_behavior_only', '外部行为参考')
if _lang == 'en':
replacements = {
'blocked_external_callable': 'external replay still to review',
'pyjhora_behavior_only / not_multiengine_parity': 'external behavior note',
'pyjhora_behavior_only': 'external behavior note',
'parameter_sensitive': 'reference',
'blocked': 'not supplied',
'observed_relationships': 'formed',
'observed_empty': 'not formed',
'external_replay_pending': 'to be supplied',
'Evidence label': 'note',
}
else:
replacements = {
'blocked_external_callable': '外部回放待校对',
'pyjhora_behavior_only / not_multiengine_parity': '外部行为参考',
'pyjhora_behavior_only': '外部行为参考',
'parameter_sensitive': '参考',
'blocked': '未列',
'observed_relationships': '已形成',
'observed_empty': '未形成',
'external_replay_pending': '待补充',
'Evidence label': '证据口径',
'证据标签': '证据口径',
}
for raw, public in replacements.items():
text = text.replace(raw, public)
return text
@@ -2047,12 +2092,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
current_year = _dict(annual.get('pl9_current_year_results'))
current_sections = _list(current_year.get('sections'))
if current_sections:
lines.extend([
'### p139-p141 Varshaphala 年度结果解释',
'',
'| 项目 | 锚点 | 年度主题 |',
'|------|------|----------|',
])
if _lang == 'en':
lines.extend([
'### Varshaphala annual result notes',
'',
'| Item | Anchor | Annual theme |',
'|------|--------|---------------|',
])
else:
lines.extend([
'### p139-p141 Varshaphala 年度结果解释',
'',
'| 项目 | 锚点 | 年度主题 |',
'|------|------|----------|',
])
for section in current_sections:
if not isinstance(section, dict):
continue
@@ -2070,12 +2123,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
annual_dasha = _dict(annual.get('pl9_annual_dasha_results'))
dasha_rows = _list(annual_dasha.get('rows'))
if dasha_rows:
lines.extend([
'### p144-p145 Annual Dasha 结果解释',
'',
'| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |',
'|------|------|----------|----------|------|',
])
if _lang == 'en':
lines.extend([
'### Annual Dasha result notes',
'',
'| Stage | Lord | Annual house | Date boundary | Theme |',
'|-------|------|---------------|---------------|-------|',
])
else:
lines.extend([
'### p144-p145 Annual Dasha 结果解释',
'',
'| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |',
'|------|------|----------|----------|------|',
])
for row in dasha_rows:
if not isinstance(row, dict):
continue
@@ -2102,6 +2163,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
f"{_cell(row.get('start'))} | {_cell(row.get('end'))} | {_public_status_note(row.get('boundary_status'))} |"
)
lines.append('')
readings = [str(row.get('reading')).strip() for row in mudda_rows if isinstance(row, dict) and str(row.get('reading') or '').strip()]
if readings:
lines.extend([
'#### Mudda period notes' if _lang == 'en' else '#### Mudda 阶段可能主题',
'',
])
for reading in readings:
lines.append(f"- {_public_report_text(reading)}")
lines.append('')
if _lang == 'en':
lines.append('Day-level rows and a second balance profile are not in this table.')
else:
lines.append('这张表没有日级行,也没有第二套余额口径。')
lines.append('')
patyayini_rows = _list(normalized.get('patyayini_rows'))
if patyayini_rows:
lines.extend([
@@ -2151,9 +2226,15 @@ def render_pl9_parity_markdown(packet: dict) -> str:
if not isinstance(row, dict):
continue
forming = row.get('forming_planets') or row.get('planets') or row.get('raw_relationships')
motion_key = str(row.get('motion') or row.get('aspect_motion') or '-')
motion_labels = (
{'applying': 'applying', 'separating': 'separating'}
if _lang == 'en'
else {'applying': '入相', 'separating': '分离'}
)
lines.append(
f"| {_cell(row.get('yoga') or row.get('name'))} | {_cell(_public_report_text(row.get('observation_status') or row.get('condition_status') or row.get('present')))} | "
f"{_cell(forming)} | {_cell({'applying': '入相', 'separating': '分离'}.get(str(row.get('motion') or row.get('aspect_motion') or '-'), row.get('motion') or row.get('aspect_motion') or '-'))} |"
f"{_cell(forming)} | {_cell(motion_labels.get(motion_key, motion_key))} |"
)
lines.append('')
saham_rows = _list(normalized.get('saham_rows'))
@@ -2175,26 +2256,40 @@ def render_pl9_parity_markdown(packet: dict) -> str:
public_replay = _dict(annual.get('public_panchavargiya_replay'))
replay_coverage = _dict(public_replay.get('coverage'))
if public_replay:
lines.extend([
'### 公开 Panchavargiya 数值复核边界',
'',
'| 项目 | 结果 |',
'|------|------|',
f"| 公开复核状态 | {_cell(public_replay.get('status'))} |",
f"| 覆盖行星 | {_cell(replay_coverage.get('planet_count'))} |",
f"| 已匹配分量 | {_cell(replay_coverage.get('exact_component_match_count'))} / {_cell(replay_coverage.get('component_count'))} |",
f"| 已匹配总分 | {_cell(replay_coverage.get('exact_total_match_count'))} / {_cell(replay_coverage.get('planet_count'))} |",
f"| 可直接用于公式升级 | {_cell(public_replay.get('promotion_allowed'))} |",
'',
])
if _lang == 'en':
lines.extend([
'### Public Panchavargiya value check',
'',
'| Item | Result |',
'|------|--------|',
f"| Public check status | {_cell(public_replay.get('status'))} |",
f"| Planets covered | {_cell(replay_coverage.get('planet_count'))} |",
f"| Components matched | {_cell(replay_coverage.get('exact_component_match_count'))} / {_cell(replay_coverage.get('component_count'))} |",
f"| Totals matched | {_cell(replay_coverage.get('exact_total_match_count'))} / {_cell(replay_coverage.get('planet_count'))} |",
f"| Usable for a formula upgrade | {_cell(public_replay.get('promotion_allowed'))} |",
'',
])
else:
lines.extend([
'### 公开 Panchavargiya 数值复核边界',
'',
'| 项目 | 结果 |',
'|------|------|',
f"| 公开复核状态 | {_cell(public_replay.get('status'))} |",
f"| 覆盖行星 | {_cell(replay_coverage.get('planet_count'))} |",
f"| 已匹配分量 | {_cell(replay_coverage.get('exact_component_match_count'))} / {_cell(replay_coverage.get('component_count'))} |",
f"| 已匹配总分 | {_cell(replay_coverage.get('exact_total_match_count'))} / {_cell(replay_coverage.get('planet_count'))} |",
f"| 可直接用于公式升级 | {_cell(public_replay.get('promotion_allowed'))} |",
'',
])
annual_extra_specs = (
('annual_anchor_rows', 'p126 年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')),
('annual_planet_rows', 'p127 年度行星资料', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')),
('annual_anchor_rows', 'Annual anchors' if _lang == 'en' else 'p126 年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')),
('annual_planet_rows', 'Annual planet data' if _lang == 'en' else 'p127 年度行星资料', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')),
('true_solar_return_rows', 'True Solar Return', ('field', 'value', 'date', 'time', 'dt_local')),
('panchadhikari_rows', 'Panchadhikari', ('planet', 'role', 'value', 'sign', 'house')),
('tripataki_placement_rows', 'Tripataki Placement', ('planet', 'sign', 'house', 'degree', 'placement')),
('tripataki_vedha_rows', 'Tripataki Vedha', ('planet', 'target', 'sign', 'house', 'result')),
('current_year_result_rows', 'p139-p141 年度结果', ('title', 'value', 'theme', 'summary', 'anchor')),
('current_year_result_rows', 'Annual result notes' if _lang == 'en' else 'p139-p141 年度结果', ('title', 'value', 'theme', 'summary', 'anchor')),
('tajika_presence_rows', 'Tajika Presence', ('yoga', 'name', 'present', 'forming_planets')),
('tajika_condition_status_rows', 'Tajika Condition', ('yoga', 'name', 'condition', 'present')),
('tajika_forming_pair_rows', 'Tajika Forming Pair', ('yoga', 'name', 'planet_a', 'planet_b', 'forming_planets')),
@@ -2282,12 +2377,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
monthly_pack = _dict(annual.get('monthly_chart_pack'))
monthly_rows = _list(monthly_pack.get('rows'))
if monthly_rows:
lines.extend([
'### 年度月返照图盘数据',
'',
'| 月份 | 本地时刻 | 年盘上升 | 上升度数 | 行星黄经(度) |',
'|------|----------|----------|----------|-----------------|',
])
if _lang == 'en':
lines.extend([
'### Monthly returns within the year',
'',
'| Month | Local time | Annual ascendant | Ascendant degree | Planet longitudes (deg) |',
'|-------|------------|------------------|------------------|--------------------------|',
])
else:
lines.extend([
'### 年度月返照图盘数据',
'',
'| 月份 | 本地时刻 | 年盘上升 | 上升度数 | 行星黄经(度) |',
'|------|----------|----------|----------|-----------------|',
])
for row in monthly_rows:
if not isinstance(row, dict):
continue
@@ -2311,12 +2414,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
for row in _list(page.get('rows')) if isinstance(row, dict)
]
if lifetime_rows:
lines.extend([
'### 生命周期年度返照图盘数据',
'',
'| 年龄 | 年份 | 年返时刻 | 上升 | 上升度数 | 行星黄经(度) |',
'|------|------|----------|------|----------|-----------------|',
])
if _lang == 'en':
lines.extend([
'### Lifetime annual return charts',
'',
'| Age | Year | Return time | Ascendant | Ascendant degree | Planet longitudes (deg) |',
'|-----|------|-------------|-----------|------------------|--------------------------|',
])
else:
lines.extend([
'### 生命周期年度返照图盘数据',
'',
'| 年龄 | 年份 | 年返时刻 | 上升 | 上升度数 | 行星黄经(度) |',
'|------|------|----------|------|----------|-----------------|',
])
for row in lifetime_rows:
asc = _dict(row.get('ascendant'))
longitudes = _dict(row.get('planet_longitudes'))
@@ -2332,20 +2443,36 @@ def render_pl9_parity_markdown(packet: dict) -> str:
)
lines.append('')
annual_series = _dict(annual.get('annual_chart_series_pack'))
for series_key, title in (
('birth_place_eight_year_overview', '出生地八年年度总览'),
('local_eight_year_overview', '现居地八年年度总览'),
):
eight_year_titles = (
(
('birth_place_eight_year_overview', 'Eight-year overview at the birthplace'),
('local_eight_year_overview', 'Eight-year overview at the current residence'),
)
if _lang == 'en'
else (
('birth_place_eight_year_overview', '出生地八年年度总览'),
('local_eight_year_overview', '现居地八年年度总览'),
)
)
for series_key, title in eight_year_titles:
series = _dict(annual_series.get(series_key))
series_rows = _list(series.get('rows'))
if not series_rows:
continue
lines.extend([
f'### {title}',
'',
'| 年份 | 年龄 | 年返时刻 | 年盘上升 |',
'|------|------|----------|----------|',
])
if _lang == 'en':
lines.extend([
f'### {title}',
'',
'| Year | Age | Return time | Annual ascendant |',
'|------|-----|-------------|-------------------|',
])
else:
lines.extend([
f'### {title}',
'',
'| 年份 | 年龄 | 年返时刻 | 年盘上升 |',
'|------|------|----------|----------|',
])
for row in series_rows:
if not isinstance(row, dict):
continue
@@ -2358,7 +2485,41 @@ def render_pl9_parity_markdown(packet: dict) -> str:
annual_visual = annual.get('pl9_annual_visual_panels') if isinstance(annual.get('pl9_annual_visual_panels'), dict) else {}
panels = annual_visual.get('panels') if isinstance(annual_visual.get('panels'), list) else []
if panels:
lines.extend(['### p139-p155 年度北印度图盘', ''])
lines.extend([
'### Annual north-Indian chart' if _lang == 'en' else '### p139-p155 年度北印度图盘',
'',
])
else:
occupancy_longitudes = _dict(_dict(annual_chart.get('data')).get('planet_longitudes'))
if occupancy_longitudes:
signs = list(globals().get('SIGNS') or [])
asc_idx = _dict(annual_chart.get('data')).get('asc_sign_idx')
asc_name = _dict(annual_chart.get('data')).get('asc_sign')
if not isinstance(asc_idx, int) and signs and asc_name in signs:
asc_idx = signs.index(asc_name)
lines.extend([
'### Annual north-Indian chart' if _lang == 'en' else '### p139-p155 年度北印度图盘',
'',
(
'Sign occupancy from the annual longitudes already calculated. This is a text table, not a drawn chart.'
if _lang == 'en'
else '下面按已算出的年度黄经,把行星放进年返上升起的整宫。这是文字表,不是另画的图。'
),
'',
'| Planet | Sign | House from annual Lagna |' if _lang == 'en' else '| 行星 | 星座 | 自年返上升的宫 |',
'|--------|------|--------------------------|' if _lang == 'en' else '|------|------|----------------|',
])
if isinstance(asc_idx, int) and signs:
lines.append(f"| Lagna | {_cell(_token(signs[asc_idx % 12]))} | 1 |")
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
longitude = occupancy_longitudes.get(planet)
if not isinstance(longitude, (int, float)) or isinstance(longitude, bool):
continue
sign_idx = int(float(longitude) // 30) % 12
sign = signs[sign_idx] if signs else '-'
house = ((sign_idx - asc_idx) % 12) + 1 if isinstance(asc_idx, int) else '-'
lines.append(f"| {_cell(_token(planet))} | {_cell(_token(sign))} | {_cell(house)} |")
lines.append('')
for panel in panels:
if not isinstance(panel, dict):
continue
@@ -2378,13 +2539,25 @@ def render_pl9_parity_markdown(packet: dict) -> str:
annual_chart_data = _dict(annual_chart.get('data'))
solar_return = _dict(annual_pack.get('solar_return'))
solar_return_data = _dict(solar_return.get('data') or solar_return)
lines.extend([f'### {year} 年度 Varshaphala / Tajika 细项', ''])
if _lang == 'en':
lines.extend([f'### {year} annual Varshaphala / Tajika detail', ''])
else:
lines.extend([f'### {year} 年度 Varshaphala / Tajika 细项', ''])
if solar_return_data or annual_chart_data:
asc_label = 'Annual ascendant' if _lang == 'en' else '年返上升'
if _lang == 'en':
lines.extend([
'| Item | Value |',
'|------|-------|',
])
else:
lines.extend([
'| 项目 | 数值 |',
'|------|------|',
])
lines.extend([
'| 项目 | 数值 |',
'|------|------|',
f"| Varshapravesha | {_cell(solar_return_data.get('dt_local') or solar_return_data.get('local_time') or solar_return_data.get('date') or '-')} |",
f"| 年返上升 | {_cell(_token(annual_chart_data.get('asc_sign')))} |",
f"| {asc_label} | {_cell(_token(annual_chart_data.get('asc_sign')))} |",
])
year_lord = _dict(annual_pack.get('year_lord'))
year_lord_data = _dict(year_lord.get('data') or year_lord)
@@ -2398,12 +2571,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
planet_longitudes = _dict(annual_chart_data.get('planet_longitudes'))
if planet_longitudes:
signs = globals().get('SIGNS', [])
lines.extend([
'#### 年度行星资料',
'',
'| 行星 | 星座 | 落座度数 | 黄经 |',
'|------|------|----------|------|',
])
if _lang == 'en':
lines.extend([
'#### Annual planet data',
'',
'| Planet | Sign | Degree in sign | Longitude |',
'|--------|------|----------------|-----------|',
])
else:
lines.extend([
'#### 年度行星资料',
'',
'| 行星 | 星座 | 落座度数 | 黄经 |',
'|------|------|----------|------|',
])
if annual_chart_data.get('asc_sign') is not None:
lines.append(f"| Lagna | {_cell(_token(annual_chart_data.get('asc_sign')))} | - | - |")
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
@@ -2419,12 +2600,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
annual_dasha = _dict(annual_pack.get('pl9_annual_dasha_results'))
dasha_rows = _list(annual_dasha.get('rows'))
if dasha_rows:
lines.extend([
'#### Annual Dasha 阶段表',
'',
'| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |',
'|------|------|----------|----------|------|',
])
if _lang == 'en':
lines.extend([
'#### Annual Dasha periods',
'',
'| Stage | Lord | Annual house | Date boundary | Theme |',
'|-------|------|---------------|---------------|-------|',
])
else:
lines.extend([
'#### Annual Dasha 阶段表',
'',
'| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |',
'|------|------|----------|----------|------|',
])
for row in dasha_rows:
if not isinstance(row, dict):
continue
@@ -2435,15 +2624,26 @@ def render_pl9_parity_markdown(packet: dict) -> str:
)
lines.append('')
normalized = _dict(annual_pack.get('normalized_tables'))
future_specs = (
('mudda_rows', 'Mudda / Patyayini 年度 Dasha 表', ('balance_profile', 'lord', 'start', 'end', 'boundary_status')),
('patyayini_rows', 'Patyayini / Patyamsha ending-date 表', ('main', 'sub', 'boundary', 'boundary_status')),
('saham_rows', 'Sahams 点位表', ('name', 'saham', 'sign', 'degree_in_sign')),
('tajika_yoga_rows', 'Tajika Yoga Presence 表', ('yoga', 'name', 'present', 'forming_planets')),
('pyjhora_tajika_yoga_rows', 'Tajika Yoga Presence 表', ('yoga', 'name', 'present', 'forming_planets')),
('annual_planet_rows', '年度行星扩展字段', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')),
('annual_anchor_rows', '年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')),
)
if _lang == 'en':
future_specs = (
('mudda_rows', 'Mudda dasha table', ('balance_profile', 'lord', 'start', 'end', 'boundary_status')),
('patyayini_rows', 'Patyayini ending dates', ('main', 'sub', 'boundary', 'boundary_status')),
('saham_rows', 'Saham positions', ('name', 'saham', 'sign', 'degree_in_sign')),
('tajika_yoga_rows', 'Tajika yoga presence', ('yoga', 'name', 'present', 'forming_planets')),
('pyjhora_tajika_yoga_rows', 'Tajika yoga presence', ('yoga', 'name', 'present', 'forming_planets')),
('annual_planet_rows', 'Annual planet extras', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')),
('annual_anchor_rows', 'Annual anchors', ('name', 'label', 'value', 'sign', 'degree', 'house')),
)
else:
future_specs = (
('mudda_rows', 'Mudda / Patyayini 年度 Dasha 表', ('balance_profile', 'lord', 'start', 'end', 'boundary_status')),
('patyayini_rows', 'Patyayini / Patyamsha ending-date 表', ('main', 'sub', 'boundary', 'boundary_status')),
('saham_rows', 'Sahams 点位表', ('name', 'saham', 'sign', 'degree_in_sign')),
('tajika_yoga_rows', 'Tajika Yoga Presence 表', ('yoga', 'name', 'present', 'forming_planets')),
('pyjhora_tajika_yoga_rows', 'Tajika Yoga Presence 表', ('yoga', 'name', 'present', 'forming_planets')),
('annual_planet_rows', '年度行星扩展字段', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')),
('annual_anchor_rows', '年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')),
)
rendered_titles = set()
for table_key, title, preferred_keys in future_specs:
rows = [row for row in _list(normalized.get(table_key)) if isinstance(row, dict)]
@@ -2473,12 +2673,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
special = _dict(annual_pack.get('annual_special_points'))
special_data = _dict(special.get('data'))
if special_data:
lines.extend([
'#### 年度特殊点',
'',
'| 点位 | 星座 | 落座度数 | 黄经 | 守护星 |',
'|------|------|----------|------|--------|',
])
if _lang == 'en':
lines.extend([
'#### Annual special points',
'',
'| Point | Sign | Degree in sign | Longitude | Ruler |',
'|-------|------|----------------|-----------|-------|',
])
else:
lines.extend([
'#### 年度特殊点',
'',
'| 点位 | 星座 | 落座度数 | 黄经 | 守护星 |',
'|------|------|----------|------|--------|',
])
for name, row in special_data.items():
if not isinstance(row, dict):
continue
@@ -2491,12 +2699,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
monthly_pack = _dict(annual_pack.get('monthly_chart_pack'))
monthly_rows = _list(monthly_pack.get('rows'))
if monthly_rows:
lines.extend([
'#### 年度月返照图盘数据',
'',
'| 月份 | 本地时刻 | 年盘上升 | 上升度数 |',
'|------|----------|----------|----------|',
])
if _lang == 'en':
lines.extend([
'#### Monthly returns within the year',
'',
'| Month | Local time | Annual ascendant | Ascendant degree |',
'|-------|------------|------------------|-------------------|',
])
else:
lines.extend([
'#### 年度月返照图盘数据',
'',
'| 月份 | 本地时刻 | 年盘上升 | 上升度数 |',
'|------|----------|----------|----------|',
])
for row in monthly_rows:
if not isinstance(row, dict):
continue
@@ -2677,10 +2893,16 @@ def render_pl9_parity_markdown(packet: dict) -> str:
effects = row.get('effects') if isinstance(row.get('effects'), list) else []
yoga_name = row.get('name_cn') or row.get('name') or row.get('type')
combination = row.get('combination') or row.get('source_reference') or row.get('reason')
effect_text = _public_report_text(_brief(effects, 3) or row.get('interpretation') or row.get('summary'))
try:
from pl9_language_terms import soften_lifespan_wording
except ModuleNotFoundError: # pragma: no cover
from scripts.pl9_language_terms import soften_lifespan_wording
effect_text = soften_lifespan_wording(str(effect_text or ''), _lang)
lines.append(
f"| {_cell(yoga_name)} | {_cell(row.get('category') or row.get('subtype'))} | "
f"{_cell(row.get('strength'))} | {_cell(combination)} | "
f"{_cell(_public_report_text(_brief(effects, 3) or row.get('interpretation') or row.get('summary')))} |"
f"{_cell(effect_text)} |"
)
lines.append('')
if yoga_pages:
+11
View File
@@ -670,6 +670,17 @@ def solar_return_full_report(
# 9. 综合总结
result['summary'] = _make_sr_summary(result)
# Keep the longitudes and chart already calculated above so the report
# pack can print them. This does not recompute the return.
if not degraded:
result['planet_lons'] = {
name: float(value)
for name, value in planet_lons.items()
if isinstance(value, (int, float)) and not isinstance(value, bool)
}
chart = sr.get('chart') if isinstance(sr, dict) else None
if isinstance(chart, dict):
result['chart'] = chart
return result
+22 -2
View File
@@ -18,8 +18,10 @@ of the same rule. What it adds on top of the upstream function:
- `periods` are cropped to the annual window [annual_start, annual_end) and
carry `months` (share of 12) for the existing month-window consumers; the
upstream ten-group surface (the last group runs past the year end) stays
in `periods_full`. Upstream's sub-periods are not carried (no product
surface shows them).
in `periods_full`. Sub-periods already computed by
`calc_mudda_with_dasha_balance` are copied as `antardasha_periods` and
cropped to the same annual window. Day rows and a second balance profile
are not added here.
The legacy `tajika.calc_mudda_dasha` (first lord = year lord) is no longer
called from any product path.
@@ -90,6 +92,23 @@ def annual_mudda_dasha(
"start": p_start.isoformat(sep=" "),
"end": p_end.isoformat(sep=" "),
})
antardasha_periods = []
for row in raw.get("antardasha_periods") or []:
if not isinstance(row, dict) or not row.get("start") or not row.get("end"):
continue
p_start = max(_parse(row["start"]), start)
p_end = min(_parse(row["end"]), end)
if p_start >= p_end:
continue
antardasha_periods.append({
"main_order": row.get("main_order"),
"sub_order": row.get("sub_order"),
"main_lord": row.get("main_lord"),
"sub_lord": row.get("sub_lord"),
"duration_days": (p_end - p_start).total_seconds() / 86400.0,
"start": p_start.isoformat(sep=" "),
"end": p_end.isoformat(sep=" "),
})
return {
# The first-lord rule is the astrologer's round-5 ruling; dates are
# not externally replayed (upstream keeps its own parameter_sensitive).
@@ -110,6 +129,7 @@ def annual_mudda_dasha(
"balance_profile": raw.get("balance_profile"),
"balance_fraction_remaining": raw.get("balance_fraction_remaining"),
"periods": periods,
"antardasha_periods": antardasha_periods,
"periods_full": raw.get("periods"),
"independent_of_year_lord": True,
"source": SOURCE,