Sthira、Drig、Shoola、Navamsha、Niryaana Shoola 与 K.N. Rao 的 Chara 从本盘算出。Lagna Kendradi 不上表。未推送。
2299 lines
120 KiB
Python
2299 lines
120 KiB
Python
"""Full-data personal report, upstream pl9_ai_density at 23be1807.
|
||
|
||
Renderer and sanitizer only. Numbers come from this repository's engine.
|
||
Upstream calculation helpers are not copied. Sections whose packet fields are
|
||
absent are omitted. The private same-case PDF transcription in upstream
|
||
`_attach_pl9_source_visible_tables` is not copied. `customer_timing_supplement`
|
||
fits apparent positions and a -5 second node adjustment; red line 4 forbids
|
||
that profile, so it is not called.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy
|
||
import re
|
||
from collections.abc import Mapping
|
||
from typing import Any
|
||
|
||
try:
|
||
from scripts.pl9_reader_export import (
|
||
_pl9_english_report_title,
|
||
_pl9_public_report_title,
|
||
_pl9_report_language,
|
||
_sanitize_pl9_public_markdown,
|
||
_strip_pl9_user_engineering_markers,
|
||
render_pl9_parity_markdown,
|
||
)
|
||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||
from pl9_reader_export import (
|
||
_pl9_english_report_title,
|
||
_pl9_public_report_title,
|
||
_pl9_report_language,
|
||
_sanitize_pl9_public_markdown,
|
||
_strip_pl9_user_engineering_markers,
|
||
render_pl9_parity_markdown,
|
||
)
|
||
|
||
FULL_DATA_EDITION = "full_data"
|
||
REPORT_VERSION_FULL = "pl9_personal_long_report.v4"
|
||
FULL_DATA_DOMAINS = {
|
||
"career": "事业",
|
||
"wealth": "财富",
|
||
"relationship": "婚恋",
|
||
"health": "健康",
|
||
"children": "子女",
|
||
"parents": "父母",
|
||
"education": "学业",
|
||
"relocation": "迁居",
|
||
"family": "家庭",
|
||
"annual": "流年",
|
||
"timing": "时间",
|
||
"general": "综合",
|
||
}
|
||
|
||
|
||
def _pl9_public_term(value: Any) -> str:
|
||
"""Report-facing label for a planet or sign name."""
|
||
terms = {
|
||
"Sun": "太阳", "Moon": "月亮", "Mars": "火星", "Mercury": "水星",
|
||
"Jupiter": "木星", "Venus": "金星", "Saturn": "土星",
|
||
"Rahu": "北交点/罗睺", "Ketu": "南交点/计都",
|
||
"Aries": "白羊座", "Taurus": "金牛座", "Gemini": "双子座",
|
||
"Cancer": "巨蟹座", "Leo": "狮子座", "Virgo": "处女座",
|
||
"Libra": "天秤座", "Scorpio": "天蝎座", "Sagittarius": "射手座",
|
||
"Capricorn": "摩羯座", "Aquarius": "水瓶座", "Pisces": "双鱼座",
|
||
}
|
||
text = str(value or "").strip()
|
||
return terms.get(text, text or "-")
|
||
|
||
|
||
def _load_glossary():
|
||
try:
|
||
from scripts.pl9_en_glossary import CN_TERMS_EN, COMBINATION_EN, EFFECTS_EN
|
||
except ModuleNotFoundError:
|
||
from pl9_en_glossary import CN_TERMS_EN, COMBINATION_EN, EFFECTS_EN
|
||
return CN_TERMS_EN, COMBINATION_EN, EFFECTS_EN
|
||
|
||
|
||
def _load_sudarshana():
|
||
try:
|
||
from scripts.sudarshana_chakra import calc_sudarshana_chakra
|
||
except ModuleNotFoundError:
|
||
from sudarshana_chakra import calc_sudarshana_chakra
|
||
return calc_sudarshana_chakra
|
||
|
||
|
||
def _load_bhava_rows():
|
||
try:
|
||
from scripts.special_lagnas import bhava_house_rows
|
||
except ModuleNotFoundError:
|
||
from special_lagnas import bhava_house_rows
|
||
return bhava_house_rows
|
||
|
||
|
||
def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星星盘报告") -> str:
|
||
"""Keep the parity volume's facts while removing internal-only labels."""
|
||
|
||
try:
|
||
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,
|
||
replace_natural_language_term,
|
||
strip_han_plural_s,
|
||
)
|
||
|
||
text = _sanitize_pl9_public_markdown(markdown)
|
||
replacements = {
|
||
"# PL9 对标主册": f"# {title}",
|
||
"出生资料与图盘(PL9 p1-p17)": "出生资料与图盘",
|
||
"行星、星宿与宫位长页解释(PL9 p18-p29 对标)": "行星、星宿与宫位解释",
|
||
"强度、关系与 Ashtakavarga 技术页(PL9 p30-p60 对标)": "强度、关系与 Ashtakavarga",
|
||
"标准 Dasha 表格页(PL9 p61-p120 对标)": "Dasha 表格",
|
||
"Saturn 与 KP 支持页(PL9 p121-p125 对标)": "Saturn 与 KP",
|
||
"年度图盘与 Tajika(PL9 p126-p155 对标)": "年度 Varshaphala 与 Tajika",
|
||
"Bhavesh、Dosha 与 Yoga 解释页(PL9 p156-p189 对标)": "Bhavesh、Dosha 与 Yoga",
|
||
"大运长页解释(PL9 p190-p204 对标)": "大运解释",
|
||
"p3-p5 Birth Particulars / Hindu Calendar": "出生资料与印度历信息",
|
||
"p3 Birth Particulars 原版补充字段": "出生资料补充字段",
|
||
"p6 Birth Chart 行星原始位置": "Birth Chart 行星原始位置",
|
||
"p6 Birth Chart 星主与状态字段": "Birth Chart 星主与状态字段",
|
||
"p126-p127 Varshaphala 基础资料与年度行星表": "Varshaphala 基础资料与年度行星表",
|
||
"p139-p141 Varshaphala 年度结果解释": "Varshaphala 年度结果解释",
|
||
"p144-p145 Annual Dasha 结果解释": "Annual Dasha 结果解释",
|
||
"p132-p135 Mudda / Patyayini 年度 Dasha 表": "Mudda / Patyayini 年度 Dasha 表",
|
||
"missing_in_local": "未提供",
|
||
"external_mudda_start_boundary": "阶段起点",
|
||
"dasha_beginning_dates": "阶段起止日期",
|
||
"dasha_ending_dates": "阶段结束日期",
|
||
"moon_at_birth_time": "出生时月亮",
|
||
"moon_in_varshaphala": "年盘月亮",
|
||
"without_dasha_balance": "不计余额",
|
||
"balance 口径": "起始依据",
|
||
"边界口径": "日期说明",
|
||
"Progression (local ": "Progression (",
|
||
"p152 - birth_place_eight_year_overview": "",
|
||
"p153 - local_eight_year_overview": "",
|
||
"p154 - tithi_pravesh_birth_place": "",
|
||
"p155 - tithi_pravesh_current_location": "",
|
||
"外部边界:Chesta remains cross-engine method-conflicted; totals inherit that boundary。": "Chesta Bala 总分按本报告采用的计算口径解读。",
|
||
"外部边界:Chesta remains cross-engine method-conflicted; totals inherit that boundary.": "Chesta Bala 总分按本报告采用的计算口径解读。",
|
||
"资料路径": "资料来源",
|
||
"生成模块": "计算来源",
|
||
"结构": "项目",
|
||
"校验": "说明",
|
||
"functional_role_not_returned": "功能属性未单独返回",
|
||
"natural_benefic": "自然吉星",
|
||
"natural_malefic": "自然凶星",
|
||
"current_dasha": "当前 Dasha",
|
||
"birth_balance": "出生余额",
|
||
"Cycle / phase": "周期 / 阶段",
|
||
"Transit of 土星": "土星行运星座",
|
||
"Beginning date": "开始日期",
|
||
"Ending date": "结束日期",
|
||
"Duration Yr-Mn-Dy": "持续时间(年-月-日)",
|
||
"First Cycle of Sadhesati": "第一轮 Sade Sati",
|
||
"Second Cycle of Sadhesati": "第二轮 Sade Sati",
|
||
"Third Cycle of Sadhesati": "第三轮 Sade Sati",
|
||
"First Dhayya": "第一段 Dhayya",
|
||
"Second Dhayya": "第二段 Dhayya",
|
||
"Third Dhayya": "第三段 Dhayya",
|
||
"(Twelfth from birth rashi)": "(本命月亮星座前一宫)",
|
||
"(On birth rashi)": "(本命月亮星座本宫)",
|
||
"(Second from birth rashi)": "(本命月亮星座后一宫)",
|
||
";功能性角色为待补充": "",
|
||
"功能性角色为待补充。": "",
|
||
"Consideration: Mars is checked from Lagna, Moon, and Venus against the classical marriage-sensitive houses.": "校验说明:火星婚姻煞会同时从上升、月亮与金星出发,核对传统婚恋敏感宫位。",
|
||
"Consideration: this Dosha row is shown only when the local detector returns a chart-specific condition.": "校验说明:只有本命盘检测器返回具体条件时,才显示这一条 Dosha。",
|
||
"Consideration: Mars is checked from Lagna, Moon, and Venus against the classical marriage-sensitive houses.": "校验说明:火星婚姻煞会同时从上升、月亮与金星出发,核对传统婚恋敏感宫位。",
|
||
"In this chart Mars is listed in house": "本盘火星位于第",
|
||
"the detector severity is none.": "检测强度为无。",
|
||
"the detector severity is low.": "检测强度为低。",
|
||
"the detector severity is medium.": "检测强度为中。",
|
||
"the detector severity is high.": "检测强度为高。",
|
||
"Classical reference family: Muhurtha or Electional Astrology; 印度占星读书摘要.": "古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
"Classical reference family is attached in the source map.": "古典参考体系已随来源映射保留。",
|
||
"against the classical marriage-sensitive houses.": "并核对传统婚恋敏感宫位。",
|
||
"and Venus": "与金星",
|
||
"from Lagna, Moon,": "从上升与月亮",
|
||
"火星 is checked": "火星会被校验",
|
||
"In this chart 火星 is listed in 第-;": "本盘未列火星命中婚恋敏感宫位;",
|
||
"Birth Particulars / Hindu Calendar": "出生资料 / 印度历",
|
||
"Birth Chart 行星原始位置": "本命盘行星原始位置",
|
||
"Birth Chart Lords and Status Fields": "本命盘星主与状态字段",
|
||
"Chandra Chart / Bhava Spashta - Sripati System": "月亮盘 / Sripati 宫位精确表",
|
||
"Bhava Number": "宫位编号",
|
||
"Bhava Arambha / 宫位 beginning": "宫位起点",
|
||
"Bhava Madhya / Middle of 宫位": "宫位中点",
|
||
"Bhava Antya / 宫位 ending": "宫位终点",
|
||
"Planetary Raw Positions": "行星原始位置",
|
||
"| Field | Value |": "| 字段 | 数值 |",
|
||
"| Panchanga | Value |": "| Panchanga | 数值 |",
|
||
"| Planet | R/C | Sign | Degree | Longitude | House | Nakshatra | Pada | Speed |": "| 行星 | 顺逆 | 星座 | 落座度数 | 黄经 | 宫位 | 星宿 | Pada | 速度 |",
|
||
"| Planet | RL | NL | SL | SS | Status | SB |": "| 行星 | 星座主 | 星宿主 | Sub Lord | Sub-sub | 状态 | Shadbala |",
|
||
"| Point | Sign | Degree | Longitude | Lord |": "| 点位 | 星座 | 落座度数 | 黄经 | 守护星 |",
|
||
"| Body | Sign | Degree | Longitude |": "| 对象 | 星座 | 落座度数 | 黄经 |",
|
||
"| Planet | Sign | Degree | Longitude |": "| 行星 | 星座 | 落座度数 | 黄经 |",
|
||
}
|
||
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",
|
||
text,
|
||
flags=re.DOTALL,
|
||
)
|
||
text = re.sub(
|
||
r"(.+?)落在(.+?),首先把(.+?)放到(.+?)这一现实场域中阅读。",
|
||
r"\1落在\2,表示\3会主要通过\4来表现。",
|
||
text,
|
||
)
|
||
text = re.sub(r"第([1-4])足", r"第\1 Pada(星宿四分区)", text)
|
||
text = text.replace("Pada | 速度", "Pada(星宿四分区) | 速度")
|
||
text = text.replace("Natural friends", "天然友星")
|
||
text = text.replace("Natural enemies", "天然敌星")
|
||
text = text.replace("Temporary relation", "临时关系")
|
||
text = text.replace("Natal / KP Ruling Planets", "本命 / KP 主宰星")
|
||
text = text.replace("Ruling Planets", "主宰星")
|
||
text = text.replace("Sub-Planets", "副行星")
|
||
text = text.replace("Planetary", "行星")
|
||
text = text.replace("Planets", "行星")
|
||
text = text.replace("Planet", "行星")
|
||
text = replace_natural_language_term(text, "Sign", "星座")
|
||
text = text.replace("Degree in sign", "落座度数")
|
||
text = text.replace("Degree", "度数")
|
||
text = text.replace("Longitude", "黄经")
|
||
text = text.replace("Whole-sign house", "整宫宫位")
|
||
text = text.replace("House", "宫位")
|
||
text = text.replace("Object", "对象")
|
||
text = text.replace("Reference point", "参考点")
|
||
text = text.replace("Reference chart", "参考盘")
|
||
text = text.replace("Role", "角色")
|
||
text = text.replace("Yoga", "瑜伽")
|
||
text = text.replace("Category", "类别")
|
||
text = text.replace("Combination", "组合条件")
|
||
text = text.replace("Effects / notes", "作用说明")
|
||
text = text.replace("Planet 1", "行星1")
|
||
text = text.replace("Planet 2", "行星2")
|
||
text = text.replace("Exact degree", "精确角度")
|
||
text = text.replace("Actual difference", "实际角距")
|
||
text = text.replace("Orb", "容许度")
|
||
text = text.replace("Applying", "入相")
|
||
text = text.replace("From house", "起始宫位")
|
||
text = text.replace("Target house", "目标宫位")
|
||
text = text.replace("Aspect type", "相位类型")
|
||
text = text.replace("Special", "特殊相位")
|
||
text = text.replace("friends:", "友星:")
|
||
text = text.replace("enemies:", "敌星:")
|
||
text = text.replace("True", "是")
|
||
text = text.replace("False", "否")
|
||
text = text.replace("conjunction", "合相")
|
||
text = text.replace("opposition", "对冲")
|
||
text = re.sub(r"(?<![A-Za-z])special(?![A-Za-z])", "特殊", text)
|
||
text = text.replace("solar_yoga", "太阳瑜伽")
|
||
text = text.replace("lunar_yoga", "月亮瑜伽")
|
||
text = text.replace("kalatra", "婚姻类")
|
||
text = text.replace("durabhaga", "困难类")
|
||
text = text.replace("moderate", "中等")
|
||
text = text.replace("common", "普通")
|
||
text = text.replace("_aspect_", "宫相位_")
|
||
text = re.sub(
|
||
r"Supported houses gain expression through (.+?); strained houses require steadier handling, e特殊ly where the same houses repeat in 大运, divisional charts, Ashtakavarga, or the annual chart\.",
|
||
r"受支持的宫位会通过\1获得表达;若同一宫位在大运、分盘、Ashtakavarga 或年度盘中反复承压,则需要更稳妥地处理。",
|
||
text,
|
||
)
|
||
text = normalize_zh_markdown_terms(text)
|
||
dasha_heading_patterns = (
|
||
(
|
||
r"Effects of Antar 大运 of (.+?) in the Maha 大运 of (.+)",
|
||
r"\2主大运中\1子运的作用",
|
||
),
|
||
(
|
||
r"Antar 大运 of (.+?) in the Maha 大运 of (.+?):",
|
||
r"\2主大运中的\1子运:",
|
||
),
|
||
(
|
||
r"Pratyantar 大运 of (.+?) in the Antar 大运 of (.+)",
|
||
r"\2子运中的\1次子运",
|
||
),
|
||
(
|
||
r"Interpretation of the Maha 大运 of (.+)",
|
||
r"\1主大运解释",
|
||
),
|
||
)
|
||
for raw_pattern, public_pattern in dasha_heading_patterns:
|
||
text = re.sub(raw_pattern, public_pattern, text)
|
||
dasha_sentence_patterns = (
|
||
(
|
||
r"General effects during the Maha 大运 of (.+?) are read from the planet's natural significations, house placement, owned houses, dignity, 星宿, and functional role\.",
|
||
r"\1主大运的总体作用,需要从该行星的自然象征、落宫、守护宫、尊贵状态、星宿与功能角色一起阅读。",
|
||
),
|
||
(
|
||
r"General effects during the 主大运 of (.+?) are read from the planet's 自然象征, 落宫, owned houses, 尊贵状态, 星宿, and 功能角色\.",
|
||
r"\1主大运的总体作用,需要从该行星的自然象征、落宫、守护宫、尊贵状态、星宿与功能角色一起阅读。",
|
||
),
|
||
(
|
||
r"General effects during the Maha 大运 of (.+?) are read from the planet's 自然象征, 落宫, owned houses, 尊贵状态, 星宿, and 功能角色\.",
|
||
r"\1主大运的总体作用,需要从该行星的自然象征、落宫、守护宫、尊贵状态、星宿与功能角色一起阅读。",
|
||
),
|
||
(
|
||
r"General effects which are felt during the Maha 大运 of (.+?) are as follows:",
|
||
r"\1主大运期间通常会启动以下主题:",
|
||
),
|
||
(
|
||
r"General effects which are felt during the 主大运 of (.+?) are as follows:",
|
||
r"\1主大运期间通常会启动以下主题:",
|
||
),
|
||
(
|
||
r"Interpretations based on the condition of the planet in the birth chart and divisional charts are as follows:",
|
||
r"结合本命盘与分盘条件后,可按以下方式细读:",
|
||
),
|
||
(
|
||
r"In this chart (.+?) is read from house (.+?), ownership of (.+?), dignity (.+?), and 星宿 (.+?)\.",
|
||
r"在本盘中,\1需要从第\2宫、守护\3宫、尊贵状态\4与星宿\5一起阅读。",
|
||
),
|
||
(
|
||
r"In this chart (.+?) is read from house (.+?), 守护宫 of (.+?), 尊贵状态 (.+?), and 星宿 (.+?)\.",
|
||
r"在本盘中,\1需要从第\2宫、守护\3宫、尊贵状态\4与星宿\5一起阅读。",
|
||
),
|
||
(
|
||
r"Supported houses gain expression through (.+?); strained houses require steadier handling, especially where the same houses repeat in 大运, divisional charts, Ashtakavarga, or the annual chart\.",
|
||
r"受支持的宫位会通过\1获得表达;若同一宫位在大运、分盘、Ashtakavarga 或年度盘中反复承压,则需要更稳妥地处理。",
|
||
),
|
||
(
|
||
r"Owned houses (.+?) supply the concrete fields through which (.+?) gives results\.",
|
||
r"\2会通过其守护的第\1宫给出更具体的结果领域。",
|
||
),
|
||
(
|
||
r"This section follows the general/specific 大运 reading structure indexed from (.+?)\.",
|
||
r"本节按传统大运的总体作用与具体命盘条件两层结构展开,参考:\1。",
|
||
),
|
||
(
|
||
r"The Antar 大运 of (.+?) activates (.+?)'s natural themes inside the broader Maha 大运 of (.+?)\.",
|
||
r"\3主大运中的\1子运,会把\2的自然主题带入当前阶段。",
|
||
),
|
||
(
|
||
r"(.+?) brings house (.+?), ownership (.+?), dignity (.+?), and 星宿 (.+?) into focus\.",
|
||
r"\1会突出第\2宫、守护\3宫、尊贵状态\4与星宿\5。",
|
||
),
|
||
(
|
||
r"(.+?) brings house (.+?), 守护宫 (.+?), 尊贵状态 (.+?), and 星宿 (.+?) into focus\.",
|
||
r"\1会突出第\2宫、守护\3宫、尊贵状态\4与星宿\5。",
|
||
),
|
||
(
|
||
r"In the Pratyantar 大运 of (.+?) in the Antar 大运 of (.+?), (.+?)'s significations give the short-period result inside the (.+?) background\.",
|
||
r"\2子运中的\1次子运,会在\4背景下呈现\3的短周期结果。",
|
||
),
|
||
(
|
||
r"The immediate focus is house (.+?), ownership (.+?), dignity (.+?), and 星宿 (.+?)\.",
|
||
r"短期重点是第\1宫、守护\2宫、尊贵状态\3与星宿\4。",
|
||
),
|
||
(
|
||
r"The immediate focus is house (.+?), 守护宫 (.+?), 尊贵状态 (.+?), and 星宿 (.+?)\.",
|
||
r"短期重点是第\1宫、守护\2宫、尊贵状态\3与星宿\4。",
|
||
),
|
||
(
|
||
r"Supported houses gain expression through (.+?); strained houses require steadier handling, especially where the same houses repeat in 主大运, divisional charts, Ashtakavarga, or the annual chart\.",
|
||
r"受支持的宫位会通过\1获得表达;若同一宫位在主大运、分盘、Ashtakavarga 或年度盘中反复承压,则需要更稳妥地处理。",
|
||
),
|
||
)
|
||
for raw_pattern, public_pattern in dasha_sentence_patterns:
|
||
text = re.sub(raw_pattern, public_pattern, text)
|
||
text = text.replace("Maha 大运", "主大运")
|
||
text = text.replace("Antar 大运", "子运")
|
||
text = text.replace("Pratyantar 大运", "次子运")
|
||
text = text.replace("Current 大运", "当前大运")
|
||
text = text.replace("迁移/出行", "迁移与出行")
|
||
text = text.replace("strongweak", "强弱")
|
||
text = text.replace("Topic", "主题")
|
||
text = text.replace("Practice", "修持")
|
||
text = normalize_zh_markdown_terms(text)
|
||
text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE)
|
||
drop_fragments = (
|
||
"年度图盘渲染索引",
|
||
"年度图盘内容预览",
|
||
"年度北印度盘预览会嵌入 PDF",
|
||
"annual candidate structures visible",
|
||
"annual sensitive points visible",
|
||
"从年度执行层往下看",
|
||
"Topic | Detected / phase | Key facts | Summary",
|
||
"Detected / phase",
|
||
"Reference:",
|
||
"source_path",
|
||
"计算项",
|
||
"计算来源",
|
||
"资料来源",
|
||
"Report Layers",
|
||
"Worksheet Index",
|
||
"Report Pack Manifest",
|
||
"Reference:",
|
||
"资料来源",
|
||
"计算来源",
|
||
"| method |",
|
||
"swisseph_newton",
|
||
"jd_ut:",
|
||
"time_offset_seconds",
|
||
"口径_time_offset",
|
||
)
|
||
cleaned_lines = [
|
||
line.rstrip()
|
||
for line in text.splitlines()
|
||
if not any(fragment in line for fragment in drop_fragments)
|
||
]
|
||
text = "\n".join(cleaned_lines)
|
||
text = re.sub(r"\s*\(PL9 p\d+(?:-p\d+)?[^)]*\)", "", text)
|
||
text = re.sub(r"\s*PL9 p\d+(?:-p\d+)?", "", text)
|
||
text = re.sub(r"(?m)^(#{2,5}\s*)p\d+(?:-p\d+)?\s*[·.-]?\s*", r"\1", text)
|
||
text = re.sub(r"(?m)^(#{2,5}\s*)(.+?)\s*(PL9 p\d+(?:-p\d+)?[^)]*)", r"\1\2", text)
|
||
text = re.sub(r"Progression \(local (\d+)\)", r"Progression (\1)", text)
|
||
text = _strip_pl9_user_engineering_markers(text)
|
||
text = normalize_zh_markdown_terms(text)
|
||
text = text.replace("项目化资料附录", "结构化资料附录")
|
||
zh_heading_repairs = {
|
||
"Birth Data and Calculation 口径": "出生资料与计算口径",
|
||
"Core Chart Raw Positions": "本命盘原始位置",
|
||
"Divisional Charts Numeric Tables": "分盘数值位置表",
|
||
"Sensitive Points": "敏感点",
|
||
"Upagraha and Sub-行星 Points": "Upagraha 与副行星点",
|
||
"Upagraha and 副行星 Points": "Upagraha 与副行星点",
|
||
"行星 Friendship Matrix": "行星友敌关系矩阵",
|
||
"行星 Friendship 矩阵源数据": "行星友敌关系矩阵源数据",
|
||
"六维力量 Components": "六维力量细项",
|
||
"Functional 自然吉星 / 自然凶星 Layer": "功能性吉凶星层",
|
||
"Bhava 年龄状态 宫位 强度": "十二宫力量",
|
||
"行星ary Friendship Matrix": "行星友敌关系矩阵",
|
||
"行星ary Friendship 矩阵源数据": "行星友敌关系矩阵源数据",
|
||
"Sarvashtakavarga": "Sarvashtakavarga 总分",
|
||
"Bhinnashtakavarga": "Bhinnashtakavarga 单星八分表",
|
||
"KP 行星 星座ificators": "KP 行星征象星",
|
||
"KP 宫位 Cusps and 星座ificators": "KP 宫头与征象星",
|
||
# Since the term pass stopped rewriting inside words (BUG-1274),
|
||
# "Significators" arrives intact; these keys match that spelling.
|
||
"KP 宫位 Cusps and Significators": "KP 宫头与征象星",
|
||
"KP 行星 Significators": "KP 行星征象星",
|
||
"KP Significators / 行星 宫主": "KP 征象星 / 行星主星",
|
||
"KP Chart / Cusps": "KP 星盘 / 宫头",
|
||
"Annual Tajika瑜伽 Rows": "年度 Tajika 瑜伽行",
|
||
"KP 星座ificators / 行星 Lords": "KP 征象星 / 行星主星",
|
||
"Upagraha / Sub-行星s 点位": "Upagraha / 副行星点位",
|
||
"大运 Tables": "Dasha 表格",
|
||
"Annual Varshaphala Core Fields": "年度 Varshaphala 核心字段",
|
||
"Annual Mudda 大运 Rows": "年度 Mudda Dasha 行",
|
||
"Annual Patyayini / Patyamsha Rows": "年度 Patyayini / Patyamsha 行",
|
||
"Annual Saham Rows": "年度 Saham 行",
|
||
"Annual 行星 Extended Fields": "年度行星扩展字段",
|
||
"Annual Anchor Points": "年度锚点",
|
||
"Monthly Chart Pack": "月度图盘包",
|
||
"Lifetime Annual Chart Pages": "生命期年度图盘页",
|
||
"Birth-Place Eight-Year Annual Series": "出生地八年年度序列",
|
||
"Local Eight-Year Annual Series": "现居地八年年度序列",
|
||
"Applicable Yogas": "可用瑜伽",
|
||
"Dosha and Transit-Condition Summary": "Dosha 与行运状态摘要",
|
||
"Dosha and Transit Summary": "Dosha 与行运摘要",
|
||
"| Topic | Detected / phase | Summary |": "| 项目 | 是否成立 / 阶段 | 摘要 |",
|
||
"Dharidhra:": "贫困组合:",
|
||
"not listed": "未列",
|
||
"Weak": "弱",
|
||
"Moderate": "中等",
|
||
"Strong": "强",
|
||
"weak": "弱",
|
||
"medium": "中等",
|
||
"strong": "强",
|
||
"house ": "第",
|
||
}
|
||
text = replace_natural_language_term(text, "house condition", "宫位条件")
|
||
for raw, public in zh_heading_repairs.items():
|
||
text = text.replace(raw, public)
|
||
# Heading lines only: a Latin yoga name keeps a space before 瑜伽 ("Dhana 瑜伽").
|
||
text = re.sub(r"(?m)^(#{1,6} .*?[A-Za-z])瑜伽", r"\1 瑜伽", text)
|
||
text = replace_natural_language_term(text, " condition", "宫条件")
|
||
zh_public_key_repairs = {
|
||
"mangal_dosha": "火星婚姻煞",
|
||
"kaal_sarp_dosha": "时蛇煞",
|
||
"pitra_dosha": "祖业/父系业力煞",
|
||
"sade_sati": "Sade Sati(土星七年半)",
|
||
"Mangala / Kuja Dosha": "火星婚姻煞(Mangala / Kuja Dosha)",
|
||
"Kaal Sarp Dosha": "时蛇煞(Kaal Sarp Dosha)",
|
||
"Pitra Dosha": "祖业/父系业力煞(Pitra Dosha)",
|
||
}
|
||
for raw, public in zh_public_key_repairs.items():
|
||
text = text.replace(raw, public)
|
||
# A source cell that already carries the gloss would be wrapped a second time.
|
||
text = re.sub(
|
||
r"(火星婚姻煞|时蛇煞|祖业/父系业力煞)(\1(([^)]+)))",
|
||
r"\1(\2)",
|
||
text,
|
||
)
|
||
text = text.replace("Sarvashtakavarga 总分 总分", "Sarvashtakavarga 总分")
|
||
text = text.replace("Bhinnashtakavarga 单星八分表 单星八分表", "Bhinnashtakavarga 单星八分表")
|
||
text = re.sub(r"Mula(?:(根[,、/]本源))+", "Mula(根、本源)", text)
|
||
text = re.sub(r"根宿(Mula(根[,、/]本源))", "Mula(根、本源)", text)
|
||
text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE)
|
||
text = _clean_pl9_user_table_placeholders(text, language="zh")
|
||
text = _repair_user_facing_orphan_table_headers(text, language="zh")
|
||
text = re.sub(r"\n{3,}", "\n\n", text).strip()
|
||
visible_lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||
first_body_line = ""
|
||
for line in visible_lines[:8]:
|
||
if line.startswith("#"):
|
||
continue
|
||
first_body_line = line
|
||
break
|
||
has_opening_summary = bool(first_body_line) and any(
|
||
marker in first_body_line for marker in ("摘要:", "结论:", "重点:")
|
||
)
|
||
if text.startswith(f"# {title}") and not has_opening_summary:
|
||
summary = "\n".join(
|
||
[
|
||
f"# {title}",
|
||
"",
|
||
"摘要:本册先保留完整排盘资料,再按强度、Dasha、KP、年度与解释页进入证据附录。",
|
||
"",
|
||
"## 重点",
|
||
"1. 先核对出生资料、图盘、度数和计算口径。",
|
||
"2. 再看 Shadbala、Ashtakavarga、Dasha、KP 与年度 Tajika 表格。",
|
||
"3. 最后用证据附录核验来源、边界与仍需复核的项目。",
|
||
]
|
||
)
|
||
text = re.sub(r"^#\s+.*$", summary, text, count=1, flags=re.MULTILINE)
|
||
return strip_han_plural_s(text) + "\n"
|
||
|
||
|
||
|
||
def _clean_pl9_user_table_placeholders(markdown: str, *, language: str) -> str:
|
||
"""Remove empty placeholder cells and compressed table fragments from user exports."""
|
||
|
||
zh_planet_names = "太阳|月亮|火星|水星|木星|金星|土星|北交点/罗睺|南交点/计都"
|
||
en_planet_names = "Sun|Moon|Mars|Mercury|Jupiter|Venus|Saturn|Rahu|Ketu"
|
||
|
||
def normalize_condition_cell(cell: str) -> str:
|
||
value = cell.strip()
|
||
if value in {"", "-", "未列", "not listed"}:
|
||
return "未列" if language == "zh" else "not listed"
|
||
if language == "zh":
|
||
value = re.sub(rf"\b(\d+)({zh_planet_names})(\d+)\b", r"第\1宫;\2在第\3宫", value)
|
||
value = re.sub(rf"\b({zh_planet_names})(\d+)\b", r"\1在第\2宫", value)
|
||
value = re.sub(r"(?<!\d)(\d+)KendraTrikona\b", r"第\1宫;角宫/三分宫条件", value)
|
||
value = re.sub(r"(?<!\d)(\d+)Kendra\b", r"第\1宫;角宫条件", value)
|
||
value = re.sub(r"(?<!\d)(\d+)弱/?", r"第\1宫较弱", value)
|
||
value = re.sub(r"(?<!\d)(\d+)/(?=\s|$|[|,,])", r"第\1宫条件", value)
|
||
value = re.sub(r"第(\d+)宫条件第(\d+)宫条件", r"第\1宫、第\2宫条件", value)
|
||
value = re.sub(r"第(\d+)宫条件第(\d+)宫条件", r"第\1宫、第\2宫条件", value)
|
||
if re.fullmatch(r"\d+", value):
|
||
if len(value) == 2:
|
||
return f"第{value[0]}/{value[1]}宫条件"
|
||
return f"第{value}宫条件"
|
||
value = re.sub(r"\b74,\s*D1/D9", "第7/4宫条件;D1/D9", value)
|
||
return value
|
||
value = re.sub(rf"\b(\d+)({en_planet_names})(\d+)\b", r"house \1; \2 in house \3", value)
|
||
value = re.sub(rf"\b({en_planet_names})(\d+)\b", r"\1 in house \2", value)
|
||
value = re.sub(r"(?<!\d)(\d+)KendraTrikona\b", r"house \1; kendra/trikona condition", value)
|
||
value = re.sub(r"(?<!\d)(\d+)Kendra\b", r"house \1; kendra condition", value)
|
||
value = re.sub(r"(?<!\d)(\d+)weak/?", r"house \1 weak", value)
|
||
value = re.sub(r"(?<!\d)(\d+)/(?=\s|$|[|,])", r"house \1 condition", value)
|
||
if re.fullmatch(r"\d+", value):
|
||
if len(value) == 2:
|
||
return f"houses {value[0]}/{value[1]} condition"
|
||
return f"house {value} condition"
|
||
value = re.sub(r"\b74,\s*D1/D9", "houses 7/4; D1/D9", value)
|
||
return value
|
||
|
||
def compress_markdown_tables(raw_text: str) -> str:
|
||
lines = raw_text.splitlines()
|
||
out: list[str] = []
|
||
i = 0
|
||
empty_values = {"", "-", "未列", "not listed"}
|
||
while i < len(lines):
|
||
if (
|
||
i + 1 < len(lines)
|
||
and lines[i].lstrip().startswith("|")
|
||
and lines[i + 1].lstrip().startswith("|")
|
||
and set(lines[i + 1].replace("|", "").replace("-", "").replace(":", "").strip()) == set()
|
||
):
|
||
block: list[str] = []
|
||
while i < len(lines) and lines[i].lstrip().startswith("|"):
|
||
block.append(lines[i])
|
||
i += 1
|
||
rows = [[cell.strip() for cell in line.strip().strip("|").split("|")] for line in block]
|
||
if len(rows) >= 2:
|
||
headers = rows[0]
|
||
body = rows[2:]
|
||
for row in rows[2:]:
|
||
for index, header in enumerate(headers):
|
||
if index >= len(row):
|
||
continue
|
||
if header in {"组合条件", "Combination"}:
|
||
row[index] = normalize_condition_cell(row[index])
|
||
elif header in {"作用说明", "Effects / notes", "Notes", "Summary"} and row[index].strip() in empty_values:
|
||
row[index] = "未列" if language == "zh" else "not listed"
|
||
remove: set[int] = set()
|
||
for index, header in enumerate(headers):
|
||
column = [row[index] if index < len(row) else "" for row in body]
|
||
empty_count = sum(1 for cell in column if cell.strip() in empty_values)
|
||
if header in {"作用说明", "Effects / notes", "Notes", "Summary"} and body and empty_count / len(body) >= 0.65:
|
||
remove.add(index)
|
||
if header in {"组合条件", "Combination"} and body and empty_count / len(body) >= 0.9:
|
||
remove.add(index)
|
||
if remove and len(remove) < len(headers):
|
||
rows = [[cell for index, cell in enumerate(row) if index not in remove] for row in rows]
|
||
rows[1] = ["---"] * len(rows[0])
|
||
out.extend("| " + " | ".join(row) + " |" for row in rows)
|
||
else:
|
||
out.extend(block)
|
||
continue
|
||
out.append(lines[i])
|
||
i += 1
|
||
return "\n".join(out)
|
||
|
||
text = markdown
|
||
empty_label = "未列" if language == "zh" else "not listed"
|
||
text = re.sub(r"(?:^|;\s*)-(?:\s*;\s*-){1,}\s*$", empty_label, text, flags=re.MULTILINE)
|
||
text = re.sub(r"\|\s*(?:-\s*\|\s*){3,}(?:missing_in_local|未单独列出)\s*\|", "", text)
|
||
text = re.sub(r"(?m)^\|\s*-\s*\|.*\|\s*(?:missing_in_local|未单独列出|not listed)\s*\|\s*$", "", text)
|
||
text = text.replace("missing_in_local", empty_label)
|
||
text = re.sub(r"\blisted planets(\d+)\b", r"\1 listed planet(s)", text)
|
||
text = re.sub(r"\b(木星|太阳|月亮|火星|水星|金星|土星|北交点/罗睺|南交点/计都)(\d+)\b", r"\1在第\2宫", text)
|
||
text = re.sub(r"\b(Jupiter|Sun|Moon|Mars|Mercury|Venus|Saturn|Rahu|Ketu)(\d+)\b", r"\1 in house \2", text)
|
||
text = re.sub(r"\b(MercuryMoon|SunMercury|MoonVenus)(\d+)\b", r"\1 in house \2", text)
|
||
text = re.sub(r"\b(\d+)(Mars|Venus|Jupiter|Sun|Moon|Mercury|Saturn|Rahu|Ketu)(\d+)\b", r"house \1; \2 in house \3", text)
|
||
text = re.sub(r"\b(\d+)弱/", r"第\1宫较弱", text)
|
||
text = re.sub(r"\b(\d+)weak/", r"house \1 weak", text)
|
||
text = re.sub(r"\b(\d+)/\b", r"第\1宫条件", text)
|
||
text = re.sub(r"//,\s*", "", text)
|
||
text = text.replace("D1/D9debilitated", "D1/D9 落陷" if language == "zh" else "D1/D9 debilitated")
|
||
text = text.replace("Dharidhra:", "贫困组合:" if language == "zh" else "Daridra combination:")
|
||
text = text.replace("Dharidhra Yoga", "Daridra Yoga")
|
||
text = text.replace("-; -; 弱", "弱")
|
||
text = text.replace("-; -; weak", "weak")
|
||
text = re.sub(r"-;\s*(强|弱|中等|strong|weak|medium|moderate);\s*-", r"\1", text)
|
||
text = re.sub(r"-;\s*/", empty_label, text)
|
||
text = re.sub(r"\b(MercuryMoon|SunVenus|VenusSun|MoonVenus|VenusMoon)(?:\s+in\s+)?第?(\d+)\b", r"\1 in house \2", text)
|
||
text = re.sub(r"\b(SunVenus|VenusSun|MoonVenus|VenusMoon|MercuryMoon)(\d+)\b", r"\1 in house \2", text)
|
||
text = re.sub(r"\b(MercuryVenus|VenusMercury)(\d+)\b", r"\1 in house \2", text)
|
||
text = re.sub(r"\b(\d+)KendraTrikona\b", r"house \1; kendra/trikona condition", text)
|
||
text = re.sub(r"\b(\d+)Kendra\b", r"house \1; kendra condition", text)
|
||
text = re.sub(r"\b(\d+)/(?=\s*\|)", r"house \1 condition", text)
|
||
text = text.replace("weak/", "weak")
|
||
text = text.replace(", Kendra", "kendra condition")
|
||
text = re.sub(r"\b(Mars|Mercury|Venus|Jupiter|Saturn|Sun|Moon|Rahu|Ketu)(\d+)(Aries|Taurus|Gemini|Cancer|Leo|Virgo|Libra|Scorpio|Sagittarius|Capricorn|Aquarius|Pisces)\b", r"\1 in house \2, \3", text)
|
||
text = re.sub(r"\b(Saturn|Mars|Mercury|Venus|Jupiter|Sun|Moon|Rahu|Ketu)(medium|weak|strong|moderate)\b", r"\1 \2", text)
|
||
if language == "zh":
|
||
text = re.sub(
|
||
r"Consideration:\s*(.+?)会被校验 from 上升,\s*月亮,\s*and 金星 并核对传统婚恋敏感宫位。\s*In this chart\s+(.+?)\s+is listed in 第(.+?)\s+in\s+(.+?);\s*检测强度为(.+?)。\s*古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
r"校验说明:\1会同时从上升、月亮与金星出发,核对传统婚恋敏感宫位;本盘中,\2位于第\3宫、\4,检测强度为\5。古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
text,
|
||
)
|
||
text = re.sub(
|
||
r"校验说明:火星婚姻煞会同时从上升、月亮与金星出发,核对传统婚恋敏感宫位。\s*本盘火星位于第\s*(.+?)\s+in\s+(.+?);\s*检测强度为(.+?)。\s*古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
r"校验说明:火星婚姻煞会同时从上升、月亮与金星出发,核对传统婚恋敏感宫位;本盘中,火星位于第\1宫、\2,检测强度为\3。古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
text,
|
||
)
|
||
text = re.sub(
|
||
r"Consideration:\s*火星会被校验 from 上升,\s*月亮,\s*and 金星 并核对传统婚恋敏感宫位。\s*In this chart 火星 is listed in 第(.+?)\s+in\s+(.+?);\s*the detector severity is\s+(.+?)\.\s*古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
r"校验说明:火星婚姻煞会同时从上升、月亮与金星出发,核对传统婚恋敏感宫位;本盘中,火星位于第\1宫、\2,检测强度为\3。古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
text,
|
||
)
|
||
text = re.sub(r"\n项目字段:.*?(?=\n)", "", text)
|
||
text = re.sub(
|
||
r"Consideration:\s*(.+?)\s+is checked from\s+(.+?)\s+against the classical marriage-sensitive houses\.\s*In this chart\s+(.+?)\s+is listed in house\s+(.+?);?\s+the detector severity is\s+(.+?)\.\s*Classical reference family:\s*(.+?)\.",
|
||
r"校验说明:\1会从\2出发,核对传统婚恋敏感宫位;本盘中,\3列于第\4宫,检测强度为\5。古典参考体系:\6。",
|
||
text,
|
||
)
|
||
text = re.sub(
|
||
r"Consideration:\s*(.+?)\s+is checked from\s+(.+?)\s+against the classical marriage-sensitive houses\.\s*In this chart\s+(.+?)\s+is listed in\s+(.+?);?\s+the detector severity is\s+(.+?)\.\s*Classical reference family:\s*(.+?)\.",
|
||
r"校验说明:\1会从\2出发,核对传统婚恋敏感宫位;本盘中,\3列于\4,检测强度为\5。古典参考体系:\6。",
|
||
text,
|
||
)
|
||
text = re.sub(
|
||
r"Consideration:\s*火星会被校验 from 上升,\s*月亮,\s*and 金星 并核对传统婚恋敏感宫位。\s*In this chart 火星 is listed in 第-;\s*检测强度为无。\s*古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
"校验说明:火星婚姻煞会同时从上升、月亮与金星出发,核对传统婚恋敏感宫位;本盘未列火星命中婚恋敏感宫位,检测强度为无。古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
text,
|
||
)
|
||
text = re.sub(
|
||
r"Consideration:\s*火星 is checked from 上升,\s*月亮,\s*and 金星 against the classical marriage-sensitive houses\.\s*In this chart 火星 is listed in 第-;\s*the detector severity is none\.\s*Classical reference family:\s*Muhurtha or Electional Astrology;\s*印度占星读书摘要\.",
|
||
"校验说明:火星婚姻煞会同时从上升、月亮与金星出发,核对传统婚恋敏感宫位;本盘未列火星命中婚恋敏感宫位,检测强度为无。古典参考体系:Muhurtha / Electional Astrology 与印度占星读书摘要。",
|
||
text,
|
||
)
|
||
text = re.sub(
|
||
r"Consideration:\s*this Dosha row is shown only when the local detector returns a chart-specific condition\.\s*Classical reference family is attached in the source map\.",
|
||
"校验说明:只有本命盘检测器返回具体条件时,才显示这一条 Dosha;古典参考体系已随来源映射保留。",
|
||
text,
|
||
)
|
||
planet_pair_terms = {
|
||
"MercuryMoon": "水星与月亮",
|
||
"SunVenus": "太阳与金星",
|
||
"VenusSun": "金星与太阳",
|
||
"MoonVenus": "月亮与金星",
|
||
"VenusMoon": "金星与月亮",
|
||
"MercuryVenus": "水星与金星",
|
||
"VenusMercury": "金星与水星",
|
||
}
|
||
for raw, public in planet_pair_terms.items():
|
||
text = text.replace(raw, public)
|
||
english_planet_to_zh = {
|
||
"Sun": "太阳",
|
||
"Moon": "月亮",
|
||
"Mars": "火星",
|
||
"Mercury": "水星",
|
||
"Jupiter": "木星",
|
||
"Venus": "金星",
|
||
"Saturn": "土星",
|
||
"Rahu": "北交点/罗睺",
|
||
"Ketu": "南交点/计都",
|
||
}
|
||
def triple_planets_to_zh(match: re.Match[str]) -> str:
|
||
names = [english_planet_to_zh.get(match.group(i), match.group(i)) for i in (1, 2, 3)]
|
||
return f"{'、'.join(names)}在第{match.group(4)}宫"
|
||
text = re.sub(
|
||
r"\b(Sun|Moon|Mars|Mercury|Jupiter|Venus|Saturn|Rahu|Ketu)(Sun|Moon|Mars|Mercury|Jupiter|Venus|Saturn|Rahu|Ketu)(Sun|Moon|Mars|Mercury|Jupiter|Venus|Saturn|Rahu|Ketu)(\d+)\b",
|
||
triple_planets_to_zh,
|
||
text,
|
||
)
|
||
text = re.sub(r"in house (\d+)", r"在第\1宫", text)
|
||
else:
|
||
text = re.sub(r"第(\d+)宫条件第(\d+)宫条件(\d+)", r"houses \1/\2/\3 condition", text)
|
||
text = re.sub(r"第(\d+)宫条件(\d+)", r"houses \1/\2 condition", text)
|
||
text = re.sub(r"第(\d+)宫条件", r"house \1 condition", text)
|
||
text = re.sub(r"\b(Moon|Sun|Mars|Mercury|Jupiter|Venus|Saturn|Rahu|Ketu)(Rahu|Ketu|Moon|Sun|Mars|Mercury|Jupiter|Venus|Saturn)(Rahu|Ketu|Moon|Sun|Mars|Mercury|Jupiter|Venus|Saturn)(\d+)\b", r"\1, \2, \3 in house \4", text)
|
||
text = re.sub(r"\b(\d+)(strong|medium|weak|moderate)/", r"house \1 \2", text)
|
||
text = re.sub(r"\b(\d+)Navamsa(\d+)/house (\d+) condition(\d+)", r"Navamsa \1/\2; houses \3/\4 condition", text)
|
||
text = re.sub(r"\b(\d+)Navamsa(\d+)/houses? ([0-9/]+) condition", r"Navamsa \1/\2; houses \3 condition", text)
|
||
text = re.sub(r"\b(\d+)Navamsa(\d+)/(\d+)", r"Navamsa \1/\2/\3 condition", text)
|
||
text = text.replace("MercuryMoon", "Mercury-Moon")
|
||
text = text.replace("SunMercury", "Sun-Mercury")
|
||
text = text.replace("MoonVenus", "Moon-Venus")
|
||
text = text.replace("SunVenus", "Sun-Venus")
|
||
text = text.replace("VenusSun", "Venus-Sun")
|
||
text = text.replace("VenusMoon", "Venus-Moon")
|
||
text = text.replace("MercuryVenus", "Mercury-Venus")
|
||
text = text.replace("VenusMercury", "Venus-Mercury")
|
||
text = re.sub(r"\|\s*-\s*\|", f"| {empty_label} |", text)
|
||
text = compress_markdown_tables(text)
|
||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||
return text
|
||
|
||
|
||
|
||
def _repair_user_facing_orphan_table_headers(markdown: str, *, language: str) -> str:
|
||
"""Restore user-facing table headers removed by language cleanup.
|
||
|
||
The source report may contain Chinese section/table headers before English
|
||
sanitization. Dropping those lines is correct, but the markdown separator
|
||
row must not be left as the first row of a table.
|
||
"""
|
||
|
||
def is_table_line(line: str) -> bool:
|
||
return line.lstrip().startswith("|") and line.rstrip().endswith("|")
|
||
|
||
def is_separator(line: str) -> bool:
|
||
stripped = line.strip()
|
||
if not is_table_line(stripped):
|
||
return False
|
||
body = stripped.replace("|", "").replace("-", "").replace(":", "").strip()
|
||
return body == ""
|
||
|
||
def cells(line: str) -> list[str]:
|
||
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
||
|
||
def has_cjk(value: str) -> bool:
|
||
return bool(re.search(r"[\u4e00-\u9fff]", value))
|
||
|
||
def header_for(next_cells: list[str]) -> list[str]:
|
||
count = len(next_cells)
|
||
sample = " | ".join(next_cells)
|
||
if language == "zh":
|
||
if count == 4 and re.search(r"火星婚姻煞|时蛇煞|Sade Sati|Dosha", sample):
|
||
return ["项目", "是否成立", "条件", "说明"]
|
||
return [f"列{index}" for index in range(1, count + 1)]
|
||
if count == 2:
|
||
return ["Field", "Value"]
|
||
if count == 3 and re.search(r"Saham|Arudha|Lagna", sample, re.I):
|
||
return ["Point", "Sign", "Degree"]
|
||
if count == 4 and re.search(r"\b(Sun|Moon|Mars|Mercury|Jupiter|Venus|Saturn|Rahu|Ketu)\b", sample):
|
||
if re.search(r"\d+\.\d+°", sample):
|
||
return ["Planet", "Sign", "Longitude", "House"]
|
||
return ["Lord", "Sign/Rashi", "Start", "End"]
|
||
if count == 4 and re.search(r"\d{4}-\d{2}-\d{2}", sample):
|
||
return ["Year", "Age", "Solar return time", "Annual ascendant"]
|
||
if count == 4:
|
||
return ["Name", "Sign", "Degree", "House"]
|
||
if count == 5 and re.search(r"\d{4}-\d{2}-\d{2}", sample):
|
||
if re.search(r"\d{4}-\d{2}-\d{2}.*-", sample):
|
||
return ["Order", "Lord", "Duration", "Period", "Notes"]
|
||
return ["Order", "Lord", "Start", "End", "Years"]
|
||
if count == 5 and re.search(r"^\d+$", next_cells[0]):
|
||
return ["House", "Sign", "SAV", "Strength", "Notes"]
|
||
if count == 5:
|
||
return ["House", "Sign", "Lord", "Score", "Key factors"]
|
||
if count == 6 and re.search(r"\d{4}-\d{2}-\d{2}", sample):
|
||
return ["Order", "Major lord", "Sub lord", "Sub-sub lord", "Start", "End"]
|
||
if count == 6 and re.search(r"Paravatamsa|Simhasanamsa|Vimsopaka", sample):
|
||
return ["Planet", "Score", "Grade", "Combustion", "Retrograde", "Debilitated"]
|
||
if count == 6:
|
||
return ["Order", "Year", "Solar return time", "Annual ascendant", "Ascendant degree", "Planetary longitudes"]
|
||
if count == 8:
|
||
return ["Planet 1", "Planet 2", "Aspect", "Exact degree", "Actual difference", "Orb", "Applying", "Strength"]
|
||
return [f"Column {index}" for index in range(1, count + 1)]
|
||
|
||
lines = markdown.splitlines()
|
||
out: list[str] = []
|
||
i = 0
|
||
while i < len(lines):
|
||
line = lines[i]
|
||
if not is_separator(line):
|
||
out.append(line)
|
||
i += 1
|
||
continue
|
||
previous_is_table = bool(out and is_table_line(out[-1]) and not is_separator(out[-1]))
|
||
if previous_is_table:
|
||
out.append(line)
|
||
i += 1
|
||
continue
|
||
j = i + 1
|
||
while j < len(lines) and not lines[j].strip():
|
||
j += 1
|
||
if j >= len(lines) or not is_table_line(lines[j]) or is_separator(lines[j]):
|
||
i += 1
|
||
continue
|
||
next_cells = cells(lines[j])
|
||
header = header_for(next_cells)
|
||
if language == "en":
|
||
header = [cell if not has_cjk(cell) else f"Column {idx}" for idx, cell in enumerate(header, 1)]
|
||
out.append("| " + " | ".join(header) + " |")
|
||
out.append("| " + " | ".join(["---"] * len(header)) + " |")
|
||
i += 1
|
||
repaired = "\n".join(out)
|
||
lines = repaired.splitlines()
|
||
compacted: list[str] = []
|
||
i = 0
|
||
while i < len(lines):
|
||
if (
|
||
i + 1 < len(lines)
|
||
and is_table_line(lines[i])
|
||
and is_separator(lines[i + 1])
|
||
):
|
||
j = i + 2
|
||
while j < len(lines) and not lines[j].strip():
|
||
j += 1
|
||
next_starts_new_empty_table = (
|
||
j + 1 < len(lines)
|
||
and is_table_line(lines[j])
|
||
and not is_separator(lines[j])
|
||
and is_separator(lines[j + 1])
|
||
)
|
||
if j >= len(lines) or not is_table_line(lines[j]) or is_separator(lines[j]) or next_starts_new_empty_table:
|
||
i = i + 2
|
||
continue
|
||
compacted.append(lines[i])
|
||
i += 1
|
||
lines = compacted
|
||
compacted = []
|
||
i = 0
|
||
while i < len(lines):
|
||
compacted.append(lines[i])
|
||
if is_separator(lines[i]):
|
||
j = i + 1
|
||
while j < len(lines) and not lines[j].strip():
|
||
j += 1
|
||
if j > i + 1 and j < len(lines) and is_table_line(lines[j]) and not is_separator(lines[j]):
|
||
i = j
|
||
continue
|
||
i += 1
|
||
return "\n".join(compacted)
|
||
|
||
|
||
|
||
def _sanitize_pl9_ai_density_markdown_en(markdown: str, title: str = "Vedic Astrology Chart Report") -> str:
|
||
"""English user volume: remove engineering labels without translating facts."""
|
||
|
||
try:
|
||
from pl9_language_terms import normalize_en_markdown_terms
|
||
except ModuleNotFoundError: # pragma: no cover - package import compatibility
|
||
from scripts.pl9_language_terms import normalize_en_markdown_terms
|
||
|
||
text = markdown
|
||
replacements = {
|
||
"# PL9 对标主册": f"# {title}",
|
||
"PL9 对标主册": title,
|
||
"PL9 风格专业排盘导出": title,
|
||
"parameter_sensitive / unverified": "reference",
|
||
"parameter_sensitive": "reference",
|
||
"partial_verified": "listed",
|
||
"pyjhora_behavior_only / not_multiengine_parity": "reference",
|
||
"restricted_or_unclosed": "reference",
|
||
"not_applicable": "not listed",
|
||
"blocked_and_conflict_fields_cannot_generate_final_predictions": "listed only",
|
||
"blocked": "not listed",
|
||
"profile_sensitive": "reference",
|
||
"source_path": "source",
|
||
"producer": "calculation item",
|
||
"schema": "item",
|
||
"audit": "review",
|
||
"status": "status",
|
||
"dasha_beginning_dates": "beginning dates",
|
||
"dasha_ending_dates": "ending dates",
|
||
"birth_balance": "birth balance",
|
||
"moon_in_varshaphala": "Moon in Varshaphala",
|
||
"without_dasha_balance": "without Dasha balance",
|
||
"external_mudda_start_boundary": "start boundary",
|
||
"p132-p135 Mudda / Patyayini 年度 Dasha 表": "Mudda / Patyayini Annual Dasha Table",
|
||
"p136-p138 Tajika Yoga Presence 对照表": "Tajika Yoga Presence Table",
|
||
"行星、星宿与宫位长页解释(PL9 p18-p29 对标)": "Planets, Nakshatras and Houses",
|
||
"大运长页解释(PL9 p190-p204 对标)": "Dasha Interpretations",
|
||
"强度、关系与 Ashtakavarga 技术页(PL9 p30-p60 对标)": "Strength, Relationships and Ashtakavarga",
|
||
"标准 Dasha 表格页(PL9 p61-p120 对标)": "Standard Dasha Tables",
|
||
"Saturn 与 KP 支持页(PL9 p121-p125 对标)": "Saturn and KP Tables",
|
||
"年度图盘与 Tajika(PL9 p126-p155 对标)": "Annual Varshaphala and Tajika",
|
||
"Bhavesh、Dosha 与 Yoga 解释页(PL9 p156-p189 对标)": "Bhavesh, Dosha and Yoga Interpretations",
|
||
"Result: when present, Kuja/Mangala Dosha is read as heat, impatience, conflict, or pressure around partnership handling; when absent or cancelled, the report does not promote it as a dominant relationship obstacle. The result must be read with the seventh house, Venus, Upapada, Navamsha, current Dasha, and partner-chart comparison where available.": "When present, Kuja/Mangala Dosha is read as heat, impatience, conflict, or pressure around partnership handling. When absent or cancelled, it is not treated as a dominant relationship obstacle. This factor is read together with the seventh house, Venus, Upapada, Navamsha, current Dasha, and partner-chart comparison where available.",
|
||
"Consideration: this Dosha row is shown only when the local detector returns a chart-specific condition. Classical reference family is attached in the source map.": "This Dosha row is shown only when the chart-specific condition is present. The classical reference family is retained in the source layer.",
|
||
"Result: this row contributes a supporting condition and is not promoted over the core chart, Dasha, and divisional evidence.": "This row contributes a supporting condition and is not promoted over the core chart, Dasha, and divisional evidence.",
|
||
}
|
||
for raw, public in replacements.items():
|
||
text = text.replace(raw, public)
|
||
reverse_terms = {
|
||
"出生资料与图盘": "Birth Data and Charts",
|
||
"姓名": "Name",
|
||
"出生": "Birth",
|
||
"计算口径": "Calculation profile",
|
||
"出生日期": "Birth date",
|
||
"出生时间": "Birth time",
|
||
"纬度": "Latitude",
|
||
"经度": "Longitude",
|
||
"时区": "Time zone",
|
||
"岁差名称": "Ayanamsa name",
|
||
"岁差值": "Ayanamsa value",
|
||
"岁差": "Ayanamsa",
|
||
"交点模式": "Node mode",
|
||
"吉性_count": "auspicious_count",
|
||
"中(Mixed)": "Mixed",
|
||
"行星原始位置": "Planetary Raw Positions",
|
||
"星主与状态字段": "Lords and Status Fields",
|
||
"星宿": "Nakshatra",
|
||
"状态": "Status",
|
||
"对象": "Object",
|
||
"落座度数": "Degree in sign",
|
||
"落座": "Sign",
|
||
"黄经": "Longitude",
|
||
"整宫宫位": "Whole-sign house",
|
||
"资料": "Data",
|
||
"点位": "Points",
|
||
"敏感点与特殊落点": "Sensitive and Special Points",
|
||
"本命与分盘北印度图盘": "Natal and Divisional North Indian Charts",
|
||
"Ashtakavarga 完整宫位分数": "Ashtakavarga Full House Scores",
|
||
"分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。": "Divisional charts are sensitive to birth time. The raw calculations are here for checking and do not by themselves make a conclusion more certain.",
|
||
"行星主星与状态": "Planet Lords and Dignity",
|
||
"| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |": "| Planet | Sign lord | Star lord | Sub lord | Sub-sub lord | Dignity | Strength ratio |",
|
||
"Avastha 行星状态": "Planetary Avasthas",
|
||
"Sudarshan Chakra 资料": "Sudarshan Chakra Data",
|
||
"Sudarshana Chakra(三参考点原始结构)": "Sudarshana Chakra Tri-Reference Structure",
|
||
"数值位置表": "Numeric Position Table",
|
||
"上升": "Lagna",
|
||
"合计": "Total",
|
||
"月亮 Chart": "Moon Chart",
|
||
"九分盘": "Navamsha",
|
||
"可用瑜伽总表": "Applicable Yoga Summary",
|
||
"可用吉性瑜伽": "Applicable Benefic Yogas",
|
||
"可用混合型瑜伽": "Applicable Mixed Yogas",
|
||
"可用凶性瑜伽": "Applicable Malefic Yogas",
|
||
"检出的瑜伽数量": "Yoga detected count",
|
||
"参考层": "Context layers",
|
||
"| 瑜伽 | 类别 | 强度 | 组合条件 | 作用说明 |": "| Yoga | Category | Strength | Combination | Effects / notes |",
|
||
"|------|------|------|----------|----------|": "|------|----------|----------|-------------|-----------------|",
|
||
"合相": "conjunction",
|
||
"特殊组合": "special",
|
||
"婚恋": "kalatra",
|
||
"不利组合": "durbhaga",
|
||
"太阳瑜伽": "solar yoga",
|
||
"月亮瑜伽": "lunar yoga",
|
||
"中等": "moderate",
|
||
"常见": "common",
|
||
"偶见": "occasional",
|
||
"强": "strong",
|
||
"弱": "weak",
|
||
"中强": "medium-strong",
|
||
"中": "medium",
|
||
"主题": "Topic",
|
||
"建议": "Practice",
|
||
"宝石/次数": "Gem / count",
|
||
"说明": "Notes",
|
||
"太阳": "Sun",
|
||
"月亮": "Moon",
|
||
"火星": "Mars",
|
||
"水星": "Mercury",
|
||
"木星": "Jupiter",
|
||
"金星": "Venus",
|
||
"土星": "Saturn",
|
||
"北交点/罗睺": "Rahu",
|
||
"南交点/计都": "Ketu",
|
||
"北交点": "Rahu",
|
||
"南交点": "Ketu",
|
||
"极友": "great friend",
|
||
"入友": "friendly",
|
||
"友好星座": "friendly sign",
|
||
"中性": "neutral",
|
||
"落陷": "debilitated",
|
||
"入庙": "own sign",
|
||
"充足": "adequate",
|
||
"自然吉星": "natural benefic",
|
||
"自然凶星": "natural malefic",
|
||
"功能吉星": "functional benefic",
|
||
"功能凶星": "functional malefic",
|
||
"功能中性": "functional neutral",
|
||
"第": "house ",
|
||
"宫": "",
|
||
"足": "pada",
|
||
"大运": "Dasha",
|
||
"财富": "wealth",
|
||
"事业": "career",
|
||
"健康": "health",
|
||
"吉性": "benefic",
|
||
"王权/事业": "authority/career",
|
||
"天象格局": "configuration",
|
||
"子女/创造": "children/creativity",
|
||
"体质参考": "constitution reference",
|
||
"寿命/健康": "constitution reference",
|
||
"愿望/收益": "desire/gains",
|
||
"解脱/内在": "moksha/inner life",
|
||
"贵人/助力": "patronage/support",
|
||
"迁移/出行": "travel/movement",
|
||
"婚姻稳定": "marital stability",
|
||
"自定义条件满足": "custom condition satisfied",
|
||
"同在": "together in",
|
||
"在": "in",
|
||
"与": "and",
|
||
"白羊座": "Aries",
|
||
"金牛座": "Taurus",
|
||
"双子座": "Gemini",
|
||
"巨蟹座": "Cancer",
|
||
"狮子座": "Leo",
|
||
"处女座": "Virgo",
|
||
"天秤座": "Libra",
|
||
"天蝎座": "Scorpio",
|
||
"射手座": "Sagittarius",
|
||
"摩羯座": "Capricorn",
|
||
"水瓶座": "Aquarius",
|
||
"双鱼座": "Pisces",
|
||
"前阿沙陀(Purva Ashadha)": "Purva Ashadha",
|
||
"Mula(根、本源)": "Mula",
|
||
"后破伽(Uttara Phalguni)": "Uttara Phalguni",
|
||
"后阿沙陀(Uttara Ashadha)": "Uttara Ashadha",
|
||
"后跋陀罗(Uttara Bhadrapada)": "Uttara Bhadrapada",
|
||
"阿湿毗尼(Ashwini)": "Ashwini",
|
||
"巴拉尼(Bharani)": "Bharani",
|
||
"基利提卡(Krittika)": "Krittika",
|
||
"罗希尼(Rohini)": "Rohini",
|
||
"鹿首(Mrigashira)": "Mrigashira",
|
||
"阿尔德拉(Ardra)": "Ardra",
|
||
"普那婆苏(Punarvasu)": "Punarvasu",
|
||
"普沙(Pushya)": "Pushya",
|
||
"阿什列沙(Ashlesha)": "Ashlesha",
|
||
"摩伽(Magha)": "Magha",
|
||
"前破伽(Purva Phalguni)": "Purva Phalguni",
|
||
"哈斯塔(Hasta)": "Hasta",
|
||
"吉多罗(Chitra)": "Chitra",
|
||
"斯瓦蒂(Swati)": "Swati",
|
||
"毗舍佉(Vishakha)": "Vishakha",
|
||
"阿奴罗陀(Anuradha)": "Anuradha",
|
||
"杰耶什塔(Jyeshtha)": "Jyeshtha",
|
||
"室罗伐那(Shravana)": "Shravana",
|
||
"陀尼湿陀(Dhanishta)": "Dhanishta",
|
||
"百药宿(Shatabhisha)": "Shatabhisha",
|
||
"前跋陀罗(Purva Bhadrapada)": "Purva Bhadrapada",
|
||
"雷瓦蒂(Revati)": "Revati",
|
||
}
|
||
for raw, public in sorted(reverse_terms.items(), key=lambda item: len(item[0]), reverse=True):
|
||
text = text.replace(raw, public)
|
||
phrase_terms = {
|
||
"Sudarshana Chakra 三参考点盘 (BPHS标准)": "Sudarshana Chakra tri-reference chart (BPHS standard)",
|
||
"行星、Nakshatra与宫位解释": "Planets, Nakshatras and Houses",
|
||
"身份、目标与可见责任": "identity, purpose and visible responsibility",
|
||
"情绪、安全感与适应方式": "emotional security and adaptive response",
|
||
"行动、竞争与执行方式": "action, competition and execution style",
|
||
"学习、表达与交易能力": "learning, expression and exchange",
|
||
"信念、判断与扩张方式": "belief, judgment and expansion",
|
||
"关系、审美与协调能力": "relationship, taste and harmonizing capacity",
|
||
"结构、压力与长期责任": "structure, pressure and long-term responsibility",
|
||
"业力放大、突破与非传统路径": "amplification, disruption and unconventional pathways",
|
||
"收束、分离与内在化路径": "release, separation and inwardization",
|
||
"职业、职责与社会角色": "career, duty and public role",
|
||
"合作、关系与对外互动": "partnership, contracts and public interaction",
|
||
"自我选择、身体节律和人生方向": "self-direction, body rhythm and life orientation",
|
||
"家庭基础、居住与内在稳定": "home base, residence and inner stability",
|
||
"行动、学习与沟通": "initiative, learning and communication",
|
||
"资源、表达与家庭价值": "resources, speech and family values",
|
||
"创造、学习与判断": "creativity, learning and judgment",
|
||
"服务、压力与日常责任": "service, pressure and daily obligations",
|
||
"共享资源、风险与深层变化": "shared resources, risk and deep change",
|
||
"信念、远行与高阶学习": "belief, long-distance travel and higher learning",
|
||
"收益、社群与长期愿景": "gains, networks and long-range aims",
|
||
"休整、支出与幕后领域": "retreat, expenditure and behind-the-scenes matters",
|
||
"这颗星的表达会通过定位星所在宫位继续展开": "the planet continues through the house occupied by its dispositor",
|
||
"Nakshatra主会进一步修饰这颗星的表达方式,并与当前Dasha节奏一起阅读": "the nakshatra lord further qualifies the planet and is read with the current Dasha rhythm",
|
||
"自然属性与功能属性不同,阅读时需要同时参考两层": "the natural and functional roles differ, so both layers are considered",
|
||
"这些力量信息用于判断该落点的表现strongweak,并与宫主、Nakshatra和Dasha节奏合看": "these strength factors are read with the house lord, nakshatra and Dasha rhythm",
|
||
"当前不在Dasha主导层,其Topic更偏向本命基础背景": "is not currently a leading Dasha factor; its topic is mainly natal background",
|
||
"会把该宫Topic带到": "carries this house topic into",
|
||
"表现为必须落到行动和成果": "showing that the matter must be grounded in action and results",
|
||
"表现为需要通过他人关系显化": "showing that the matter expresses through others and relationship interfaces",
|
||
"表现为本人主动承接": "showing direct personal involvement",
|
||
"判断strongweak时同时参考宫主尊贵、同宫行星、相位、六维力量、分盘重复度和当前Dasha": "strength is judged with dignity, conjunctions, aspects, Shadbala, divisional repetition and current Dasha",
|
||
"这一宫主位置需要与行星力量、分盘和相位合看": "this house-lord placement is read with planetary strength, divisions and aspects",
|
||
"当前不在活动Dasha主导层,该宫主Topic以本命基础背景为主": "is not currently active as a Dasha ruler; this house topic remains natal background",
|
||
"无其他同宫行星": "no other planets in the same house",
|
||
"本案检测到": "detected in this chart",
|
||
"本案未检测到": "not detected in this chart",
|
||
"原始摘要": "source summary",
|
||
"结构字段": "structured fields",
|
||
"未列额外结构字段": "no additional structured fields listed",
|
||
"传统辅助Practice": "Traditional Supportive Practices",
|
||
"瑜伽": "Yoga",
|
||
"格局": "Yoga",
|
||
"王者": "authority",
|
||
"落陷取消": "debilitation cancellation",
|
||
"大地": "earth/support",
|
||
"后": "after",
|
||
"前": "before",
|
||
"月吉夹": "Moon hemmed by benefics",
|
||
"身体": "body",
|
||
"疾病": "illness",
|
||
"独子": "single child",
|
||
"常行": "frequent movement",
|
||
"真诚": "sincerity",
|
||
"田园": "field/land",
|
||
"婚姻": "marriage",
|
||
"贤妻": "spouse virtue",
|
||
"性欲": "sensuality",
|
||
"消瘦体型": "lean body",
|
||
"精神错乱": "mental disturbance",
|
||
"言语障碍": "speech impediment",
|
||
"车": "cart",
|
||
"贫困": "poverty",
|
||
"被亲属遗弃": "family abandonment",
|
||
"母亲不利": "mother affliction",
|
||
"权力地位;事业成功;社会影响力": "authority, career success and social influence",
|
||
"财富丰厚;生活富足;物质成功": "wealth, comfort and material success",
|
||
"财富充裕;生活舒适;性情愉快": "wealth, comfort and pleasant disposition",
|
||
"自力更生;财富充裕;受人尊敬": "self-reliance, wealth and respect",
|
||
"名声清白;受人敬仰;事业有成": "clean reputation, respect and career success",
|
||
"婚姻延迟;需要匹配化解": "marital delay; matching and remedy may be needed",
|
||
"婚姻延迟;配偶关系紧张": "marital delay and partnership tension",
|
||
"财务困难;收入不稳定;需节俭": "financial difficulty, unstable income and need for thrift",
|
||
"表达受阻,可能表现为说话不顺、迟疑或难以清楚表达": "speech may be obstructed, hesitant or difficult to express clearly",
|
||
"思绪容易扰动,需留意情绪、压力与判断稳定性": "the mind may be disturbed; emotional pressure and judgment stability need care",
|
||
"体型偏瘦,体质较敏感": "leaner body type and sensitive constitution",
|
||
"水瓶": "Aquarius",
|
||
"天蝎": "Scorpio",
|
||
"白羊": "Aries",
|
||
"摩羯": "Capricorn",
|
||
"金牛": "Taurus",
|
||
"双子": "Gemini",
|
||
"巨蟹": "Cancer",
|
||
"狮子": "Leo",
|
||
"处女": "Virgo",
|
||
"天秤": "Libra",
|
||
"射手": "Sagittarius",
|
||
"双鱼": "Pisces",
|
||
"火": "Mars",
|
||
"水": "Mercury",
|
||
"木": "Jupiter",
|
||
"金": "Venus",
|
||
"土": "Saturn",
|
||
"日": "Sun",
|
||
"月": "Moon",
|
||
}
|
||
for raw, public in sorted(phrase_terms.items(), key=lambda item: len(item[0]), reverse=True):
|
||
text = text.replace(raw, public)
|
||
text = normalize_en_markdown_terms(text)
|
||
|
||
def _retain_remaining_cjk_lines(raw_text: str) -> str:
|
||
# Preserve source values when the English glossary has no translation.
|
||
# Dropping a whole row makes the English delivery look clean while
|
||
# silently losing evidence. A visible marker keeps the row auditable
|
||
# until a real translation is added.
|
||
kept: list[str] = []
|
||
for line in raw_text.splitlines():
|
||
if re.search(r"[\u4e00-\u9fff]", line):
|
||
line = re.sub(
|
||
r"[\u4e00-\u9fff]+",
|
||
lambda match: f"[source text retained: {match.group(0)}]",
|
||
line,
|
||
)
|
||
kept.append(line.rstrip())
|
||
return "\n".join(kept)
|
||
|
||
text = _retain_remaining_cjk_lines(text)
|
||
drop_fragments = (
|
||
"jd_ut:",
|
||
"time_offset_seconds",
|
||
"swisseph_newton",
|
||
"source_path",
|
||
"producer",
|
||
"schema",
|
||
"parameter_sensitive",
|
||
"blocked",
|
||
"PyJHora",
|
||
)
|
||
cleaned_lines = [
|
||
line.rstrip()
|
||
for line in text.splitlines()
|
||
if not any(fragment in line for fragment in drop_fragments)
|
||
]
|
||
text = "\n".join(cleaned_lines)
|
||
text = re.sub(r"(?m)^(#{2,6}\s*)p\d+(?:-p\d+)?\s*[·.-]?\s*", r"\1", text)
|
||
text = re.sub(r"\s*\(PL9 p\d+(?:-p\d+)?[^)]*\)", "", text)
|
||
text = re.sub(r"\s*(PL9 p\d+(?:-p\d+)?[^)]*)", "", text)
|
||
text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE)
|
||
text = text.replace(":", ": ").replace(";", "; ").replace(",", ", ").replace("。", ".")
|
||
text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
|
||
note_heading = "## Local Equivalent Interpretive Notes"
|
||
first_note = text.find(note_heading)
|
||
second_note = text.find(note_heading, first_note + len(note_heading)) if first_note >= 0 else -1
|
||
if second_note >= 0:
|
||
text = text[:second_note].rstrip() + "\n"
|
||
if not text.startswith("# "):
|
||
text = f"# {title}\n\n{text}"
|
||
text = _clean_pl9_user_table_placeholders(text, language="en")
|
||
known_header_repairs = {
|
||
"|-------|------|------|-------|----------|": "| House | Sign | Lord | Score | Key factors |\n|-------|------|------|-------|----------|",
|
||
"|----------|----------|--------|--------------|-------------------|-----|----------|----------|": "| Planet 1 | Planet 2 | Aspect | Exact degree | Actual difference | Orb | Applying | Strength |\n|----------|----------|--------|--------------|-------------------|-----|----------|----------|",
|
||
}
|
||
for separator, repaired in known_header_repairs.items():
|
||
text = re.sub(
|
||
rf"(?m)^{re.escape(separator)}$",
|
||
repaired,
|
||
text,
|
||
)
|
||
text = _repair_user_facing_orphan_table_headers(text, language="en")
|
||
# The parity renderer contributes sections after the raw appendix has
|
||
# already been normalized. Run the same semantic cleanup at the final
|
||
# user-volume boundary so legacy payload fragments cannot leak through.
|
||
text = re.sub(
|
||
r"\b(Mars|Jupiter|Venus|Saturn|Mercury|Sun|Moon|Rahu|Ketu)\1(?=\d|\b)",
|
||
r"\1 ",
|
||
text,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
text = re.sub(
|
||
r"\b(Mars|Jupiter|Venus|Saturn|Mercury|Sun|Moon|Rahu|Ketu)(\d{1,2})\b",
|
||
r"\1 \2",
|
||
text,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
text = re.sub(r"(?i)(condition)(houses?|signs?|planets?)", r"\1 \2", text)
|
||
text = re.sub(
|
||
r"\b(houses?|signs?|planets?)\s+\1\b",
|
||
r"\1",
|
||
text,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
# 防御性幂等:折叠残留的 "condition condition"(确保 sanitize 可安全重复执行)。
|
||
text = re.sub(r"\b(condition)(?:\s+condition)+\b", r"\1", text, flags=re.IGNORECASE)
|
||
text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
|
||
return text
|
||
|
||
|
||
|
||
def _pl9_markdown_hygiene_receipt(markdown: str, *, language: str) -> dict[str, Any]:
|
||
"""Check the Markdown before rendering user-facing PDF."""
|
||
|
||
try:
|
||
from pl9_language_terms import language_quality_receipt
|
||
except ModuleNotFoundError: # pragma: no cover - package import compatibility
|
||
from scripts.pl9_language_terms import language_quality_receipt
|
||
|
||
def is_table_line(line: str) -> bool:
|
||
stripped = line.strip()
|
||
return stripped.startswith("|") and stripped.endswith("|")
|
||
|
||
def is_separator(line: str) -> bool:
|
||
stripped = line.strip()
|
||
if not is_table_line(stripped):
|
||
return False
|
||
return stripped.replace("|", "").replace("-", "").replace(":", "").strip() == ""
|
||
|
||
def orphan_separator_lines(text: str) -> list[int]:
|
||
lines = text.splitlines()
|
||
offenders: list[int] = []
|
||
for index, line in enumerate(lines):
|
||
if not is_separator(line):
|
||
continue
|
||
previous = lines[index - 1] if index else ""
|
||
next_line = lines[index + 1] if index + 1 < len(lines) else ""
|
||
if (
|
||
not is_table_line(previous)
|
||
or is_separator(previous)
|
||
or not is_table_line(next_line)
|
||
or is_separator(next_line)
|
||
):
|
||
offenders.append(index + 1)
|
||
return offenders
|
||
|
||
if language == 'zh':
|
||
forbidden = [
|
||
"parameter_sensitive", "source_path", "producer", "blocked",
|
||
"Applicable ", "| Yoga | Category | Strength | Combination | Effects / notes |",
|
||
"conjunction", "kalatra", "solar_yoga", "lunar_yoga", "durbhaga",
|
||
"Excessive sensuality", "Sharp intellect", "Dharidhra", "{planets}",
|
||
"长页解释", "True 普沙", "Results of ", "field_状态",
|
||
"Maha 大运", "Antar 大运", "Pratyantar 大运", "Effects of ",
|
||
"General effects during", "General effects which are felt",
|
||
"Interpretations based on the condition",
|
||
"Natural friends", "Natural enemies", "Temporary relation",
|
||
"Planet 1", "Planet 2", "Exact degree", "Actual difference",
|
||
"From house", "Target house", "Aspect type", "Applying",
|
||
"现实场域中阅读", "这一现实场域",
|
||
]
|
||
else:
|
||
forbidden = [
|
||
"parameter_sensitive", "source_path", "producer", "blocked",
|
||
"未单独列出", "长页解释", "True 普沙", "{planets}",
|
||
"dasha_beginning_dates", "dasha_ending_dates", "jd_ut:", "time_offset_seconds",
|
||
":", ";", ",", "。", "、", "(", ")", "——",
|
||
"solar_yoga", "lunar_yoga",
|
||
]
|
||
violations = [item for item in forbidden if item in markdown]
|
||
if language == 'en':
|
||
chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", markdown))
|
||
if chinese_chars:
|
||
violations.append(f"chinese_characters:{chinese_chars}")
|
||
language_receipt = language_quality_receipt(markdown, language=language)
|
||
violations.extend(language_receipt.get("violations") or [])
|
||
orphan_lines = orphan_separator_lines(markdown)
|
||
if orphan_lines:
|
||
violations.append(f"orphan_table_separators:{','.join(str(line) for line in orphan_lines[:12])}")
|
||
return {
|
||
"schema": "pl9.markdown_hygiene_receipt.v1",
|
||
"language": language,
|
||
"status": "pass" if not violations else "fail",
|
||
"violations": violations,
|
||
"language_quality": language_receipt,
|
||
}
|
||
|
||
|
||
|
||
def _pl9_ai_density_visibility_receipt(markdown: str, *, language: str,
|
||
maturity_contract: dict[str, Any] | None = None) -> dict[str, Any]:
|
||
"""Fail fast when mature PL9 AI-density page families disappear."""
|
||
|
||
if language == 'en':
|
||
required = {
|
||
"raw_positions": ("Birth Chart Planetary Raw Positions",),
|
||
"divisional_tables": ("Natal and Divisional North Indian Charts", "D1 Rashi Numeric Position Table"),
|
||
"technical_pages": ("Shadbala", "Ashtakavarga"),
|
||
"dasha_tables": ("Vimshottari Dasha", "Pratyantar Dasha"),
|
||
"saturn_kp": ("Sade Sati", "Dhayya", "KP"),
|
||
"annual_tables": ("Tajika", "without Dasha balance", "Sahams"),
|
||
"interpretive_notes": ("Maha Dasha", "Pratyantar Dasha"),
|
||
}
|
||
else:
|
||
required = {
|
||
"numeric_charts": ("D1 Rashi 数值位置表", "月亮盘", "特殊上升"),
|
||
"technical_pages": ("六维力量", "Ashtakavarga", "相位矩阵"),
|
||
"dasha_tables": ("Vimshottari AD", "Vimshottari PD", "Kala Chakra 大运"),
|
||
"saturn_kp": ("Sade Sati", "Dhayya", "KP"),
|
||
"annual_pages": ("年度图盘与 Tajika", "Mudda", "Patyayini", "Sahams"),
|
||
"long_form_interpretation": ("行星、星宿与宫位解释", "Bhavesh 宫主逐宫解释", "## 大运解释"),
|
||
}
|
||
missing = {
|
||
family: [marker for marker in markers if marker not in markdown]
|
||
for family, markers in required.items()
|
||
}
|
||
missing = {family: markers for family, markers in missing.items() if markers}
|
||
return {
|
||
"schema": "pl9.ai_density_visibility_receipt.v1",
|
||
"language": language,
|
||
"status": "pass" if not missing else "fail",
|
||
"missing": missing,
|
||
}
|
||
|
||
|
||
|
||
def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str:
|
||
"""Render the English AI/astrologer edition directly from structured payload."""
|
||
|
||
CN_TERMS_EN, COMBINATION_EN, EFFECTS_EN = _load_glossary()
|
||
calc_sudarshana_chakra = _load_sudarshana()
|
||
|
||
english_field_explanations = {
|
||
"effect_on_confidence": (
|
||
"Reduce confidence when functional and natural indications conflict."
|
||
),
|
||
"confidence_impact": (
|
||
"Confidence is reduced when functional and natural indications conflict."
|
||
),
|
||
}
|
||
|
||
def clean(value: Any) -> str:
|
||
if value is None:
|
||
return "-"
|
||
if isinstance(value, bool):
|
||
return "true" if value else "false"
|
||
if isinstance(value, float):
|
||
return f"{value:.6f}".rstrip("0").rstrip(".")
|
||
if isinstance(value, (int,)):
|
||
return str(value)
|
||
if isinstance(value, dict):
|
||
parts = []
|
||
for key, item in list(value.items())[:8]:
|
||
key_text = clean(key)
|
||
if key_text in english_field_explanations:
|
||
value_text = english_field_explanations[key_text]
|
||
else:
|
||
value_text = clean(item)
|
||
parts.append(f"{key_text}={value_text}")
|
||
text = "; ".join(parts)
|
||
if len(value) > 8:
|
||
text += f"; ... +{len(value) - 8} fields"
|
||
return text[:480] + ("..." if len(text) > 480 else "")
|
||
if isinstance(value, (list, tuple, set)):
|
||
parts = [clean(item) for item in list(value)[:12]]
|
||
text = ", ".join(parts)
|
||
if len(value) > 12:
|
||
text += f", ... +{len(value) - 12} items"
|
||
return text[:480] + ("..." if len(text) > 480 else "")
|
||
text = str(value)
|
||
# 整串中文 effect 短词 -> glossary 直译(最高优先级,避免后续替换破坏语义)
|
||
_stripped = text.strip()
|
||
if _stripped in EFFECTS_EN:
|
||
return EFFECTS_EN[_stripped]
|
||
# 组合条件整句 -> 英文(逐词替换之前全串精确匹配,避免整句被拆坏)
|
||
if _stripped in COMBINATION_EN:
|
||
return COMBINATION_EN[_stripped]
|
||
# 中文行星/星座/结构词 -> 英文(combination 混排串逐词替换,避免删 CJK 后黏连)。
|
||
# 按 key 长度降序替换:长词(宫主/上升主)先于短词(宫/主),避免被拆开覆盖。
|
||
for _cn, _en in sorted(CN_TERMS_EN.items(), key=lambda kv: -len(kv[0])):
|
||
text = text.replace(_cn, _en)
|
||
replacements = {
|
||
":": ": ",
|
||
";": "; ",
|
||
",": ", ",
|
||
"。": ".",
|
||
"、": ", ",
|
||
"(": "(",
|
||
")": ")",
|
||
"——": ": ",
|
||
"{planets}": "listed planets",
|
||
"solar_yoga": "solar yoga",
|
||
"lunar_yoga": "lunar yoga",
|
||
"北交点": "Rahu",
|
||
"南交点": "Ketu",
|
||
"极友": "great friend",
|
||
"入友": "friendly",
|
||
"友好星座": "friendly sign",
|
||
"中性": "neutral",
|
||
"落陷": " debilitated",
|
||
"入庙": "own sign",
|
||
"中强": "medium-strong",
|
||
"强": " strong",
|
||
"弱": " weak",
|
||
"中": "medium",
|
||
}
|
||
for raw, public in replacements.items():
|
||
text = text.replace(raw, public)
|
||
text = re.sub(r"(?:^|;\s*)-(?:\s*;\s*-){1,}\s*$", "-", text)
|
||
text = re.sub(r"\blisted planets(\d+)\b", r"\1 listed planet(s)", text)
|
||
text = text.replace("missing_in_local", "-")
|
||
text = text.replace("Dharidhra:", "Daridra combination:")
|
||
text = text.replace("D1/D9debilitated", "D1/D9 debilitated")
|
||
text = re.sub(r"^(\d+)weak/$", r"house \1 weak", text)
|
||
text = re.sub(r"^(\d+)/$", r"house \1 condition", text)
|
||
text = re.sub(r"^//,\s*", "", text)
|
||
text = re.sub(r"([A-Za-z]{3,})\1", r"\1", text)
|
||
text = re.sub(
|
||
r"\b(Mars|Jupiter|Venus|Saturn|Mercury|Sun|Moon|Rahu|Ketu)(\d{1,2})\b",
|
||
r"\1 \2",
|
||
text,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
text = re.sub(r"\b(houses?|signs?|planets?)\s+\1\b", r"\1", text, flags=re.IGNORECASE)
|
||
# 先折叠连续重复的 condition(如 "condition condition"),再重排
|
||
text = re.sub(r"\b(condition)(?:\s+condition)+\b", r"\1", text, flags=re.IGNORECASE)
|
||
text = re.sub(
|
||
r"\b(houses?|signs?|planets?)\s+([0-9/, -]+?)\s+condition\b",
|
||
r"condition \1 \2",
|
||
text,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
text = re.sub(r"[\u4e00-\u9fff]+", "", text)
|
||
# 黏连修复:行星 + dignity 词(如 Venusexalted -> Venus exalted)
|
||
text = re.sub(
|
||
r"\b(Mars|Jupiter|Venus|Saturn|Mercury|Sun|Moon|Rahu|Ketu)(exalted|debilitated|strong|weak|moderate|medium|own|moolatrikona|combust)\b",
|
||
r"\1 \2",
|
||
text,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
# 黏连修复:小写词尾 + 大写词首(dignity/行星 + 星座,如 strongAquarius / SaturnAquarius)
|
||
text = re.sub(r"([a-z])([A-Z])", r"\1 \2", text)
|
||
# condition 重复去重(如 "condition condition")
|
||
text = re.sub(r"\b(condition)\s+\1\b", r"\1", text, flags=re.IGNORECASE)
|
||
# 移除 "=" 残留(Sade Sati 等 rashi 标签前缀,如 "=Capricorn" -> "Capricorn")
|
||
text = re.sub(r"=\s*(?=[A-Z])", "", text)
|
||
text = re.sub(r"\b([A-Za-z ]+)\(\1\)", r"\1", text)
|
||
text = re.sub(r"\s+", " ", text)
|
||
text = re.sub(r"\(\s*\)", "", text)
|
||
text = text.strip() or "-"
|
||
if not re.search(r"[A-Za-z0-9]", text):
|
||
# 删 CJK / 无英文等价后降级为 "-"(空占位),不输出 "Not available" 占位符。
|
||
return "-"
|
||
return text[:480] + ("..." if len(text) > 480 else "")
|
||
|
||
def clean_effect(value: Any) -> str:
|
||
"""Translate one effect phrase; return "" when it has no English equivalent."""
|
||
if not isinstance(value, str):
|
||
return clean(value)
|
||
stripped = value.strip()
|
||
translated = EFFECTS_EN.get(stripped)
|
||
if 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):
|
||
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:
|
||
if not rows:
|
||
return ""
|
||
cleaned_rows = [[clean(cell) for cell in row] for row in rows]
|
||
empty_values = {"", "-", "None", "none", "null", "missing_in_local"}
|
||
filtered_rows: list[list[str]] = []
|
||
for row in cleaned_rows:
|
||
non_empty = [cell for cell in row if cell not in empty_values]
|
||
if not non_empty:
|
||
continue
|
||
if row and row[0] in empty_values and len(non_empty) <= 1:
|
||
continue
|
||
if len(row) > 1 and row[0] not in empty_values and not any(cell not in empty_values for cell in row[1:]):
|
||
continue
|
||
filtered_rows.append(row)
|
||
cleaned_rows = filtered_rows
|
||
if not cleaned_rows:
|
||
return ""
|
||
keep_indices: list[int] = []
|
||
for index, header in enumerate(headers):
|
||
column = [row[index] if index < len(row) else "-" for row in cleaned_rows]
|
||
non_empty = [cell for cell in column if cell not in empty_values]
|
||
if not non_empty:
|
||
continue
|
||
if header in {"Longitude", "House"} and len(non_empty) <= 1 and len(cleaned_rows) > 3:
|
||
continue
|
||
keep_indices.append(index)
|
||
headers = [headers[index] for index in keep_indices]
|
||
cleaned_rows = [[row[index] if index < len(row) else "-" for index in keep_indices] for row in cleaned_rows]
|
||
out = ["| " + " | ".join(headers) + " |", "|" + "|".join(["---"] * len(headers)) + "|"]
|
||
for row in cleaned_rows:
|
||
display_row = []
|
||
for header, cell in zip(headers, row):
|
||
if cell == "-" and header in {"Effects / notes", "Summary", "Notes", "Detected / phase"}:
|
||
display_row.append("not listed")
|
||
else:
|
||
display_row.append(cell)
|
||
out.append("| " + " | ".join(display_row) + " |")
|
||
return "\n".join(out)
|
||
|
||
def dict_get(path: tuple[str, ...], default: Any = None) -> Any:
|
||
obj: Any = packet
|
||
for key in path:
|
||
if not isinstance(obj, dict):
|
||
return default
|
||
obj = obj.get(key)
|
||
return obj if obj is not None else default
|
||
|
||
def dict_get_any(*paths: tuple[str, ...], default: Any = None) -> Any:
|
||
for path in paths:
|
||
value = dict_get(path)
|
||
if value:
|
||
return value
|
||
return default
|
||
|
||
lines: list[str] = [f"# {_pl9_english_report_title(packet)}", ""]
|
||
birth = dict_get(("birth_info",), {})
|
||
if isinstance(birth, dict):
|
||
lines.extend([
|
||
"## Birth Data and Calculation Profile",
|
||
"",
|
||
table(["Field", "Value"], [
|
||
["Name", birth.get("name") or birth.get("user_name")],
|
||
["Birth date", birth.get("date")],
|
||
["Birth time", birth.get("time")],
|
||
["Latitude", birth.get("lat")],
|
||
["Longitude", birth.get("lon")],
|
||
["Time zone", f"UTC{birth.get('tz'):+}" if isinstance(birth.get("tz"), (int, float)) else birth.get("tz")],
|
||
["Ayanamsa", birth.get("ayanamsa_display") or birth.get("ayanamsa_name") or birth.get("ayanamsa")],
|
||
["Node mode", birth.get("node_mode")],
|
||
]),
|
||
"",
|
||
])
|
||
|
||
asc = dict_get(("core_chart", "ascendant"), {})
|
||
planets = dict_get(("core_chart", "planets"), {})
|
||
rows = []
|
||
if isinstance(asc, dict):
|
||
rows.append(["Lagna", asc.get("sign"), asc.get("degree_in_sign"), asc.get("degree_raw") or asc.get("lon"), 1, "-", "-", "-", "-"])
|
||
if isinstance(planets, dict):
|
||
for planet in ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]:
|
||
item = planets.get(planet, {})
|
||
if isinstance(item, dict):
|
||
rows.append([
|
||
planet,
|
||
item.get("sign"),
|
||
item.get("degree_in_sign"),
|
||
item.get("degree") or item.get("longitude"),
|
||
item.get("house"),
|
||
item.get("nakshatra"),
|
||
item.get("pada"),
|
||
item.get("status"),
|
||
item.get("speed"),
|
||
])
|
||
lines.extend(["## Core Chart Raw Positions", "", table(["Object", "Sign", "Degree in sign", "Longitude", "House", "Nakshatra", "Pada", "Status", "Speed"], rows), ""])
|
||
|
||
if isinstance(asc, dict) and isinstance(planets, dict):
|
||
try:
|
||
asc_lon = asc.get("longitude")
|
||
if asc_lon is None:
|
||
asc_lon = asc.get("lon") or asc.get("degree") or asc.get("degree_raw")
|
||
planet_lons = {
|
||
planet: item.get("longitude") if item.get("longitude") is not None else item.get("degree")
|
||
for planet, item in planets.items()
|
||
if isinstance(item, dict) and (item.get("longitude") is not None or item.get("degree") is not None)
|
||
}
|
||
if asc_lon is not None and planet_lons:
|
||
sudarshana = calc_sudarshana_chakra(planet_lons, float(asc_lon))
|
||
reference_rows = []
|
||
for key, value in (sudarshana.get("reference_points") or {}).items():
|
||
if isinstance(value, dict):
|
||
reference_rows.append([key, value.get("sign"), value.get("role")])
|
||
if reference_rows:
|
||
lines.extend(["## Sudarshana Chakra", "", table(["Reference point", "Sign", "Role"], reference_rows), ""])
|
||
chart_rows = []
|
||
for chart_name, chart in (sudarshana.get("three_charts") or {}).items():
|
||
if not isinstance(chart, dict):
|
||
continue
|
||
for planet, item in chart.items():
|
||
if isinstance(item, dict):
|
||
chart_rows.append([chart_name, planet, item.get("sign"), item.get("house")])
|
||
if chart_rows:
|
||
lines.extend(["### Sudarshana Chakra Three-Reference Placements", "", table(["Reference chart", "Planet", "Sign", "House"], chart_rows), ""])
|
||
except Exception:
|
||
pass
|
||
|
||
d1_sheet = dict_get(("worksheets", "d1_rasi_bhava"), {})
|
||
d1_houses = d1_sheet.get("houses") if isinstance(d1_sheet, dict) else {}
|
||
d1_planets = d1_sheet.get("planets") if isinstance(d1_sheet, dict) else {}
|
||
if not isinstance(d1_houses, dict):
|
||
d1_houses = dict_get(("core_chart", "houses"), {})
|
||
if not isinstance(d1_planets, dict):
|
||
d1_planets = dict_get(("core_chart", "planets"), {})
|
||
if isinstance(d1_houses, dict) and isinstance(d1_planets, dict) and d1_houses:
|
||
current_dasha = dict_get(("timing_and_predictive_systems", "dasha", "current_dasha"), {})
|
||
active_lords: set[str] = set()
|
||
if isinstance(current_dasha, dict) and current_dasha.get("lord"):
|
||
active_lords.add(str(current_dasha.get("lord")))
|
||
for ad_row in current_dasha.get("antardasha_timeline") or []:
|
||
if isinstance(ad_row, dict) and ad_row.get("is_current") and ad_row.get("lord"):
|
||
active_lords.add(str(ad_row.get("lord")))
|
||
house_topics = {
|
||
1: "self, body and life direction",
|
||
2: "resources, speech and family values",
|
||
3: "initiative, learning and communication",
|
||
4: "home, residence and inner stability",
|
||
5: "creativity, intelligence and judgment",
|
||
6: "service, pressure and daily duties",
|
||
7: "partnership, marriage and public interaction",
|
||
8: "shared resources, risk, research and transformation",
|
||
9: "belief, teachers, higher learning and long journeys",
|
||
10: "career, responsibility and public role",
|
||
11: "gains, networks and long-range aims",
|
||
12: "retreat, expenditure, sleep, foreign places and release",
|
||
}
|
||
destination_topics = {
|
||
1: "identity and bodily expression",
|
||
2: "resources, speech and family continuity",
|
||
3: "effort, skill and communication",
|
||
4: "home, land and emotional foundation",
|
||
5: "learning, creativity and merit",
|
||
6: "workload, service, rivalry and repair",
|
||
7: "partnership, clients and public dealings",
|
||
8: "shared resources, research, vulnerability and deep change",
|
||
9: "belief, study, teachers and distant movement",
|
||
10: "career, authority and visible duty",
|
||
11: "income, networks and fulfilment of goals",
|
||
12: "expense, retreat, sleep, foreign links and release",
|
||
}
|
||
summary_rows = []
|
||
narrative_rows: list[str] = []
|
||
for house_index in range(1, 13):
|
||
house = d1_houses.get(f"house_{house_index}") or d1_houses.get(str(house_index)) or {}
|
||
if not isinstance(house, dict):
|
||
continue
|
||
lord = house.get("lord")
|
||
lord_row = d1_planets.get(str(lord)) if lord is not None else {}
|
||
if not isinstance(lord_row, dict):
|
||
lord_row = {}
|
||
sign = house.get("cusp_sign") or house.get("sign")
|
||
lord_sign = lord_row.get("sign")
|
||
lord_house = lord_row.get("house")
|
||
dignity = lord_row.get("status") or lord_row.get("dignity")
|
||
activation = "active Dasha layer" if str(lord) in active_lords else "natal background"
|
||
placement = f"{clean(lord_sign)} house {clean(lord_house)}"
|
||
summary_rows.append([house_index, sign, lord, placement, dignity, activation])
|
||
topic = house_topics.get(house_index, f"house {house_index} topics")
|
||
destination = destination_topics.get(int(lord_house), f"house {clean(lord_house)} topics") if str(lord_house).isdigit() else "its placement field"
|
||
narrative_rows.extend([
|
||
f"### House {house_index} lord: {clean(lord)}",
|
||
"",
|
||
f"House {house_index} covers {topic}. Its sign is {clean(sign)}, and its lord is {clean(lord)}.",
|
||
"",
|
||
f"The lord is placed in {placement}, with dignity/status {clean(dignity)}. This carries the house-{house_index} agenda into {destination}.",
|
||
"",
|
||
f"This placement is read with dignity, conjunctions, aspects, Shadbala, divisional repetition and Dasha timing. Current timing layer: {activation}.",
|
||
"",
|
||
])
|
||
if summary_rows:
|
||
lines.extend(["## Bhavesh House-Lord Interpretations", "", table(["House", "Sign", "Lord", "Lord placement", "Lord dignity", "Dasha activation"], summary_rows), ""])
|
||
lines.extend(narrative_rows)
|
||
|
||
moon_chart = dict_get(("divisional_and_special_charts", "moon_chart"), {})
|
||
if isinstance(moon_chart, dict):
|
||
rows = []
|
||
moon_planets = (moon_chart.get("raw") or {}).get("planets") if isinstance(moon_chart.get("raw"), dict) else moon_chart
|
||
for planet, item in (moon_planets or {}).items():
|
||
if isinstance(item, dict):
|
||
rows.append([planet, item.get("sign"), item.get("degree") or item.get("longitude"), item.get("house_from_moon") or item.get("house")])
|
||
lines.extend(["## Moon Chart", "", table(["Object", "Sign", "Longitude", "House"], rows), ""])
|
||
|
||
varga = dict_get(("divisional_and_special_charts", "varga_full"), {})
|
||
if isinstance(varga, dict):
|
||
lines.extend(["## Divisional Charts Numeric Tables", ""])
|
||
for chart_name, chart_payload in varga.items():
|
||
positions = chart_payload.get("positions") if isinstance(chart_payload, dict) else None
|
||
if not isinstance(positions, dict):
|
||
positions = chart_payload if isinstance(chart_payload, dict) else None
|
||
rows = []
|
||
if isinstance(positions, dict):
|
||
for obj_name, item in positions.items():
|
||
if str(obj_name).startswith("_") or obj_name in {"planets"}:
|
||
continue
|
||
if isinstance(item, dict):
|
||
rows.append([obj_name, item.get("sign"), item.get("degree_in_sign"), item.get("longitude"), item.get("house")])
|
||
if rows:
|
||
lines.extend([f"### {clean(chart_name)}", "", table(["Object", "Sign", "Degree in sign", "Longitude", "House"], rows), ""])
|
||
|
||
special_lagnas = dict_get(("divisional_and_special_charts", "special_lagnas"), {})
|
||
if isinstance(special_lagnas, dict):
|
||
rows = []
|
||
for name, item in special_lagnas.items():
|
||
if isinstance(item, dict) and item.get("sign"):
|
||
rows.append([name, item.get("sign"), item.get("sign_degree") or item.get("degree"), item.get("degree"), item.get("house")])
|
||
lines.extend(["## Special Lagnas", "", table(["Point", "Sign", "Degree in sign", "Longitude", "House"], rows), ""])
|
||
|
||
sensitive = dict_get(("divisional_and_special_charts", "sensitive_points"), {})
|
||
if isinstance(sensitive, dict):
|
||
rows = []
|
||
for name, item in sensitive.items():
|
||
if isinstance(item, dict) and item:
|
||
rows.append([name, item.get("sign"), item.get("degree_in_sign"), item.get("longitude"), item.get("lord")])
|
||
lines.extend(["## Sensitive Points", "", table(["Point", "Sign", "Degree in sign", "Longitude", "Lord"], rows), ""])
|
||
|
||
upagrahas = dict_get(("divisional_and_special_charts", "upagrahas", "raw"), {})
|
||
if isinstance(upagrahas, dict):
|
||
rows = []
|
||
sign_names = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
|
||
for name, item in upagrahas.items():
|
||
if isinstance(item, dict):
|
||
sign = item.get("sign")
|
||
if sign is None and isinstance(item.get("sign_idx"), int):
|
||
sign = sign_names[item["sign_idx"] % 12]
|
||
rows.append([name, sign, item.get("degree_in_sign"), item.get("longitude")])
|
||
lines.extend(["## Upagraha and Sub-Planet Points", "", table(["Point", "Sign", "Degree in sign", "Longitude"], rows), ""])
|
||
|
||
strength_sheet = dict_get_any(
|
||
("strengths_and_scores",),
|
||
("worksheets", "strengths_and_scores"),
|
||
default={},
|
||
)
|
||
shadbala = {}
|
||
if isinstance(strength_sheet, dict):
|
||
shadbala = (strength_sheet.get("shadbala") or {}).get("planets") if isinstance(strength_sheet.get("shadbala"), dict) else {}
|
||
if isinstance(shadbala, dict):
|
||
rows = []
|
||
for planet, item in shadbala.items():
|
||
if isinstance(item, dict):
|
||
sthana = item.get("sthana_bala", {})
|
||
kala = item.get("kala_bala", {})
|
||
rows.append([planet, sthana.get("total"), item.get("dig_bala"), kala.get("total"), item.get("chesta_bala"), item.get("naisargika_bala"), item.get("drik_bala"), item.get("total_virupas")])
|
||
lines.extend(["## Shadbala Components", "", table(["Planet", "Sthana", "Dig", "Kala", "Chesta", "Naisargika", "Drik", "Total"], rows), ""])
|
||
|
||
if isinstance(strength_sheet, dict):
|
||
functional = strength_sheet.get("functional_benefic_malefic")
|
||
if isinstance(functional, dict):
|
||
rows = []
|
||
for key, value in functional.items():
|
||
if key in english_field_explanations:
|
||
rows.append([key, english_field_explanations[key]])
|
||
continue
|
||
if isinstance(value, (list, tuple)):
|
||
rows.append([key, ", ".join(map(clean, value))])
|
||
elif isinstance(value, dict):
|
||
rows.append([key, "; ".join(f"{clean(k)}={clean(v)}" for k, v in value.items())])
|
||
else:
|
||
rows.append([key, value])
|
||
lines.extend(["## Functional Benefic / Malefic Layer", "", table(["Field", "Value"], rows), ""])
|
||
|
||
bhava_bala = strength_sheet.get("bhava_bala")
|
||
if isinstance(bhava_bala, dict):
|
||
bhava_house_rows = _load_bhava_rows()
|
||
rows = []
|
||
for item in bhava_house_rows(bhava_bala):
|
||
rows.append([item['house'], item.get("total") if item.get("total") is not None else item.get("score"), item.get("strength") or item.get("grade"), item.get("notes")])
|
||
lines.extend(["## Bhava Bala House Strength", "", table(["House", "Score", "Strength", "Notes"], rows), ""])
|
||
|
||
friendship = strength_sheet.get("planetary_friendship")
|
||
if isinstance(friendship, dict):
|
||
rows = []
|
||
for planet, item in friendship.items():
|
||
if isinstance(item, dict):
|
||
rows.append([planet, item.get("natural_friends"), item.get("natural_enemies"), item.get("temporary_relation") or item.get("compound_relationship")])
|
||
lines.extend(["## Planetary Friendship Matrix", "", table(["Planet", "Natural friends", "Natural enemies", "Temporary relation"], rows), ""])
|
||
|
||
av = strength_sheet.get("ashtakavarga") if isinstance(strength_sheet, dict) else {}
|
||
if isinstance(av, dict):
|
||
sav = av.get("sav", {})
|
||
if isinstance(sav, dict):
|
||
scores = sav.get("scores", {})
|
||
rows = [[sign, score] for sign, score in scores.items()] if isinstance(scores, dict) else []
|
||
lines.extend(["## Sarvashtakavarga", "", table(["Sign", "SAV"], rows), ""])
|
||
bav = av.get("bav", {})
|
||
if isinstance(bav, dict):
|
||
rows = []
|
||
for planet, scores in bav.items():
|
||
if isinstance(scores, dict):
|
||
bindus = scores.get("bindus")
|
||
if isinstance(bindus, list) and len(bindus) >= 12:
|
||
row = [planet] + bindus[:12] + [scores.get("total")]
|
||
else:
|
||
row = [planet] + [scores.get(sign) for sign in ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]] + [scores.get("total")]
|
||
rows.append(row)
|
||
lines.extend(["## Bhinnashtakavarga", "", table(["Planet", "Ar", "Ta", "Ge", "Cn", "Le", "Vi", "Li", "Sc", "Sg", "Cp", "Aq", "Pi", "Total"], rows), ""])
|
||
|
||
kp = dict_get(("advanced_systems", "kp"), {})
|
||
if isinstance(kp, dict):
|
||
planet_rows = []
|
||
for planet, item in (kp.get("planets") or {}).items():
|
||
if isinstance(item, dict):
|
||
lord = item.get("kp_lords", {})
|
||
sig = item.get("significators", {})
|
||
planet_rows.append([planet, lord.get("sign"), lord.get("nakshatra"), lord.get("rasi_lord"), lord.get("nakshatra_lord"), lord.get("sub_lord"), lord.get("sub_sub_lord"), sig.get("A"), sig.get("B"), sig.get("C"), sig.get("D")])
|
||
house_rows = []
|
||
for house, item in (kp.get("houses") or {}).items():
|
||
if isinstance(item, dict):
|
||
lord = item.get("kp_lords", {})
|
||
sig = item.get("significators", {})
|
||
house_rows.append([house, item.get("sign"), item.get("cusp_longitude"), lord.get("nakshatra"), lord.get("rasi_lord"), lord.get("nakshatra_lord"), lord.get("sub_lord"), lord.get("sub_sub_lord"), sig.get("A"), sig.get("B"), sig.get("C"), sig.get("D")])
|
||
lines.extend(["## KP Planet Significators", "", table(["Planet", "Sign", "Nakshatra", "Rasi lord", "Star lord", "Sub lord", "Sub-sub lord", "A", "B", "C", "D"], planet_rows), ""])
|
||
lines.extend(["## KP House Cusps and Significators", "", table(["House", "Sign", "Cusp longitude", "Nakshatra", "Rasi lord", "Star lord", "Sub lord", "Sub-sub lord", "A", "B", "C", "D"], house_rows), ""])
|
||
|
||
dasha_families = dict_get(("timing_and_predictive_systems", "dasha_master_pack", "families"), {})
|
||
if isinstance(dasha_families, dict):
|
||
lines.extend(["## Dasha Tables", ""])
|
||
for family, payload in dasha_families.items():
|
||
if not isinstance(payload, dict):
|
||
continue
|
||
periods = payload.get("periods") or payload.get("mahadasha") or []
|
||
rows = []
|
||
for item in periods[:80] if isinstance(periods, list) else []:
|
||
if isinstance(item, dict):
|
||
rows.append([item.get("lord") or item.get("planet"), item.get("rashi") or item.get("sign"), item.get("start") or item.get("start_date"), item.get("end") or item.get("end_date"), item.get("years") or (item.get("duration") or {}).get("years")])
|
||
if rows:
|
||
lines.extend([f"### {clean(family).replace('_', ' ').title()}", "", table(["Lord", "Sign/Rashi", "Start", "End", "Years"], rows), ""])
|
||
|
||
annual_pack = dict_get_any(
|
||
("worksheets", "timing_and_predictive_systems", "annual_tajika_pack"),
|
||
("timing_and_predictive_systems", "annual_tajika_pack"),
|
||
default={},
|
||
)
|
||
tajika = dict_get(("timing_and_predictive_systems", "tajika"), {})
|
||
if isinstance(tajika, dict) or isinstance(annual_pack, dict):
|
||
canonical_muntha = annual_pack.get("muntha") if isinstance(annual_pack, dict) else {}
|
||
canonical_year_lord = annual_pack.get("year_lord") if isinstance(annual_pack, dict) else {}
|
||
muntha = canonical_muntha if isinstance(canonical_muntha, dict) and canonical_muntha else tajika.get("muntha", {})
|
||
year_lord = canonical_year_lord if isinstance(canonical_year_lord, dict) and canonical_year_lord else tajika.get("year_lord", {})
|
||
if isinstance(muntha, dict) and muntha.get("status") == "conflict":
|
||
values = muntha.get("values") if isinstance(muntha.get("values"), list) else []
|
||
muntha_signs = [
|
||
item.get("value", {}).get("muntha_sign")
|
||
for item in values
|
||
if isinstance(item, dict) and isinstance(item.get("value"), dict)
|
||
]
|
||
muntha_value = "Conflict: " + " vs ".join(str(value) for value in muntha_signs if value)
|
||
else:
|
||
muntha_data = muntha.get("data") if isinstance(muntha.get("data"), dict) else muntha
|
||
muntha_value = muntha_data.get("muntha_sign") or muntha_data.get("sign")
|
||
if isinstance(year_lord, dict) and year_lord.get("status") == "conflict":
|
||
values = year_lord.get("values") if isinstance(year_lord.get("values"), list) else []
|
||
year_lords = [
|
||
item.get("value", {}).get("year_lord") or item.get("value", {}).get("lord")
|
||
for item in values
|
||
if isinstance(item, dict) and isinstance(item.get("value"), dict)
|
||
]
|
||
year_lord_value = "Conflict: " + " vs ".join(str(value) for value in year_lords if value)
|
||
else:
|
||
year_lord_data = year_lord.get("data") if isinstance(year_lord, dict) else {}
|
||
year_lord_value = (
|
||
year_lord_data.get("year_lord") or year_lord_data.get("lord")
|
||
if isinstance(year_lord_data, dict)
|
||
else year_lord
|
||
)
|
||
lines.extend(["## Annual Varshaphala Core Fields", "", table(["Field", "Value"], [
|
||
["Muntha sign", muntha_value],
|
||
["Muntha lord", (
|
||
(muntha.get("data") or {}).get("muntha_lord")
|
||
if isinstance(muntha, dict) and isinstance(muntha.get("data"), dict)
|
||
else (muntha.get("muntha_lord") if isinstance(muntha, dict) else None)
|
||
)],
|
||
["Year lord", year_lord_value],
|
||
]), ""])
|
||
mudda = (
|
||
annual_pack.get("mudda_dasha", {})
|
||
if isinstance(annual_pack, dict)
|
||
else {}
|
||
)
|
||
if not isinstance(mudda, dict) or not mudda:
|
||
mudda = tajika.get("mudda_dasha", {}) if isinstance(tajika, dict) else {}
|
||
if isinstance(mudda, dict):
|
||
rows = [[item.get("order"), item.get("lord"), item.get("months")] for item in mudda.get("dasha_sequence", []) if isinstance(item, dict)]
|
||
lines.extend(["## Mudda Dasha", "", table(["Order", "Lord", "Months"], rows), ""])
|
||
|
||
if isinstance(annual_pack, dict):
|
||
normalized = annual_pack.get("normalized_tables") if isinstance(annual_pack.get("normalized_tables"), dict) else {}
|
||
annual_table_specs = (
|
||
("mudda_rows", "Annual Mudda Dasha Rows", ("balance_profile", "lord", "start", "end", "boundary_status")),
|
||
("patyayini_rows", "Annual Patyayini / Patyamsha Rows", ("main", "sub", "boundary", "boundary_status")),
|
||
("saham_rows", "Annual Saham Rows", ("name", "saham", "sign", "degree_in_sign")),
|
||
("tajika_yoga_rows", "Annual Tajika Yoga Rows", ("yoga", "name", "present", "forming_planets")),
|
||
("annual_planet_rows", "Annual Planet Extended Fields", ("planet", "sign", "degree", "longitude", "nakshatra", "pada")),
|
||
("annual_anchor_rows", "Annual Anchor Points", ("name", "label", "value", "sign", "degree", "house")),
|
||
)
|
||
for key, title, preferred_keys in annual_table_specs:
|
||
rows_src = normalized.get(key)
|
||
if not isinstance(rows_src, list):
|
||
continue
|
||
rows_src = [row for row in rows_src if isinstance(row, dict)]
|
||
if not rows_src:
|
||
continue
|
||
keys = [field for field in preferred_keys if any(row.get(field) not in (None, "") for row in rows_src)]
|
||
if not keys:
|
||
keys = [
|
||
field for field in rows_src[0]
|
||
if field not in {"source_path", "execution_status", "confidence_status", "boundary_status", "status", "selection_status"}
|
||
and not isinstance(rows_src[0].get(field), (dict, list, tuple))
|
||
][:6]
|
||
if not keys:
|
||
continue
|
||
lines.extend([f"## {title}", "", table([field.replace("_", " ").title() for field in keys], [[row.get(field) for field in keys] for row in rows_src]), ""])
|
||
monthly_pack = annual_pack.get("monthly_chart_pack") if isinstance(annual_pack.get("monthly_chart_pack"), dict) else {}
|
||
monthly_rows = monthly_pack.get("rows") if isinstance(monthly_pack.get("rows"), list) else []
|
||
if monthly_rows:
|
||
rows = []
|
||
for row in monthly_rows:
|
||
if not isinstance(row, dict):
|
||
continue
|
||
asc = row.get("ascendant") if isinstance(row.get("ascendant"), dict) else {}
|
||
rows.append([row.get("month_index"), row.get("local_datetime"), asc.get("sign"), asc.get("degree_in_sign") or asc.get("degree")])
|
||
lines.extend(["## Monthly Chart Pack", "", table(["Month", "Local time", "Annual ascendant", "Ascendant degree"], rows), ""])
|
||
lifetime_pack = annual_pack.get("lifetime_annual_chart_pages") if isinstance(annual_pack.get("lifetime_annual_chart_pages"), dict) else {}
|
||
lifetime_pages = lifetime_pack.get("pages") if isinstance(lifetime_pack.get("pages"), list) else []
|
||
lifetime_rows = []
|
||
for page in lifetime_pages:
|
||
if not isinstance(page, dict):
|
||
continue
|
||
for row in page.get("rows") or []:
|
||
if not isinstance(row, dict):
|
||
continue
|
||
asc = row.get("ascendant") if isinstance(row.get("ascendant"), dict) else {}
|
||
lifetime_rows.append([row.get("completed_age"), row.get("target_year"), row.get("local_datetime"), asc.get("sign"), asc.get("degree_in_sign") or asc.get("degree")])
|
||
if lifetime_rows:
|
||
lines.extend(["## Lifetime Annual Chart Pages", "", table(["Age", "Year", "Solar return time", "Annual ascendant", "Ascendant degree"], lifetime_rows), ""])
|
||
series_pack = annual_pack.get("annual_chart_series_pack") if isinstance(annual_pack.get("annual_chart_series_pack"), dict) else {}
|
||
for series_key, title in (
|
||
("birth_place_eight_year_overview", "Birth-Place Eight-Year Annual Series"),
|
||
("local_eight_year_overview", "Local Eight-Year Annual Series"),
|
||
):
|
||
series = series_pack.get(series_key) if isinstance(series_pack.get(series_key), dict) else {}
|
||
series_rows = series.get("rows") if isinstance(series.get("rows"), list) else []
|
||
rows = [
|
||
[row.get("target_year"), row.get("age"), row.get("solar_return_local") or row.get("local_datetime"), row.get("annual_ascendant")]
|
||
for row in series_rows
|
||
if isinstance(row, dict)
|
||
]
|
||
if rows:
|
||
lines.extend([f"## {title}", "", table(["Year", "Age", "Solar return time", "Annual ascendant"], rows), ""])
|
||
|
||
sahams = dict_get(("advanced_systems", "sahams"), {})
|
||
if isinstance(sahams, dict):
|
||
rows = []
|
||
for name, item in sahams.items():
|
||
if name in {"daynight_evidence", "formula_traces"}:
|
||
continue
|
||
if isinstance(item, dict):
|
||
rows.append([name, item.get("sign"), item.get("degree_in_sign"), item.get("longitude")])
|
||
lines.extend(["## Sahams", "", table(["Name", "Sign", "Degree in sign", "Longitude"], rows), ""])
|
||
|
||
yoga = dict_get(("advanced_systems", "yoga", "detected_yogas"), [])
|
||
if isinstance(yoga, list):
|
||
rows = []
|
||
for item in yoga:
|
||
if isinstance(item, dict):
|
||
effects = [effect for effect in (clean_effect(e) for e in item.get("effects", [])) if effect]
|
||
effects_text = "; ".join(effects) if effects else "-"
|
||
rows.append([item.get("name"), item.get("category"), item.get("strength"), item.get("combination"), effects_text])
|
||
lines.extend(["## Applicable Yogas", "", table(["Yoga", "Category", "Strength", "Combination", "Effects / notes"], rows), ""])
|
||
|
||
dosha = dict_get(("advanced_systems", "yogas_doshas"), {})
|
||
if isinstance(dosha, dict):
|
||
rows = []
|
||
dosha_labels = {
|
||
"mangal_dosha": "Mangala / Kuja Dosha",
|
||
"kaal_sarp_dosha": "Kaal Sarp Dosha",
|
||
"pitra_dosha": "Pitra Dosha",
|
||
"sade_sati": "Sade Sati",
|
||
}
|
||
for key in ["mangal_dosha", "kaal_sarp_dosha", "pitra_dosha", "sade_sati"]:
|
||
item = dosha.get(key)
|
||
if isinstance(item, dict):
|
||
rows.append([
|
||
dosha_labels.get(key, key),
|
||
item.get("present") or item.get("detected") or item.get("status"),
|
||
item.get("summary") or item.get("phase") or item.get("type"),
|
||
])
|
||
lines.extend(["## Dosha and Transit Summary", "", table(["Topic", "Detected / phase", "Summary"], rows), ""])
|
||
|
||
text = "\n".join(line for line in lines if line is not None)
|
||
# Some legacy yoga payloads already contain duplicated labels. Normalize
|
||
# them after all sections have been assembled, including generated rows.
|
||
text = re.sub(
|
||
r"\b(Mars|Jupiter|Venus|Saturn|Mercury|Sun|Moon|Rahu|Ketu)\1(?=\d|\b)",
|
||
r"\1 ",
|
||
text,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
text = re.sub(r"(?i)(condition)(houses?|signs?|planets?)", r"\1 \2", text)
|
||
text = re.sub(
|
||
r"\b(houses?|signs?|planets?)\s+\1\b",
|
||
r"\1",
|
||
text,
|
||
flags=re.IGNORECASE,
|
||
)
|
||
text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
|
||
return text
|
||
|
||
|
||
|
||
def _render_pl9_ai_density_raw_data_markdown_zh(packet: dict) -> str:
|
||
calc_sudarshana_chakra = _load_sudarshana()
|
||
"""Render Chinese structured appendix from the same payload used by English."""
|
||
|
||
def clean(value: Any) -> str:
|
||
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()
|
||
return _pl9_public_term(text) if text else "-"
|
||
|
||
def table(headers: list[str], rows: list[list[Any]]) -> str:
|
||
if not rows:
|
||
return ""
|
||
out = ["| " + " | ".join(headers) + " |", "|" + "|".join(["---"] * len(headers)) + "|"]
|
||
for row in rows:
|
||
out.append("| " + " | ".join(clean(cell) for cell in row) + " |")
|
||
return "\n".join(out)
|
||
|
||
def dict_get(path: tuple[str, ...], default: Any = None) -> Any:
|
||
obj: Any = packet
|
||
for key in path:
|
||
if not isinstance(obj, dict):
|
||
return default
|
||
obj = obj.get(key)
|
||
return obj if obj is not None else default
|
||
|
||
lines: list[str] = [f"# {_pl9_public_report_title(packet)}", ""]
|
||
asc = dict_get(("core_chart", "ascendant"), {})
|
||
planets = dict_get(("core_chart", "planets"), {})
|
||
if isinstance(asc, dict) and isinstance(planets, dict):
|
||
try:
|
||
asc_lon = asc.get("longitude")
|
||
if asc_lon is None:
|
||
asc_lon = asc.get("lon") or asc.get("degree") or asc.get("degree_raw")
|
||
planet_lons = {
|
||
planet: item.get("longitude") if item.get("longitude") is not None else item.get("degree")
|
||
for planet, item in planets.items()
|
||
if isinstance(item, dict) and (item.get("longitude") is not None or item.get("degree") is not None)
|
||
}
|
||
if asc_lon is not None and planet_lons:
|
||
sudarshana = calc_sudarshana_chakra(planet_lons, float(asc_lon))
|
||
reference_rows = []
|
||
reference_labels = {
|
||
"ascendant_lagna": "出生上升参考点",
|
||
"moon_lagna": "月亮参考点",
|
||
"sun_lagna": "太阳参考点",
|
||
}
|
||
for key, value in (sudarshana.get("reference_points") or {}).items():
|
||
if isinstance(value, dict):
|
||
reference_rows.append([reference_labels.get(key, key), value.get("sign"), value.get("role")])
|
||
if reference_rows:
|
||
lines.extend(["## Sudarshana Chakra(三参考点盘)", "", table(["参考点", "星座", "角色"], reference_rows), ""])
|
||
|
||
chart_rows = []
|
||
chart_labels = {
|
||
"ascendant_lagna": "从出生上升起算",
|
||
"moon_lagna": "从月亮起算",
|
||
"sun_lagna": "从太阳起算",
|
||
}
|
||
for chart_name, chart in (sudarshana.get("three_charts") or {}).items():
|
||
if not isinstance(chart, dict):
|
||
continue
|
||
for planet, item in chart.items():
|
||
if isinstance(item, dict):
|
||
chart_rows.append([chart_labels.get(chart_name, chart_name), planet, item.get("sign"), item.get("house")])
|
||
if chart_rows:
|
||
lines.extend([
|
||
"### Sudarshana Chakra 三参考点落宫表",
|
||
"",
|
||
table(["参考盘", "行星", "星座", "宫位"], chart_rows),
|
||
"",
|
||
])
|
||
except Exception:
|
||
pass
|
||
|
||
text = "\n".join(line for line in lines if line is not None)
|
||
text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
|
||
return text
|
||
|
||
|
||
|
||
|
||
_SITE_BHAVA_SCORE = re.compile(r"\(Benefic\):\s*\+2|\(Malefic\):\s*-1\.5|吉星\s*\+2|凶星\s*-1\.5")
|
||
_NEECHA_RAJA = re.compile(r"Neecha Bhanga Raja Yoga|落陷取消王瑜伽|坏座位消王")
|
||
|
||
|
||
def _apply_local_report_conventions(markdown: str, *, language: str = "zh") -> str:
|
||
"""Keep this site's report wording inside the upstream appendix."""
|
||
if language == "en":
|
||
text = _NEECHA_RAJA.sub(
|
||
"Neecha Bhanga (debilitation cancellation, not a Raja Yoga)",
|
||
markdown,
|
||
)
|
||
text = re.sub(r"\bguaranteed\b", "certain", text, flags=re.IGNORECASE)
|
||
text = re.sub(r"\bguarantee\b", "certain outcome", text, flags=re.IGNORECASE)
|
||
text = re.sub(r"\bdivorce\b", "relationship strain", text, flags=re.IGNORECASE)
|
||
text = re.sub(r"\bmiscarriage\b", "a sensitive health topic", text, flags=re.IGNORECASE)
|
||
text = re.sub(r"\binfertility\b", "a sensitive fertility topic", text, flags=re.IGNORECASE)
|
||
else:
|
||
text = _NEECHA_RAJA.sub("落陷取消(Neecha Bhanga)", markdown)
|
||
text = text.replace("疾病诊断", "身体负荷话题")
|
||
text = text.replace("一定会", "有可能")
|
||
text = re.sub(r"(?<!不是)必然", "可能", text)
|
||
text = text.replace("再婚", "关系变化")
|
||
text = text.replace("几段关系", "关系主题")
|
||
text = text.replace("早夭", "早年体质")
|
||
text = text.replace("疾病", "身体负荷")
|
||
text = text.replace("落陷取消王", "落陷取消")
|
||
text = _SITE_BHAVA_SCORE.sub("", text)
|
||
return text
|
||
|
||
|
||
def _append_native_rashi_dashas(markdown: str, packet: dict) -> str:
|
||
"""Full-data edition only. The parity reader and the consult card do not call this."""
|
||
try:
|
||
from rashi_dasha_report import render_rashi_dasha_chapter
|
||
except ModuleNotFoundError: # pragma: no cover - package import compatibility
|
||
from scripts.rashi_dasha_report import render_rashi_dasha_chapter
|
||
chapter = render_rashi_dasha_chapter(packet if isinstance(packet, dict) else {})
|
||
if not chapter.strip():
|
||
return markdown
|
||
return markdown.rstrip() + "\n\n" + chapter
|
||
|
||
|
||
def _insert_opening_focus(markdown: str, focus: str) -> str:
|
||
"""Put the current-period focus directly under the title."""
|
||
first = next((line.strip() for line in focus.splitlines() if line.strip()), "")
|
||
if not first or first in markdown:
|
||
return markdown
|
||
match = re.search(r"^#\s+.+$", markdown, flags=re.MULTILINE)
|
||
if not match:
|
||
return focus.rstrip() + "\n\n" + markdown
|
||
return markdown[: match.end()] + "\n\n" + focus.rstrip() + "\n" + markdown[match.end() :]
|
||
|
||
|
||
def _interpretation_tools():
|
||
try:
|
||
from pl9_interpretation_packs import attach_full_data_interpretation, opening_focus_block
|
||
except ModuleNotFoundError: # pragma: no cover - package import compatibility
|
||
from scripts.pl9_interpretation_packs import attach_full_data_interpretation, opening_focus_block
|
||
return attach_full_data_interpretation, opening_focus_block
|
||
|
||
|
||
def render_full_data_markdown(packet: dict) -> str:
|
||
"""Upstream `pl9_ai_density` branch at 23be1807, on this site's parity body."""
|
||
attach_full_data_interpretation, opening_focus_block = _interpretation_tools()
|
||
if isinstance(packet, dict):
|
||
attach_full_data_interpretation(packet)
|
||
focus = opening_focus_block(packet)
|
||
else:
|
||
focus = ""
|
||
language = _pl9_report_language(packet if isinstance(packet, dict) else {})
|
||
if language == "en":
|
||
structured_appendix = re.sub(
|
||
r"^#\s+.*?(?:\n{2,}|\n)",
|
||
"",
|
||
_render_pl9_ai_density_raw_data_markdown_en(packet),
|
||
count=1,
|
||
flags=re.DOTALL,
|
||
).strip()
|
||
english_markdown = _append_native_rashi_dashas(
|
||
_insert_opening_focus(render_pl9_parity_markdown(packet), focus),
|
||
packet,
|
||
)
|
||
if structured_appendix:
|
||
english_markdown = (
|
||
english_markdown.rstrip()
|
||
+ "\n\n## Structured Data Appendix\n\n"
|
||
+ "<details>\n<summary>Machine-readable source fields (raw evidence)</summary>\n\n"
|
||
+ structured_appendix
|
||
+ "\n\n</details>\n"
|
||
)
|
||
rendered = _sanitize_pl9_ai_density_markdown_en(
|
||
english_markdown,
|
||
_pl9_english_report_title(packet),
|
||
)
|
||
return _apply_local_report_conventions(rendered, language="en")
|
||
zh_structured_markdown = _sanitize_pl9_ai_density_markdown(
|
||
_render_pl9_ai_density_raw_data_markdown_en(packet),
|
||
_pl9_public_report_title(packet),
|
||
)
|
||
zh_structured_appendix = re.sub(
|
||
r"^#\s+.*?(?:\n{2,}|\n)",
|
||
"",
|
||
zh_structured_markdown,
|
||
count=1,
|
||
flags=re.DOTALL,
|
||
).strip()
|
||
zh_markdown = _append_native_rashi_dashas(
|
||
_insert_opening_focus(render_pl9_parity_markdown(packet), focus),
|
||
packet,
|
||
)
|
||
if zh_structured_appendix and "## 结构化资料附录" not in zh_markdown:
|
||
zh_markdown = (
|
||
zh_markdown.rstrip()
|
||
+ "\n\n## 结构化资料附录\n\n"
|
||
+ "<details>\n<summary>机器可读源字段(原始证据)</summary>\n\n"
|
||
+ zh_structured_appendix
|
||
+ "\n\n</details>\n"
|
||
)
|
||
rendered = _sanitize_pl9_ai_density_markdown(zh_markdown, _pl9_public_report_title(packet))
|
||
return _apply_local_report_conventions(rendered, language="zh")
|
||
|
||
|
||
def read_full_data_domain(packet: Mapping, domain: str) -> dict:
|
||
"""Deep-copied packet view for one consult domain.
|
||
|
||
Field selection belongs to the consult card. This entry only freezes the
|
||
domain name and detaches the copy from the live packet.
|
||
"""
|
||
if domain not in FULL_DATA_DOMAINS:
|
||
raise KeyError(domain)
|
||
if not isinstance(packet, Mapping):
|
||
raise TypeError("packet must be a mapping")
|
||
return copy.deepcopy({
|
||
"domain": domain,
|
||
"label": FULL_DATA_DOMAINS[domain],
|
||
"report_version": packet.get("report_version"),
|
||
"birth_info": packet.get("birth_info"),
|
||
"core_chart": packet.get("core_chart"),
|
||
"calculation_profile": packet.get("calculation_profile"),
|
||
"worksheets": packet.get("worksheets"),
|
||
})
|