feat(report): English reader edition from the same packet; yoga combination English twins (E1/E2/E3 wiring)
- /api/professional_report_reference accepts languages [zh,en]; one packet, zh byte-identical, markdown_en + reader_dasha_applicability_en, or english_unavailable when a Chinese character would leak. - pl9_reader_english renders the parity volume with report_language=en and a fixed term table; yoga_engine carries combination_en / effects_en / strength_en. - 75 fictional charts: zero Han, same line count, same numbers outside yoga tables. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0c06ab1d78
commit
419e1f4d37
@@ -0,0 +1,169 @@
|
||||
"""English edition of the user-facing PL9 reader (TASK-report-english-edition-20260929).
|
||||
|
||||
The Chinese reader is ``pl9_reader_export._render_pl9_user_markdown``: it renders the
|
||||
parity volume and then localises it with ordered string passes. The English
|
||||
edition renders the same parity volume from the same packet with
|
||||
``report_language = 'en'`` (planet and sign tokens stay English, yoga rows use the
|
||||
engine's ``*_en`` twins), then:
|
||||
|
||||
1. drops the same audit-only lines and page references as the Chinese reader;
|
||||
2. replaces the renderer's hard-coded Chinese labels with fixed English text
|
||||
(``pl9_reader_english_terms``);
|
||||
3. replaces engineering vocabulary with English public wording.
|
||||
|
||||
Nothing here computes. A Chinese character that survives is a missing entry,
|
||||
and the caller treats the English edition as unavailable rather than shipping it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
|
||||
try:
|
||||
from scripts.pl9_reader_english_terms import PHRASES_EN, REGEX_EN
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
from pl9_reader_english_terms import PHRASES_EN, REGEX_EN
|
||||
|
||||
HAN = re.compile(r"[㐀-鿿豈-]")
|
||||
|
||||
|
||||
def han_leaks(text: str) -> list[str]:
|
||||
"""Distinct runs of Han characters, for the self-check and for tests."""
|
||||
return sorted(set(re.findall(r"[㐀-鿿豈-]+", text)))
|
||||
|
||||
|
||||
def _english_packet(packet: dict) -> dict:
|
||||
"""Deep copy with the language flag set and yoga rows switched to their English twins."""
|
||||
english = copy.deepcopy(packet)
|
||||
english['report_language'] = 'en'
|
||||
advanced = ((english.get('worksheets') or {}).get('advanced_systems') or {})
|
||||
yoga = advanced.get('yoga') if isinstance(advanced, dict) else None
|
||||
if isinstance(yoga, dict):
|
||||
for key in ('detected_yogas', 'yogas'):
|
||||
rows = yoga.get(key)
|
||||
if not isinstance(rows, list):
|
||||
continue
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if 'combination_en' in row:
|
||||
row['combination'] = row['combination_en']
|
||||
if 'effects_en' in row:
|
||||
row['effects'] = row['effects_en']
|
||||
if 'strength_en' in row:
|
||||
row['strength'] = row['strength_en']
|
||||
row['name_cn'] = row.get('name') or row.get('name_cn')
|
||||
return english
|
||||
|
||||
|
||||
def _public_wording_en(text: str) -> str:
|
||||
"""English counterpart of the Chinese reader's engineering-vocabulary pass."""
|
||||
for raw, public in (
|
||||
("parameter_sensitive / unverified", "to be checked"),
|
||||
("pyjhora_behavior_only / not_multiengine_parity", "reference"),
|
||||
("blocked_and_conflict_fields_cannot_generate_final_predictions", "data only"),
|
||||
("parameter_sensitive", "reference"),
|
||||
("partial_verified", "listed"),
|
||||
("pyjhora_behavior_only", "reference"),
|
||||
("restricted_or_unclosed", "to be checked"),
|
||||
("blocked_external_callable", "external replay pending"),
|
||||
("not_applicable", "not listed"),
|
||||
("missing_in_local", "not listed separately"),
|
||||
("functional_role_not_returned", "not listed separately"),
|
||||
("external_mudda_start_boundary", "reference start"),
|
||||
("dasha_beginning_dates", "start dates"),
|
||||
("dasha_ending_dates", "end dates"),
|
||||
("natural_benefic", "natural benefic"),
|
||||
("natural_malefic", "natural malefic"),
|
||||
("functional_benefic", "functional benefic"),
|
||||
("functional_malefic", "functional malefic"),
|
||||
("functional_neutral", "functional neutral"),
|
||||
("observed_relationships", "formed"),
|
||||
("observed_empty", "not formed"),
|
||||
("external_replay_pending", "pending"),
|
||||
("annual_tajika_pack", "annual structure"),
|
||||
("kp_monthly_report_packet", "monthly focus"),
|
||||
("source_path", "source"),
|
||||
("{planets}", "the planets involved"),
|
||||
("blocked", "not listed"),
|
||||
):
|
||||
text = text.replace(raw, public)
|
||||
text = re.sub(r"\b[a-zA-Z_]+_profile\b", "calculation setting", text)
|
||||
text = re.sub(r"\b[a-zA-Z_]+_status\b", "status", text)
|
||||
text = re.sub(r"\b[a-zA-Z_]+_producer\b", "calculation", text)
|
||||
text = re.sub(r"\b[a-zA-Z_]+_path\b", "source", text)
|
||||
text = re.sub(r"\bproducer\b", "calculation", text, flags=re.I)
|
||||
text = re.sub(r"\bschema\b", "structure", text, flags=re.I)
|
||||
text = re.sub(r"\bprofile\b", "setting", text)
|
||||
text = re.sub(r"\bProfile\b", "Setting", text)
|
||||
text = re.sub(r"\baudit\b", "check", text)
|
||||
return text
|
||||
|
||||
|
||||
def _translate(text: str) -> str:
|
||||
for pattern, replacement in REGEX_EN:
|
||||
text = pattern.sub(replacement, text)
|
||||
for raw, public in PHRASES_EN:
|
||||
text = text.replace(raw, public)
|
||||
return text
|
||||
|
||||
|
||||
def render_pl9_user_markdown_en(packet: dict) -> str:
|
||||
"""Render the English reader volume from the same packet the Chinese one uses."""
|
||||
try:
|
||||
from scripts import pl9_reader_export as reader
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
import pl9_reader_export as reader
|
||||
|
||||
english = _english_packet(packet)
|
||||
text = reader.render_pl9_parity_markdown(english)
|
||||
title = reader._pl9_english_report_title(english)
|
||||
text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE)
|
||||
text = re.sub(r"(PL9 p\d+(?:-p\d+)?[^)]*)", "", text)
|
||||
text = re.sub(r"\s*\(PL9 p\d+(?:-p\d+)?[^)]*\)", "", text)
|
||||
drop_fragments = (
|
||||
"本册只编排原版",
|
||||
"研究性证据",
|
||||
"审计",
|
||||
"producer",
|
||||
"PyJHora",
|
||||
"source_path",
|
||||
"schema",
|
||||
"blocked",
|
||||
"parameter_sensitive",
|
||||
"native producer",
|
||||
"Report Layers",
|
||||
"Worksheet Index",
|
||||
"Report Pack Manifest",
|
||||
"发布状态",
|
||||
"质量检查",
|
||||
"Chart Identity",
|
||||
"不自动进入公开发布",
|
||||
"对标主册",
|
||||
)
|
||||
lines: list[str] = []
|
||||
for raw_line in text.splitlines():
|
||||
if any(fragment in raw_line for fragment in drop_fragments):
|
||||
continue
|
||||
line = re.sub(r"p\d+-p\d+\s*", "", raw_line)
|
||||
line = re.sub(r"p\d+\s*[·.-]\s*", "", line)
|
||||
line = re.sub(r"^#+\s*p\d+\s*", lambda match: match.group(0).replace(match.group(0).split()[-1], ""), line)
|
||||
line = line.replace(" · ", " ")
|
||||
lines.append(line.rstrip())
|
||||
text = "\n".join(lines)
|
||||
text = _translate(text)
|
||||
text = text.replace("PL9 p", "p. ").replace("PL9", "classical Jyotish")
|
||||
text = _public_wording_en(text)
|
||||
text = re.sub(r"(?m)^(#{2,6}\s*)p\d+(?:-p\d+)?\s*[·.-]?\s*", r"\1", text)
|
||||
text = re.sub(r"(?m)^(#{1,6})\s+/\s+", r"\1 ", text)
|
||||
text = re.sub(r"(?m)^(#{1,6}) {2,}", r"\1 ", text)
|
||||
text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE)
|
||||
# Full-width punctuation from the shared renderer reads badly in English.
|
||||
for wide, ascii_form in ((";", "; "), (",", ", "), (":", ": "), ("、", ", "), ("(", " ("), (")", ")"), ("—", " - ")):
|
||||
text = text.replace(wide, ascii_form)
|
||||
text = re.sub(r"(?<=\S) {2,}(?=\S)", " ", text)
|
||||
text = re.sub(r"\( ", "(", text)
|
||||
text = re.sub(r"[ \t]+\n", "\n", text)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
|
||||
return text
|
||||
@@ -0,0 +1,244 @@
|
||||
"""Fixed English text for the Chinese labels in the PL9 reader volume.
|
||||
|
||||
Two sources of Chinese reach the reader: labels hard-coded in
|
||||
``pl9_reader_export.render_pl9_parity_markdown`` and short texts the engine
|
||||
modules write in Chinese (dosha summaries, Jaimini karaka roles, method notes,
|
||||
aspect names). Both are listed here. Templated engine texts are matched with
|
||||
regular expressions; everything else is an exact phrase.
|
||||
|
||||
Rules for entries: no Chinese in a value, and every number in the Chinese text
|
||||
appears in the English text in the same order, so the two editions carry the
|
||||
same numbers (tests/test_report_english_edition.py checks both).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_PHRASES: dict[str, str] = {
|
||||
# ── chapter and section headings ──
|
||||
"## 出生资料与图盘": "## Birth Data and Charts",
|
||||
"## 行星、星宿与宫位解释": "## Planets, Nakshatras and Houses",
|
||||
"## 强度、关系与 Ashtakavarga 技术页": "## Strength, Relationships and Ashtakavarga",
|
||||
"## 标准 Dasha 表格页": "## Standard Dasha Tables",
|
||||
"## 原版 Dasha 日期表(同案 PL9 数据)": "## Original Dasha Date Tables (same chart)",
|
||||
"## 原版 Dasha 日期表": "## Original Dasha Date Tables",
|
||||
"## Saturn 与 KP 支持页": "## Saturn and KP",
|
||||
"## 年度图盘与 Tajika": "## Annual Charts and Tajika",
|
||||
"## Bhavesh、Dosha 与 Yoga 解释页": "## Bhavesh, Dosha and Yoga",
|
||||
"## 大运解释": "## Dasha Interpretation",
|
||||
"出生资料补充": "Birth Particulars (supplement)",
|
||||
"Birth Chart 行星原始位置": "Birth Chart Planetary Positions",
|
||||
"行星主星与状态": "Planet Lords and Dignity",
|
||||
"Sudarshan Chakra 资料": "Sudarshan Chakra",
|
||||
"Upagraha / Sub-Planets 点位": "Upagraha / Sub-Planet Positions",
|
||||
"特殊上升与派生点补充": "Special Lagnas and Points (supplement)",
|
||||
"敏感点与特殊落点": "Sensitive Points and Special Placements",
|
||||
"本命与分盘北印度图盘": "Natal and Divisional Charts (North Indian)",
|
||||
" 数值位置表": " Position Table",
|
||||
"Shadbala / Bhava Bala 摘要": "Shadbala / Bhava Bala Summary",
|
||||
"Avastha 行星状态": "Avastha Planetary States",
|
||||
"Bhava Bala 十二宫力量": "Bhava Bala Strength of the Twelve Houses",
|
||||
"Planetary Friendship 矩阵源数据": "Planetary Friendship Matrix",
|
||||
"相位矩阵": "Aspect Matrix",
|
||||
"Jaimini Karaka、Arudha Pada 与特殊 Lagna": "Jaimini Karakas, Arudha Padas and Special Lagnas",
|
||||
"Ashtakavarga 完整宫位分数": "Ashtakavarga Full House Scores",
|
||||
"Vimsopaka / Pushkara / Vargottama 辅助强度": "Vimsopaka / Pushkara / Vargottama Supporting Strength",
|
||||
"Ashtakavarga Pinda 与 Shodhya/Yoga Pinda 数值表。": "Ashtakavarga Pinda and Shodhya / Yoga Pinda values.",
|
||||
"Ashtakavarga Shodhya / Yoga Pinda 原始值": "Ashtakavarga Shodhya / Yoga Pinda Raw Values",
|
||||
"Chakra 图阵页": "Chakra Diagrams",
|
||||
"以下表格保留同一命盘原版报告中的完整可见日期和星座边界,便于逐项读取与校对。":
|
||||
"The tables below keep every visible date and sign boundary from the original report for this chart, for line-by-line reading and checking.",
|
||||
"KP 原版细格": "KP Original Detail Grid",
|
||||
"Varshaphala 基础资料与年度行星表": "Varshaphala Basics and Annual Planet Table",
|
||||
"Varshaphala 年度结果解释": "Varshaphala Annual Results",
|
||||
"Annual Dasha 结果解释": "Annual Dasha Results",
|
||||
"Mudda / Patyayini 年度 Dasha 表": "Mudda / Patyayini Annual Dasha Table",
|
||||
"Patyayini / Patyamsha ending-date 表": "Patyayini / Patyamsha Ending-Date Table",
|
||||
"Tajika Yoga Presence 对照表": "Tajika Yoga Presence Comparison",
|
||||
"Tajika Yoga Presence 表": "Tajika Yoga Presence Table",
|
||||
"Sahams 点位表": "Saham Positions",
|
||||
"公开 Panchavargiya 数值复核边界": "Public Panchavargiya Value Check",
|
||||
"年度特殊点": "Annual Special Points",
|
||||
"Tripataki Chakra 年盘落点": "Tripataki Chakra Annual Placements",
|
||||
"年度月返照图盘数据": "Monthly Returns within the Year",
|
||||
"生命周期年度返照图盘数据": "Lifetime Annual Return Charts",
|
||||
"出生地八年年度总览": "Eight-Year Overview (birthplace)",
|
||||
"现居地八年年度总览": "Eight-Year Overview (current residence)",
|
||||
"年度北印度图盘": "Annual Charts (North Indian)",
|
||||
" 年度 Varshaphala / Tajika 细项": " Annual Varshaphala / Tajika Details",
|
||||
"年度行星资料": "Annual Planet Data",
|
||||
"Annual Dasha 阶段表": "Annual Dasha Periods",
|
||||
"年度行星扩展字段": "Annual Planet Extras",
|
||||
"年度锚点": "Annual Anchors",
|
||||
"年度行星资料": "Annual Planet Data",
|
||||
"年度结果": "Annual Results",
|
||||
"宫主(Bhavesh)落宫解释": "House Lords (Bhavesh) by Placement",
|
||||
"Bhavesh 宫主逐宫解释": "Bhavesh House by House",
|
||||
"Dosha 逐项事实与解释": "Dosha Facts and Notes",
|
||||
"传统辅助建议": "Traditional Supportive Practices",
|
||||
# ── table headers ──
|
||||
"| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |":
|
||||
"| Planet | Sign lord | Star lord | Sub lord | Sub-sub lord | Dignity | Strength ratio |",
|
||||
"星座主、星宿主、分主、分分主:这颗行星所在星座、星宿与 KP 细分各自的主星。力量比:六维力量与所需力量之比,1 以上为达标。":
|
||||
"Sign lord, star lord, sub lord and sub-sub lord: the rulers of the planet's sign, nakshatra and KP subdivisions. Strength ratio: Shadbala against the required strength; 1 or above meets it.",
|
||||
"| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |": "| Body | Sign | Degree in sign | Longitude | Whole-sign house |",
|
||||
"| House | Sign | Lord | Score | 关键因素 |": "| House | Sign | Lord | Score | Key factors |",
|
||||
"| House | Sign | SAV | BAV/承接 | Full with Lagna |": "| House | Sign | SAV | BAV / carry | Full with Lagna |",
|
||||
"| 序号 | 主运 | 起始 | 结束 | 年数 |": "| No. | Mahadasha | Start | End | Years |",
|
||||
"| Vimshottari AD | 主运 | 子运 | 起始 | 结束 |": "| Vimshottari AD | Mahadasha | Antardasha | Start | End |",
|
||||
"| Vimshottari PD | 主运 | 子运 | 次子运 | 起始 | 结束 |": "| Vimshottari PD | Mahadasha | Antardasha | Pratyantardasha | Start | End |",
|
||||
"| 序号 | 主星/星座 | 起始 | 结束 | 年数 |": "| No. | Lord / sign | Start | End | Years |",
|
||||
"| AD/child | 主层 | 子层 | 起始 | 结束 |": "| AD/child | Main period | Sub-period | Start | End |",
|
||||
"| 主运 | 起始年龄 | 子段 | 起始 | 结束 |": "| Mahadasha | Starting age | Sub-period | Start | End |",
|
||||
"| 项目 | 锚点 | 年度主题 |": "| Item | Anchor | Theme of the year |",
|
||||
"| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |": "| Period | Lord | Annual house | Date range | Theme |",
|
||||
"| balance profile | 主星 | 开始 | 结束 | 边界口径 |": "| Balance | Lord | Start | End | Boundary basis |",
|
||||
"| 主层 | 子层 | 边界 | 边界语义 |": "| Main period | Sub-period | Boundary | Boundary meaning |",
|
||||
"| Yoga | 结果 | 形成行星 | 相位运动(入相/分离) |": "| Yoga | Result | Forming planets | Motion (applying / separating) |",
|
||||
"| Saham | 星座 | 星座内度数 |": "| Saham | Sign | Degree in sign |",
|
||||
"| 项目 | 结果 |": "| Item | Result |",
|
||||
"| 月份 | 本地时刻 | 年盘上升 | 上升度数 | 行星黄经(度) |": "| Month | Local time | Annual ascendant | Ascendant degree | Planet longitudes (deg) |",
|
||||
"| 年龄 | 年份 | 年返时刻 | 上升 | 上升度数 | 行星黄经(度) |": "| Age | Year | Return time | Ascendant | Ascendant degree | Planet longitudes (deg) |",
|
||||
"| 年份 | 年龄 | 年返时刻 | 年盘上升 |": "| Year | Age | Return time | Annual ascendant |",
|
||||
"| 项目 | 数值 |": "| Item | Value |",
|
||||
"| 行星 | 星座 | 落座度数 | 黄经 |": "| Planet | Sign | Degree in sign | Longitude |",
|
||||
"| 点位 | 星座 | 落座度数 | 黄经 | 守护星 |": "| Point | Sign | Degree in sign | Longitude | Ruler |",
|
||||
"| 月份 | 本地时刻 | 年盘上升 | 上升度数 |": "| Month | Local time | Annual ascendant | Ascendant degree |",
|
||||
"| 对象 | 建议 | 宝石/次数 |": "| For | Suggestion | Gem / count |",
|
||||
"| 公开复核状态 |": "| Public check status |",
|
||||
"| 覆盖行星 |": "| Planets covered |",
|
||||
"| 已匹配分量 |": "| Components matched |",
|
||||
"| 已匹配总分 |": "| Totals matched |",
|
||||
"| 可直接用于公式升级 |": "| Usable for formula upgrade |",
|
||||
"| 年返上升 |": "| Annual ascendant |",
|
||||
# ── labelled lines ──
|
||||
"- 姓名:": "- Name: ",
|
||||
"- 出生:": "- Born: ",
|
||||
"- 计算口径:": "- Calculation settings: ",
|
||||
"- Shadbala 方法:": "- Shadbala method: ",
|
||||
"- 外部边界:": "- Method note: ",
|
||||
"- 方法:": "- Method: ",
|
||||
"- BAV 校验:": "- BAV check: ",
|
||||
"- 当前阶段:": "- Current phase: ",
|
||||
"- 是否 active:": "- Active: ",
|
||||
"- 摘要:": "- Summary: ",
|
||||
"- 年返上升:": "- Annual ascendant: ",
|
||||
# ── birth-parameter row labels ──
|
||||
"| 出生日期 |": "| Birth date |",
|
||||
"| 出生时间 |": "| Birth time |",
|
||||
"| 出生星期 |": "| Weekday |",
|
||||
"| 出生地点 |": "| Birthplace |",
|
||||
"| 城市 |": "| City |",
|
||||
"| 省州 |": "| State / province |",
|
||||
"| 国家 |": "| Country |",
|
||||
"| 纬度 |": "| Latitude |",
|
||||
"| 经度 |": "| Longitude |",
|
||||
"| 时区 |": "| Time zone |",
|
||||
"| 岁差名称 |": "| Ayanamsa name |",
|
||||
"| 岁差值 |": "| Ayanamsa value |",
|
||||
"| 岁差 |": "| Ayanamsa |",
|
||||
"| 交点模式 |": "| Node mode |",
|
||||
"| 宫位系统 |": "| House system |",
|
||||
"| 综合性质 |": "| Overall quality |",
|
||||
"| 吉性要素数量 |": "| Auspicious elements |",
|
||||
"| 要素总数 |": "| Total elements |",
|
||||
# ── annual labels and values ──
|
||||
"Muntha 所在星座的守护星(当前本地口径)": "Ruler of the Muntha sign (current local method)",
|
||||
"两种计算口径给出的年主不同,暂不合并": "The two methods give different year lords; not merged",
|
||||
"(两者不同)": " (they differ)",
|
||||
"太阳返照": "Solar return",
|
||||
"另一口径": "Other method",
|
||||
"入相": "applying",
|
||||
"分离": "separating",
|
||||
# ── public status words (renderer helpers) ──
|
||||
"由生命周期图盘页承接": "carried by the lifetime chart pages",
|
||||
"外部回放待校对": "external replay pending",
|
||||
"外部行为参考": "reference rule",
|
||||
"参数敏感": "parameter-sensitive",
|
||||
"需补资料": "needs more data",
|
||||
"已生成": "generated",
|
||||
"已核验": "verified",
|
||||
"需校对": "to be checked",
|
||||
"不适用": "not applicable",
|
||||
"已形成": "formed",
|
||||
"未形成": "not formed",
|
||||
"待补充": "pending",
|
||||
"证据口径": "Evidence basis",
|
||||
"证据标签": "Evidence basis",
|
||||
"源数据未提供宝石或次数": "No gem or repetition count supplied",
|
||||
"一致": "match",
|
||||
"参考": "reference",
|
||||
"未列": "not listed",
|
||||
# ── engine texts (fixed) ──
|
||||
"Shadbala六重力量(absolute Virupas; precise Sthana/Dig/Kala/Drik; bounded BPHS Chesta)":
|
||||
"Shadbala six-fold strength, in Virupas, combining Sthana, Dig, Kala, Drik and BPHS Chesta",
|
||||
"Chesta remains cross-engine method-conflicted; totals inherit that boundary.":
|
||||
"Chesta Bala depends on the calculation method, so compare totals within the same method.",
|
||||
"Shadbala六重力量": "Shadbala six-fold strength",
|
||||
"Ashtakavarga八分法(BPHS/PVR书例校准v2.1)": "Ashtakavarga eight-fold method (calibrated to BPHS / PVR book examples, v2.1)",
|
||||
"吉(Subha)": "Auspicious (Subha)",
|
||||
"中(Mixed)": "Mixed",
|
||||
"凶(Asubha)": "Inauspicious (Asubha)",
|
||||
"无Kaal Sarp Dosha": "No Kaal Sarp Dosha",
|
||||
"无Mangal Dosha(Mars不在煞宫)": "No Mangal Dosha (Mars is not in a dosha house)",
|
||||
"无Pitra Dosha明显迹象": "No clear sign of Pitra Dosha",
|
||||
"Rahu位置未知,无法判断Kaal Sarp Dosha": "Rahu's position is unknown; Kaal Sarp Dosha cannot be judged",
|
||||
"Sun位置未知,无法判断Pitra Dosha": "The Sun's position is unknown; Pitra Dosha cannot be judged",
|
||||
"Moon星座未知,无法判断Sade Sati": "The Moon's sign is unknown; Sade Sati cannot be judged",
|
||||
"Mars在自身星座,部分缓和": "Mars in its own sign, partly mitigated",
|
||||
"Mars擢升,完全解除": "Mars exalted, fully cancelled",
|
||||
# Jaimini karakas (scripts/jaimini.py KARAKA_CN / KARAKA_DOMAINS)
|
||||
"灵魂星AK": "Atmakaraka AK",
|
||||
"事业星AmK": "Amatyakaraka AmK",
|
||||
"兄弟星BK": "Bhratrikaraka BK",
|
||||
"母亲星MK": "Matrikaraka MK",
|
||||
"子女星PK": "Putrakaraka PK",
|
||||
"障碍星GK": "Gnatikaraka GK",
|
||||
"配偶星DK": "Darakaraka DK",
|
||||
"父亲星PiK": "Pitrukaraka PiK",
|
||||
"灵魂使命、核心自我、人生最高目标": "Soul purpose, core self, highest aim in life",
|
||||
"事业方向、主要谋士、权力代理": "Career direction, chief adviser, agent of power",
|
||||
"兄弟姐妹、勇气、冒险精神": "Siblings, courage, spirit of adventure",
|
||||
"母亲、家庭根基、情感安全感": "Mother, family roots, emotional security",
|
||||
"子女、创造力、学生、智能成果": "Children, creativity, students, intellectual output",
|
||||
"竞争对手、疾病、障碍、转化力量": "Rivals, illness, obstacles, transforming force",
|
||||
"配偶特质、伴侣关系、婚姻质量": "Spouse's nature, partnership, quality of marriage",
|
||||
"父亲、祖先业力、传统传承": "Father, ancestral karma, tradition",
|
||||
# planets and signs, if any Chinese form slipped through a table cell
|
||||
"北交点/罗睺": "Rahu",
|
||||
"南交点/计都": "Ketu",
|
||||
"北交点": "Rahu",
|
||||
"南交点": "Ketu",
|
||||
}
|
||||
|
||||
PHRASES_EN: list[tuple[str, str]] = sorted(_PHRASES.items(), key=lambda item: len(item[0]), reverse=True)
|
||||
|
||||
# Templated engine texts. Applied before the phrase table.
|
||||
REGEX_EN: list[tuple[re.Pattern[str], str]] = [
|
||||
# scripts/pl9_reader_export.py: f"第{house}宫主:{lord}"
|
||||
(re.compile(r"第(\d+)宫主:"), r"Lord of house \1: "),
|
||||
# scripts/aspects.py / jyotish_engine.py: f"{n}宫相位"
|
||||
(re.compile(r"(\d+)宫相位"), r"\1th-house aspect"),
|
||||
# scripts/yogas_doshas.py Mangal Dosha summary
|
||||
(re.compile(r"(无|有)Mangal Dosha——Mars在(\d+)宫([A-Za-z]+),严重程度:([A-Za-z_]+)"),
|
||||
lambda m: f"{'No' if m.group(1) == '无' else 'Has'} Mangal Dosha: Mars in house {m.group(2)} {m.group(3)}, severity: {m.group(4)}"),
|
||||
# Kaal Sarp summaries
|
||||
(re.compile(r"完全Kaal Sarp Dosha——所有(\d+)颗行星在Rahu\((\d+)\)—Ketu轴线之间"),
|
||||
r"Full Kaal Sarp Dosha: all \1 planets between the Rahu (\2)–Ketu axis"),
|
||||
(re.compile(r"部分Kaal Sarp Dosha——(\d+)/(\d+)颗行星在轴线之间"),
|
||||
r"Partial Kaal Sarp Dosha: \1/\2 planets between the axis"),
|
||||
# Pitra Dosha indicators
|
||||
(re.compile(r"([A-Za-z]+)\((\d+)宫\)克Sun\((\d+)宫\)"), r"\1 (house \2) afflicts the Sun (house \3)"),
|
||||
(re.compile(r"Sun\((\d+)宫\)与Rahu\((\d+)宫\)合相/相邻"), r"Sun (house \1) conjunct or next to Rahu (house \2)"),
|
||||
(re.compile(r"有Pitra Dosha迹象:"), "Signs of Pitra Dosha: "),
|
||||
# Sade Sati framework summary
|
||||
(re.compile(r"Sade Sati阶段框架:上升期=([A-Za-z]+),高峰期=([A-Za-z]+)(最困难),下降期=([A-Za-z]+);需结合Saturn实际位置判断是否在进行中"),
|
||||
r"Sade Sati phases: rising = \1, peak = \2 (hardest), setting = \3; whether it is under way depends on Saturn's actual position"),
|
||||
# Yoga strength words left over when a rule has no strength_en: whole table cells only.
|
||||
(re.compile(r"(?<=\| )中强(?= \|)"), "Moderately strong"),
|
||||
(re.compile(r"(?<=\| )中弱(?= \|)"), "Moderately weak"),
|
||||
(re.compile(r"(?<=\| )强(?= \|)"), "Strong"),
|
||||
(re.compile(r"(?<=\| )中(?= \|)"), "Moderate"),
|
||||
(re.compile(r"(?<=\| )弱(?= \|)"), "Weak"),
|
||||
]
|
||||
@@ -57,6 +57,12 @@ def _pl9_export_markdown_for_edition(packet: dict, edition: str) -> str:
|
||||
_bind_engine_symbols()
|
||||
normalized = str(edition or "").strip()
|
||||
if normalized == "reader_main":
|
||||
if _pl9_report_language(packet) == 'en':
|
||||
try:
|
||||
from scripts.pl9_reader_english import render_pl9_user_markdown_en
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
from pl9_reader_english import render_pl9_user_markdown_en
|
||||
return render_pl9_user_markdown_en(packet)
|
||||
return _render_pl9_user_markdown(packet)
|
||||
if normalized in {"pl9_parity", "pl9_fixed_204"}:
|
||||
return render_pl9_parity_markdown(packet)
|
||||
@@ -83,15 +89,18 @@ def _pl9_english_report_title(packet: dict) -> str:
|
||||
return f"{name}'s Vedic Astrology Chart Report" if name else "Vedic Astrology Chart Report"
|
||||
|
||||
|
||||
def _dignity_label(value) -> str:
|
||||
def _dignity_label(value, language: str = 'zh') -> str:
|
||||
"""Engine dignity labels are bilingual (``入旺(Exalted)``); the reader shows one name.
|
||||
|
||||
The glossary pass would otherwise translate the English half too and print
|
||||
``入旺(入旺)``. Anything not in that shape passes through unchanged.
|
||||
``入旺(入旺)``. Anything not in that shape passes through unchanged. The
|
||||
English edition takes the English half.
|
||||
"""
|
||||
text = str(value or '').strip()
|
||||
match = re.fullmatch(r'([^()()]+?)\s*[((][A-Za-z][A-Za-z \-]*[))]', text)
|
||||
return match.group(1).strip() if match else text
|
||||
match = re.fullmatch(r'([^()()]+?)\s*[((]([A-Za-z][A-Za-z \-]*)[))]', text)
|
||||
if not match:
|
||||
return text
|
||||
return match.group(2).strip() if language == 'en' else match.group(1).strip()
|
||||
|
||||
|
||||
def _pl9_report_language(packet: dict) -> str:
|
||||
@@ -459,6 +468,7 @@ def _render_pl9_user_markdown(packet: dict) -> str:
|
||||
def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
"""Render only PL9 page-family material admitted to the parity main volume."""
|
||||
_bind_engine_symbols()
|
||||
_lang = _pl9_report_language(packet)
|
||||
def _unwrap(raw_value):
|
||||
if isinstance(raw_value, dict) and 'result' in raw_value and 'response_envelope' in raw_value:
|
||||
return raw_value.get('result')
|
||||
@@ -538,6 +548,9 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
}
|
||||
sign_map = globals().get('SIGNS_CN', {})
|
||||
raw = str(value or '').strip()
|
||||
if _lang == 'en':
|
||||
return raw or '-'
|
||||
|
||||
return label_map.get(raw) or sign_map.get(raw) or raw or '-'
|
||||
|
||||
def _annual_field_text(field, *keys) -> str:
|
||||
@@ -804,7 +817,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
lines.append(
|
||||
f"| {_cell(_token(planet))} | {_cell(_token(row.get('rasi_lord')))} | "
|
||||
f"{_cell(_token(row.get('nakshatra_lord')))} | {_cell(_token(row.get('sub_lord')))} | "
|
||||
f"{_cell(_token(row.get('sub_sub_lord')))} | {_cell(_dignity_label(row.get('status')))} | {_cell(_num(row.get('sb'), 2))} |"
|
||||
f"{_cell(_token(row.get('sub_sub_lord')))} | {_cell(_dignity_label(row.get('status'), _lang))} | {_cell(_num(row.get('sb'), 2))} |"
|
||||
)
|
||||
lines.extend([
|
||||
'',
|
||||
@@ -2367,7 +2380,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
placement = f"{_token(lord_row.get('sign'))} H{lord_row.get('house') or '-'} {_degree(lord_row.get('degree_in_sign'))}"
|
||||
lines.append(
|
||||
f"| {index} | {_cell(_token(house.get('cusp_sign') or house.get('sign')))} | {_cell(_token(lord))} | "
|
||||
f"{_cell(placement)} | {_cell(_dignity_label(lord_row.get('status') or lord_row.get('dignity')))} | {_cell('active' if str(lord) in active_lords else 'background')} |"
|
||||
f"{_cell(placement)} | {_cell(_dignity_label(lord_row.get('status') or lord_row.get('dignity'), _lang))} | {_cell('active' if str(lord) in active_lords else 'background')} |"
|
||||
)
|
||||
lines.append('')
|
||||
bhavesh_pages = _list(bhavesh_pack.get('pages'))
|
||||
@@ -2556,4 +2569,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
continue
|
||||
seen_blocks.add(value)
|
||||
lines.extend([_public_report_text(value), ''])
|
||||
if _lang == 'en':
|
||||
# The English edition does its own public-wording pass (pl9_reader_english).
|
||||
return '\n'.join(lines)
|
||||
return _sanitize_pl9_public_markdown('\n'.join(lines))
|
||||
|
||||
@@ -7,6 +7,7 @@ calculation, and delegates packet assembly/rendering to ``jyotish_engine``.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
from types import SimpleNamespace
|
||||
@@ -77,6 +78,54 @@ def _normalize_packs(value: Any) -> list[str]:
|
||||
return selected
|
||||
|
||||
|
||||
def _normalize_languages(value: Any) -> list[str]:
|
||||
"""``languages`` is optional; only ``zh`` and ``en`` exist. Absent means today's zh-only reply."""
|
||||
if value is None:
|
||||
return []
|
||||
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
||||
raise ProfessionalReportReferenceInputError("languages must be an array of strings")
|
||||
selected = []
|
||||
for item in value:
|
||||
language = item.strip().lower()
|
||||
if language not in {"zh", "en"}:
|
||||
raise ProfessionalReportReferenceInputError("languages may contain only zh and en")
|
||||
if language not in selected:
|
||||
selected.append(language)
|
||||
return selected
|
||||
|
||||
|
||||
def _english_edition(packet: dict, include_fact_tables: bool) -> dict[str, Any]:
|
||||
"""Render the English reader from a deep copy of the same packet.
|
||||
|
||||
The English edition is secondary: any failure, or any Chinese character left
|
||||
in it, drops it and reports why. The Chinese edition is never touched.
|
||||
"""
|
||||
try:
|
||||
from scripts.pl9_reader_english import HAN, _public_wording_en
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path
|
||||
from pl9_reader_english import HAN, _public_wording_en
|
||||
from pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
try:
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
markdown = _pl9_export_markdown_for_edition(english, READER_MAIN_EDITION)
|
||||
applicability = (
|
||||
_public_wording_en(render_dasha_applicability(copy.deepcopy(packet), language="en"))
|
||||
if include_fact_tables else None
|
||||
)
|
||||
except Exception: # noqa: BLE001 - the English edition must never fail the Chinese one
|
||||
return {"english_unavailable": "render_error"}
|
||||
if not isinstance(markdown, str) or not markdown.strip():
|
||||
return {"english_unavailable": "empty"}
|
||||
if HAN.search(markdown) or (applicability is not None and HAN.search(applicability)):
|
||||
return {"english_unavailable": "han_leak"}
|
||||
return {
|
||||
"markdown_en": markdown,
|
||||
**({"reader_dasha_applicability_en": applicability} if applicability is not None else {}),
|
||||
}
|
||||
|
||||
|
||||
def _load_engine():
|
||||
try:
|
||||
return import_module("scripts.jyotish_engine")
|
||||
@@ -120,6 +169,9 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
|
||||
output_format = _normalize_format(body.get("format"))
|
||||
edition = _normalize_edition(body.get("edition"))
|
||||
packs = _normalize_packs(body.get("packs"))
|
||||
languages = _normalize_languages(body.get("languages"))
|
||||
if languages and (output_format != "markdown" or edition != READER_MAIN_EDITION):
|
||||
raise ProfessionalReportReferenceInputError("languages requires format markdown and edition reader_main")
|
||||
|
||||
birth = handler._high_rigor_birth_payload(body)
|
||||
full_reading = handler._compute_full_reading_for_thematic(birth)
|
||||
@@ -142,6 +194,9 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path
|
||||
from pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
# English renders from its own deep copy first, so nothing the Chinese
|
||||
# renderer does to the packet can reach it (and vice versa).
|
||||
english = _english_edition(packet, body.get("include_fact_tables") is True) if "en" in languages else {}
|
||||
markdown = _pl9_export_markdown_for_edition(packet, READER_MAIN_EDITION)
|
||||
else:
|
||||
markdown = resolved_engine.render_pl9_markdown(packet)
|
||||
@@ -153,6 +208,7 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
|
||||
**({"fact_table_packet": report_density_packet(packet),
|
||||
"reader_dasha_applicability": clean_reader_appendix_markdown(render_dasha_applicability(packet))}
|
||||
if body.get("include_fact_tables") is True else {}),
|
||||
**(english if edition == READER_MAIN_EDITION and "en" in languages else {}),
|
||||
}
|
||||
return {
|
||||
"format": "json",
|
||||
|
||||
+132
-47
@@ -78,6 +78,81 @@ HOUSE_STRENGTHS = {
|
||||
}
|
||||
|
||||
|
||||
# ── English twin for combination text (TASK-report-english-edition-20260929) ──
|
||||
# The Chinese combination strings stay exactly as before; each one also carries
|
||||
# an English rendering so the reader's English edition never has to translate
|
||||
# Chinese back. `BiText` compares, hashes and formats as its Chinese text.
|
||||
class BiText(str):
|
||||
"""A Chinese string that also carries its English twin in ``.en``."""
|
||||
|
||||
en: str
|
||||
|
||||
def __new__(cls, zh, en=None):
|
||||
obj = super().__new__(cls, zh)
|
||||
obj.en = str(zh) if en is None else str(en)
|
||||
return obj
|
||||
|
||||
|
||||
def _en_text(value) -> str:
|
||||
return value.en if isinstance(value, BiText) else str(value)
|
||||
|
||||
|
||||
def _planet(p) -> BiText:
|
||||
return BiText(PLANET_CN.get(p, p), p)
|
||||
|
||||
|
||||
def _planets(ps) -> BiText:
|
||||
ps = list(ps)
|
||||
return BiText("、".join(PLANET_CN.get(p, p) for p in ps), ", ".join(str(p) for p in ps))
|
||||
|
||||
|
||||
def _bijoin(sep: str, parts) -> BiText:
|
||||
kept = [part for part in parts if part]
|
||||
return BiText(sep.join(str(part) for part in kept), sep.join(_en_text(part) for part in kept))
|
||||
|
||||
|
||||
DEFAULT_COMBO_EN = {
|
||||
"{a}与{b}同在第{h}宫": "{a} and {b} together in house {h}",
|
||||
"{p}在第{h}宫": "{p} in house {h}",
|
||||
"{p}在第{h}宫(从{fp}的{rel})": "{p} in house {h} ({rel} from {fp})",
|
||||
"{p}{dignity}在{sign}": "{p} {dignity} in {sign}",
|
||||
"{count}颗行星满足条件": "{count} planets meet the condition",
|
||||
"行星分布在{count}个宫位": "Planets spread over {count} houses",
|
||||
"第{target}宫被夹击": "House {target} hemmed in",
|
||||
"所有指定行星均落入{houses}": "All listed planets fall in houses {houses}",
|
||||
"所有指定行星均落入候选宫位集合{houses}": "All listed planets fall in the house set {houses}",
|
||||
"指定行星只占据{houses}": "Listed planets occupy only houses {houses}",
|
||||
"指定行星只占据候选宫位集合{houses}": "Listed planets occupy only the house set {houses}",
|
||||
"第{house}宫有{planets}": "House {house} holds {planets}",
|
||||
"{planets}在{fp}的第{rel}宫": "{planets} in house {rel} from {fp}",
|
||||
"{count}个目标宫位有指定行星": "{count} target houses hold listed planets",
|
||||
"{a}与{b}在{sign}内相距{gap:.2f}°": "{a} and {b} within {gap:.2f}° in {sign}",
|
||||
"{la}与{lb}互换星座": "{la} and {lb} exchange signs",
|
||||
"{house}宫主{lord}在第{lh}宫": "Lord of house {house}, {lord}, in house {lh}",
|
||||
"{la}与{lb}同在第{h}宫": "{la} and {lb} together in house {h}",
|
||||
"自定义条件满足": "Custom condition met",
|
||||
}
|
||||
|
||||
OFFSET_NAME_EN = {0: "same house", 1: "2nd", 2: "3rd", 3: "4th (Kendra)", 4: "5th (Trine)",
|
||||
5: "6th", 6: "7th (Kendra)", 7: "8th", 8: "9th (Trine)",
|
||||
9: "10th (Kendra)", 10: "11th", 11: "12th"}
|
||||
|
||||
STRENGTH_EN = {"强": "Strong", "中": "Moderate", "弱": "Weak", "待评估": "To be assessed", "观察": "Watch"}
|
||||
|
||||
|
||||
def _combo(cond: Dict, default_zh: str, **values) -> BiText:
|
||||
"""Format the rule's combination template in Chinese (unchanged) and in English."""
|
||||
zh = cond.get("combo_template", default_zh).format(**values)
|
||||
en_template = cond.get("combo_template_en") if "combo_template" in cond else DEFAULT_COMBO_EN.get(default_zh)
|
||||
en = None
|
||||
if isinstance(en_template, str):
|
||||
try:
|
||||
en = en_template.format(**{key: (value.en if isinstance(value, BiText) else value) for key, value in values.items()})
|
||||
except (KeyError, IndexError, ValueError):
|
||||
en = None
|
||||
return BiText(zh, en)
|
||||
|
||||
|
||||
def _get_dignity_level(planet: str, sign: str) -> str:
|
||||
"""判断行星在某星座的尊严等级"""
|
||||
if EXALTATION.get(planet) == sign:
|
||||
@@ -658,16 +733,23 @@ class YogaEngine:
|
||||
for rule in self.rules:
|
||||
matches = self._eval_rule(rule, ctx)
|
||||
for m in matches:
|
||||
combination = m.get("combination", "")
|
||||
strength = m.get("strength", rule.get("strength", "中"))
|
||||
entry = {
|
||||
"name": rule["name"],
|
||||
"name_cn": rule["name_cn"],
|
||||
"combination": m.get("combination", ""),
|
||||
"combination": str(combination),
|
||||
"effects": rule.get("effects", []),
|
||||
"strength": m.get("strength", rule.get("strength", "中")),
|
||||
"strength": strength,
|
||||
"source": rule.get("source", ""),
|
||||
"bvr_id": rule.get("bvr_id"),
|
||||
"category": rule.get("category", ""),
|
||||
"rule_id": rule.get("id", ""),
|
||||
# English twins for the reader's English edition; the Chinese
|
||||
# fields above are unchanged.
|
||||
"combination_en": _en_text(combination),
|
||||
"effects_en": list(rule.get("effects_en") or rule.get("effects", [])),
|
||||
"strength_en": rule.get("strength_en") if strength == rule.get("strength") and rule.get("strength_en") else STRENGTH_EN.get(str(strength), str(strength)),
|
||||
}
|
||||
dedup_key = rule.get("dedup_key") or rule["name"]
|
||||
dedup_val = f"{dedup_key}:{m.get('combination', '')}"
|
||||
@@ -713,17 +795,26 @@ class YogaEngine:
|
||||
if p_in_h12 and not p_in_h2:
|
||||
results.append({"name": "Voshi Yoga", "name_cn": "太阳前瑜伽",
|
||||
"combination": f"{'+'.join(p_in_h12)}在太阳第12宫",
|
||||
"combination_en": f"{'+'.join(p_in_h12)} in the 12th from the Sun",
|
||||
"effects": ["口才出众", "善于表达", "受人尊敬", "知识渊博"],
|
||||
"effects_en": ["Eloquent", "Expressive", "Respected", "Learned"],
|
||||
"strength_en": "Moderate",
|
||||
"strength": "中", "category": "solar_yoga", "source": "Parashara"})
|
||||
if p_in_h2 and not p_in_h12:
|
||||
results.append({"name": "Veshi Yoga", "name_cn": "太阳后瑜伽",
|
||||
"combination": f"{'+'.join(p_in_h2)}在太阳第2宫",
|
||||
"combination_en": f"{'+'.join(p_in_h2)} in the 2nd from the Sun",
|
||||
"effects": ["财富充裕", "生活舒适", "性情愉快", "人缘好"],
|
||||
"effects_en": ["Ample wealth", "Comfortable life", "Cheerful temperament", "Well liked"],
|
||||
"strength_en": "Moderate",
|
||||
"strength": "中", "category": "solar_yoga", "source": "Parashara"})
|
||||
if p_in_h2 and p_in_h12:
|
||||
results.append({"name": "Ubhayachari Yoga", "name_cn": "太阳双夹瑜伽",
|
||||
"combination": "太阳两侧均有行星",
|
||||
"combination_en": "Planets on both sides of the Sun",
|
||||
"effects": ["性格坚毅", "口才与财富兼备", "受人爱戴", "社会影响力"],
|
||||
"effects_en": ["Steadfast character", "Both eloquence and wealth", "Beloved by others", "Social influence"],
|
||||
"strength_en": "Strong",
|
||||
"strength": "强", "category": "solar_yoga", "source": "Parashara"})
|
||||
|
||||
# Lunar Yogas
|
||||
@@ -735,17 +826,26 @@ class YogaEngine:
|
||||
if p_in_h12 and not p_in_h2:
|
||||
results.append({"name": "Anapha Yoga", "name_cn": "月后瑜伽",
|
||||
"combination": f"{'+'.join(p_in_h12)}在月亮第12宫",
|
||||
"combination_en": f"{'+'.join(p_in_h12)} in the 12th from the Moon",
|
||||
"effects": ["体格健壮", "名声良好", "品德高尚", "行业领袖"],
|
||||
"effects_en": ["Robust physique", "Good reputation", "Noble character", "Leader in one's field"],
|
||||
"strength_en": "Moderate",
|
||||
"strength": "中", "category": "lunar_yoga", "source": "Parashara"})
|
||||
if p_in_h2 and not p_in_h12:
|
||||
results.append({"name": "Sunapha Yoga", "name_cn": "月前瑜伽",
|
||||
"combination": f"{'+'.join(p_in_h2)}在月亮第2宫",
|
||||
"combination_en": f"{'+'.join(p_in_h2)} in the 2nd from the Moon",
|
||||
"effects": ["自力更生", "财富充裕", "受人尊敬", "生活富足"],
|
||||
"effects_en": ["Self-made", "Ample wealth", "Respected", "Prosperous life"],
|
||||
"strength_en": "Moderate",
|
||||
"strength": "中", "category": "lunar_yoga", "source": "Parashara"})
|
||||
if p_in_h2 and p_in_h12:
|
||||
results.append({"name": "Durudhura Yoga", "name_cn": "月双夹瑜伽",
|
||||
"combination": "月亮两侧均有行星",
|
||||
"combination_en": "Planets on both sides of the Moon",
|
||||
"effects": ["享受丰富", "善于辞令", "性格坚定", "多元才华"],
|
||||
"effects_en": ["Many comforts", "Articulate", "Firm character", "Versatile talents"],
|
||||
"strength_en": "Strong",
|
||||
"strength": "强", "category": "lunar_yoga", "source": "Parashara"})
|
||||
|
||||
return results
|
||||
@@ -804,7 +904,7 @@ class YogaEngine:
|
||||
for sm in sub_matches:
|
||||
combo = match.get("combination", "")
|
||||
sm_combo = sm.get("combination", "")
|
||||
merged_combo = "; ".join(filter(None, [combo, sm_combo]))
|
||||
merged_combo = _bijoin("; ", [combo, sm_combo])
|
||||
new_results.append({
|
||||
"combination": merged_combo,
|
||||
"strength": sm.get("strength", match.get("strength")),
|
||||
@@ -851,8 +951,7 @@ class YogaEngine:
|
||||
h1 = ctx.house_of(a)
|
||||
h2 = ctx.house_of(b)
|
||||
if h1 is not None and h1 == h2:
|
||||
tmpl = cond.get("combo_template", "{a}与{b}同在第{h}宫")
|
||||
combo = tmpl.format(a=PLANET_CN.get(a, a), b=PLANET_CN.get(b, b), h=h1, a_raw=a, b_raw=b)
|
||||
combo = _combo(cond, "{a}与{b}同在第{h}宫", a=_planet(a), b=_planet(b), h=h1, a_raw=a, b_raw=b)
|
||||
results.append({"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"a": a, "b": b, "house": h1}})
|
||||
return results
|
||||
@@ -868,8 +967,7 @@ class YogaEngine:
|
||||
continue
|
||||
h = ctx.house_of(p)
|
||||
if h is not None and h in houses:
|
||||
tmpl = cond.get("combo_template", "{p}在第{h}宫")
|
||||
combo = tmpl.format(p=PLANET_CN.get(p, p), h=h, p_raw=p)
|
||||
combo = _combo(cond, "{p}在第{h}宫", p=_planet(p), h=h, p_raw=p)
|
||||
results.append({"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"planet": p, "house": h}})
|
||||
return results
|
||||
@@ -924,10 +1022,9 @@ class YogaEngine:
|
||||
continue
|
||||
off = ctx.house_offset(h_base, h1)
|
||||
if off in valid_offsets:
|
||||
tmpl = cond.get("combo_template", "{p}在第{h}宫(从{fp}的{rel})")
|
||||
rel_name = self._offset_name(off)
|
||||
combo = tmpl.format(p=PLANET_CN.get(p, p), h=h1, fp=PLANET_CN.get(fp, fp),
|
||||
rel=rel_name, off=off, p_raw=p, fp_raw=fp)
|
||||
rel_name = BiText(self._offset_name(off), OFFSET_NAME_EN.get(off, f"{off + 1}th"))
|
||||
combo = _combo(cond, "{p}在第{h}宫(从{fp}的{rel})", p=_planet(p), h=h1, fp=_planet(fp),
|
||||
rel=rel_name, off=off, p_raw=p, fp_raw=fp)
|
||||
results.append({"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"planet": p, "from": fp, "house": h1}})
|
||||
return results
|
||||
@@ -975,8 +1072,7 @@ class YogaEngine:
|
||||
dignity_type = status
|
||||
if ok:
|
||||
sign = ctx.sign_of(p)
|
||||
tmpl = cond.get("combo_template", "{p}{dignity}在{sign}")
|
||||
combo = tmpl.format(p=PLANET_CN.get(p, p), dignity=dignity_type, sign=sign or "", p_raw=p)
|
||||
combo = _combo(cond, "{p}{dignity}在{sign}", p=_planet(p), dignity=dignity_type, sign=sign or "", p_raw=p)
|
||||
results.append({"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"planet": p, "sign": sign}})
|
||||
return results
|
||||
@@ -1002,8 +1098,7 @@ class YogaEngine:
|
||||
ok = ok and count <= max_count
|
||||
|
||||
if ok:
|
||||
tmpl = cond.get("combo_template", "{count}颗行星满足条件")
|
||||
combo = tmpl.format(count=count, planets="、".join(PLANET_CN.get(p, p) for p in matched))
|
||||
combo = _combo(cond, "{count}颗行星满足条件", count=count, planets=_planets(matched))
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"count": count, "matched": matched}}]
|
||||
return []
|
||||
@@ -1032,8 +1127,7 @@ class YogaEngine:
|
||||
ok = ok and count <= max_count
|
||||
|
||||
if ok:
|
||||
tmpl = cond.get("combo_template", "行星分布在{count}个宫位")
|
||||
combo = tmpl.format(count=count)
|
||||
combo = _combo(cond, "行星分布在{count}个宫位", count=count)
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中")}]
|
||||
return []
|
||||
|
||||
@@ -1056,8 +1150,7 @@ class YogaEngine:
|
||||
ok = prev_ok or next_ok
|
||||
|
||||
if ok:
|
||||
tmpl = cond.get("combo_template", "第{target}宫被夹击")
|
||||
combo = tmpl.format(target=target, prev=prev_h, next=next_h)
|
||||
combo = _combo(cond, "第{target}宫被夹击", target=target, prev=prev_h, next=next_h)
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中")}]
|
||||
return []
|
||||
|
||||
@@ -1070,8 +1163,7 @@ class YogaEngine:
|
||||
occupied = [ctx.house_of(p) for p in refs]
|
||||
ok = all(h in houses for h in occupied if h is not None) and len(occupied) == len(refs)
|
||||
if ok:
|
||||
tmpl = cond.get("combo_template", "所有指定行星均落入{houses}")
|
||||
combo = tmpl.format(houses="/".join(str(h) for h in sorted(houses)), planets="、".join(PLANET_CN.get(p, p) for p in refs))
|
||||
combo = _combo(cond, "所有指定行星均落入{houses}", houses="/".join(str(h) for h in sorted(houses)), planets=_planets(refs))
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"planets": refs, "houses": sorted(houses)}}]
|
||||
return []
|
||||
@@ -1087,8 +1179,7 @@ class YogaEngine:
|
||||
occupied = [ctx.house_of(p) for p in refs]
|
||||
ok = all(h in houses_set for h in occupied if h is not None) and len(occupied) == len(refs)
|
||||
if ok:
|
||||
tmpl = cond.get("combo_template", "所有指定行星均落入候选宫位集合{houses}")
|
||||
combo = tmpl.format(houses="/".join(str(h) for h in sorted(houses_set)), planets="、".join(PLANET_CN.get(p, p) for p in refs))
|
||||
combo = _combo(cond, "所有指定行星均落入候选宫位集合{houses}", houses="/".join(str(h) for h in sorted(houses_set)), planets=_planets(refs))
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"planets": refs, "houses": sorted(houses_set)}}]
|
||||
return []
|
||||
@@ -1099,8 +1190,7 @@ class YogaEngine:
|
||||
houses = set(cond.get("houses", []))
|
||||
occupied = {ctx.house_of(p) for p in refs if ctx.house_of(p) is not None}
|
||||
if refs and occupied == houses:
|
||||
tmpl = cond.get("combo_template", "指定行星只占据{houses}")
|
||||
combo = tmpl.format(houses="/".join(str(h) for h in sorted(houses)), planets="、".join(PLANET_CN.get(p, p) for p in refs))
|
||||
combo = _combo(cond, "指定行星只占据{houses}", houses="/".join(str(h) for h in sorted(houses)), planets=_planets(refs))
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"planets": refs, "houses": sorted(houses)}}]
|
||||
return []
|
||||
@@ -1113,8 +1203,7 @@ class YogaEngine:
|
||||
for houses in house_sets:
|
||||
houses_set = set(houses)
|
||||
if refs and occupied == houses_set:
|
||||
tmpl = cond.get("combo_template", "指定行星只占据候选宫位集合{houses}")
|
||||
combo = tmpl.format(houses="/".join(str(h) for h in sorted(houses_set)), planets="、".join(PLANET_CN.get(p, p) for p in refs))
|
||||
combo = _combo(cond, "指定行星只占据候选宫位集合{houses}", houses="/".join(str(h) for h in sorted(houses_set)), planets=_planets(refs))
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"planets": refs, "houses": sorted(houses_set)}}]
|
||||
return []
|
||||
@@ -1125,8 +1214,7 @@ class YogaEngine:
|
||||
house = cond.get("house")
|
||||
matched = [p for p in refs if ctx.house_of(p) == house]
|
||||
if matched:
|
||||
tmpl = cond.get("combo_template", "第{house}宫有{planets}")
|
||||
combo = tmpl.format(house=house, planets="、".join(PLANET_CN.get(p, p) for p in matched))
|
||||
combo = _combo(cond, "第{house}宫有{planets}", house=house, planets=_planets(matched))
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"matched": matched, "house": house}}]
|
||||
return []
|
||||
@@ -1147,10 +1235,9 @@ class YogaEngine:
|
||||
target = ((base + rel_house - 2) % 12) + 1
|
||||
matched = [p for p in refs if p not in exclude and p != fp and ctx.house_of(p) == target]
|
||||
if matched:
|
||||
tmpl = cond.get("combo_template", "{planets}在{fp}的第{rel}宫")
|
||||
combo = tmpl.format(
|
||||
planets="、".join(PLANET_CN.get(p, p) for p in matched),
|
||||
fp=PLANET_CN.get(fp, fp),
|
||||
combo = _combo(cond, "{planets}在{fp}的第{rel}宫",
|
||||
planets=_planets(matched),
|
||||
fp=_planet(fp),
|
||||
rel=rel_house,
|
||||
target=target,
|
||||
)
|
||||
@@ -1174,11 +1261,10 @@ class YogaEngine:
|
||||
if max_count is not None:
|
||||
ok = ok and count <= max_count
|
||||
if ok:
|
||||
tmpl = cond.get("combo_template", "{count}个目标宫位有指定行星")
|
||||
detail = "; ".join(f"第{h}宫:" + "、".join(PLANET_CN.get(p, p) for p in ps)
|
||||
for h, ps in occupied)
|
||||
combo = tmpl.format(count=count, detail=detail,
|
||||
houses="/".join(str(h) for h, _ in occupied))
|
||||
detail = _bijoin("; ", [BiText(f"第{h}宫:" + _planets(ps), f"house {h}: " + _planets(ps).en)
|
||||
for h, ps in occupied])
|
||||
combo = _combo(cond, "{count}个目标宫位有指定行星", count=count, detail=detail,
|
||||
houses="/".join(str(h) for h, _ in occupied))
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"occupied": occupied, "count": count}}]
|
||||
return []
|
||||
@@ -1202,8 +1288,7 @@ class YogaEngine:
|
||||
if d1 is None or d2 is None:
|
||||
continue
|
||||
if abs(d1 - d2) < max_gap:
|
||||
tmpl = cond.get("combo_template", "{a}与{b}在{sign}内相距{gap:.2f}°")
|
||||
combo = tmpl.format(a=PLANET_CN.get(a, a), b=PLANET_CN.get(b, b), sign=s1,
|
||||
combo = _combo(cond, "{a}与{b}在{sign}内相距{gap:.2f}°", a=_planet(a), b=_planet(b), sign=s1,
|
||||
gap=abs(d1 - d2))
|
||||
results.append({"combination": combo, "strength": rule.get("strength", "中")})
|
||||
return results
|
||||
@@ -1221,8 +1306,7 @@ class YogaEngine:
|
||||
lord_of_sa = SIGN_LORDS.get(sa)
|
||||
lord_of_sb = SIGN_LORDS.get(sb)
|
||||
if lord_of_sa == lb and lord_of_sb == la:
|
||||
tmpl = cond.get("combo_template", "{la}与{lb}互换星座")
|
||||
combo = tmpl.format(la=PLANET_CN.get(la, la), lb=PLANET_CN.get(lb, lb))
|
||||
combo = _combo(cond, "{la}与{lb}互换星座", la=_planet(la), lb=_planet(lb))
|
||||
return [{"combination": combo, "strength": rule.get("strength", "强")}]
|
||||
return []
|
||||
|
||||
@@ -1264,7 +1348,7 @@ class YogaEngine:
|
||||
break
|
||||
matched_combo_parts.append(matches[0].get("combination", ""))
|
||||
if all_match and refs:
|
||||
combo = "; ".join(filter(None, matched_combo_parts))
|
||||
combo = _bijoin("; ", matched_combo_parts)
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中")}]
|
||||
return []
|
||||
|
||||
@@ -1299,8 +1383,7 @@ class YogaEngine:
|
||||
return []
|
||||
lh = ctx.house_of(lord)
|
||||
if lh is not None and lh in target_houses:
|
||||
tmpl = cond.get("combo_template", "{house}宫主{lord}在第{lh}宫")
|
||||
combo = tmpl.format(house=house, lord=PLANET_CN.get(lord, lord), lh=lh)
|
||||
combo = _combo(cond, "{house}宫主{lord}在第{lh}宫", house=house, lord=_planet(lord), lh=lh)
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中"),
|
||||
"bindings": {"lord": lord, "house": lh}}]
|
||||
return []
|
||||
@@ -1316,8 +1399,7 @@ class YogaEngine:
|
||||
ha = ctx.house_of(lord_a)
|
||||
hb = ctx.house_of(lord_b)
|
||||
if ha is not None and ha == hb:
|
||||
tmpl = cond.get("combo_template", "{la}与{lb}同在第{h}宫")
|
||||
combo = tmpl.format(la=PLANET_CN.get(lord_a, lord_a), lb=PLANET_CN.get(lord_b, lord_b), h=ha)
|
||||
combo = _combo(cond, "{la}与{lb}同在第{h}宫", la=_planet(lord_a), lb=_planet(lord_b), h=ha)
|
||||
return [{"combination": combo, "strength": rule.get("strength", "中")}]
|
||||
return []
|
||||
|
||||
@@ -1333,6 +1415,9 @@ class YogaEngine:
|
||||
|
||||
if result:
|
||||
combo = cond.get("combo_template", "自定义条件满足")
|
||||
if isinstance(combo, str):
|
||||
# Custom templates were never formatted; keep that, and pair the English text.
|
||||
combo = BiText(combo, cond.get("combo_template_en") if "combo_template" in cond else DEFAULT_COMBO_EN["自定义条件满足"])
|
||||
if callable(combo):
|
||||
combo = combo(ctx, bindings)
|
||||
strength = cond.get("strength", rule.get("strength", "中"))
|
||||
|
||||
Reference in New Issue
Block a user