fix(report): keep the English edition when one phrase was untranslated

The English personal report was discarded whenever a Chinese fragment survived the trailing glossary. English sections now render the English fields that were already calculated.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-22 02:56:00 +08:00
co-authored by Grok (agent loop) Cursor
parent 9812f31737
commit ba1ffeb35f
18 changed files with 815 additions and 53 deletions
+12
View File
@@ -75,6 +75,16 @@ KARAKA_DOMAINS = {
'Darakaraka': '配偶特质、伴侣关系、婚姻质量',
'Pitrukaraka': '父亲、祖先业力、传统传承',
}
KARAKA_DOMAINS_EN = {
'Atmakaraka': 'soul aim, core self, highest aim',
'Amatyakaraka': 'career direction, main advisor, delegated authority',
'Bhratrikaraka': 'siblings, courage, appetite for risk',
'Matrikaraka': 'mother, home base, emotional security',
'Putrakaraka': 'children, creativity, students, crafted results',
'Gnatikaraka': 'rivals, illness, obstacles, capacity for change',
'Darakaraka': 'spouse traits, partnership, marriage quality',
'Pitrukaraka': 'father, ancestral pattern, inherited tradition',
}
ARUDHA_NAMES = {
1: 'Arudha Lagna (AL)', 2: 'Dhana Pada (A2)', 3: 'Vikrama Pada (A3)',
@@ -119,6 +129,7 @@ def calc_chara_karaka_7(planet_degrees: Dict[str, float]) -> Dict:
'degree_in_sign': round(deg, 4),
'rank': rank,
'domain': KARAKA_DOMAINS.get(karaka, ''),
'domain_en': KARAKA_DOMAINS_EN.get(karaka, ''),
'cn_name': KARAKA_CN.get(karaka, karaka),
}
ak = results.get('Atmakaraka', {})
@@ -160,6 +171,7 @@ def calc_chara_karaka_8(planet_degrees: Dict[str, float]) -> Dict:
'rahu_reverse_applied': pname == 'Rahu',
'rank': rank,
'domain': KARAKA_DOMAINS.get(karaka, ''),
'domain_en': KARAKA_DOMAINS_EN.get(karaka, ''),
'cn_name': KARAKA_CN.get(karaka, karaka),
}
return {'karaka_table_8': results}
+4
View File
@@ -2612,10 +2612,13 @@ def calc_panchanga(sun_lon: float, moon_lon: float, weekday: int,
if score_pct >= 0.75:
overall = '吉(Subha)'
overall_en = 'Auspicious (Subha)'
elif score_pct >= 0.5:
overall = '中(Mixed)'
overall_en = 'Mixed'
else:
overall = '凶(Asubha)'
overall_en = 'Inauspicious (Asubha)'
# 特殊凶时段检查
warnings = []
@@ -2637,6 +2640,7 @@ def calc_panchanga(sun_lon: float, moon_lon: float, weekday: int,
'hora': hora,
'overall_score': round(score_pct, 2),
'overall_quality': overall,
'overall_quality_en': overall_en,
'warnings': warnings,
'auspicious_count': sum(1 for _, q in elements if q == 'subha'),
'total_elements': len(elements),
+75
View File
@@ -192,6 +192,7 @@ def rath_report_lines_zh(rath: Any) -> List[str]:
AD_DIRECTION_WORD_ZH = {"zodiacal": "顺行", "reverse": "逆行"}
AD_DIRECTION_WORD_EN = {"zodiacal": "zodiacal", "reverse": "retrograde"}
def _ad_direction_lines_zh(direction: Any) -> List[str]:
@@ -217,3 +218,77 @@ def rath_summary_line_zh(rath: Any) -> Optional[str]:
return (f"- 并列:{RATH_LABEL_ZH}:起运宫 {rath.get('seed_sign')}"
f"(第 {rath.get('strength_level_decided')} 级:{rath.get('strength_level_text')}){running}")
def rath_summary_line_en(rath: Any) -> Optional[str]:
"""English twin of `rath_summary_line_zh`. Chinese text is not reused."""
if not isinstance(rath, dict) or not rath:
return None
if not rath.get("seed_sign"):
return f"- Alongside: {RATH_LABEL_EN}: start sign undecided, {rath.get('reason') or rath.get('status')}"
md = (rath.get("current_dasha") or {}).get("md") or {}
running = f"; current mahadasha {md.get('sign')}, lord {md.get('lord') or 'undecided'}" if md else ""
level = rath.get("strength_level_decided")
level_text = rath.get("strength_level_text_en") or STRENGTH_LEVEL_EN.get(level) or ""
return (f"- Alongside: {RATH_LABEL_EN}: period start {rath.get('seed_sign')}"
f" (level {level}: {level_text}){running}")
def _ad_direction_lines_en(direction: Any) -> List[str]:
"""English twin of `_ad_direction_lines_zh`, including the book-divergence note."""
if not isinstance(direction, dict) or not direction.get("rule_text_en"):
return []
others = "; ".join(
f"{row.get('rule_text_en')} -> {AD_DIRECTION_WORD_EN.get(row.get('direction'), '-')}"
for row in direction.get("book_divergence") or [] if isinstance(row, dict))
line = (f"- sub-period direction: {direction['rule_text_en']}, "
f"{AD_DIRECTION_WORD_EN.get(direction.get('direction'), '-')}")
return [line + (f" (the book itself disagrees, not applied: {others})" if others else "")]
def rath_report_lines_en(rath: Any) -> List[str]:
"""English twin of `rath_report_lines_zh` for the full-data English edition."""
summary = rath_summary_line_en(rath)
if summary is None:
return []
lines = [summary]
if not rath.get("mahadasha"):
return lines
direction = rath.get("direction_text_en") or DIRECTION_EN.get(rath.get("direction"))
if direction:
note = f"; {rath['method_note_en']}" if rath.get("method_note_en") else ""
lines.append(f"- order: {direction}{note}")
lines.extend(["",
"| Rath MD | Sign | Lord | Start age | End age | Base years | Adjustment | Final |",
"|---------|------|------|-----------|---------|------------|------------|-------|"])
for index, row in enumerate(rath["mahadasha"], start=1):
lines.append("| {index} | {sign} | {lord} | {start} | {end} | {base} | {adjustment} | {years} |".format(
index=index, sign=row.get("sign"), lord=row.get("lord") or "undecided",
start=row.get("start_age"), end=row.get("end_age"),
base="—" if row.get("base_years") is None else row.get("base_years"),
adjustment="—" if row.get("dignity_adjustment") is None else row.get("dignity_adjustment"),
years="—" if row.get("years") is None else row.get("years")))
seen = set()
for row in rath["mahadasha"]:
sign = row.get("sign")
basis = row.get("dual_lord_basis_en")
if sign in ("Scorpio", "Aquarius") and basis and sign not in seen:
seen.add(sign)
lines.append(f"- dual lords {sign}: {basis}")
md = (rath.get("current_dasha") or {}).get("md")
rule = rath.get("current_md_antardasha_rule") or {}
if md and rath.get("current_md_antardasha"):
compared = rule.get("compared") or ["-", "-"]
lines.extend([
"",
f"- sub-periods of the current mahadasha {md.get('sign')}: compare {compared[0]} and {compared[-1]}, "
f"stronger {rule.get('stronger_sign')} (level {rule.get('strength_level_decided')}), "
f"from {rule.get('start_sign')} where lord {rule.get('lord')} sits, 12 equal parts",
*_ad_direction_lines_en(rule.get("direction")),
"",
"| Rath AD | Sign | Start age | End age |",
"|---------|------|-----------|---------|",
])
for index, row in enumerate(rath["current_md_antardasha"], start=1):
lines.append(f"| {index} | {row.get('sign')} | {row.get('start_age')} | {row.get('end_age')} |")
return lines
+36 -3
View File
@@ -2070,14 +2070,43 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str:
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), ""])
english_report = _pl9_report_language(packet) == "en"
strength_en_map: dict[str, str] = {}
han_leaks = None
if english_report:
try:
from yoga_engine import STRENGTH_EN as strength_en_map
except ImportError:
from scripts.yoga_engine import STRENGTH_EN as strength_en_map
try:
from pl9_reader_english import HAN as han_leaks
except ImportError:
from scripts.pl9_reader_english import HAN as han_leaks
def _english_plain(value: Any) -> str:
text = str(value or "").strip()
if not text or (han_leaks is not None and han_leaks.search(text)):
return ""
return text
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]
if english_report:
effects_src = item.get("effects_en") if isinstance(item.get("effects_en"), list) else []
effects = [text for text in (_english_plain(effect) for effect in effects_src) if text]
combination = _english_plain(item.get("combination_en")) or "-"
strength = _english_plain(item.get("strength_en"))
if not strength:
strength = _english_plain(strength_en_map.get(str(item.get("strength") or ""), ""))
else:
effects = [effect for effect in (clean_effect(e) for e in item.get("effects", [])) if effect]
combination = item.get("combination")
strength = item.get("strength")
effects_text = "; ".join(effects) if effects else "-"
rows.append([item.get("name"), item.get("category"), item.get("strength"), item.get("combination"), effects_text])
rows.append([item.get("name"), item.get("category"), strength, combination, effects_text])
lines.extend(["## Applicable Yogas", "", table(["Yoga", "Category", "Strength", "Combination", "Effects / notes"], rows), ""])
dosha = dict_get(("advanced_systems", "yogas_doshas"), {})
@@ -2092,10 +2121,14 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str:
for key in ["mangal_dosha", "kaal_sarp_dosha", "pitra_dosha", "sade_sati"]:
item = dosha.get(key)
if isinstance(item, dict):
if english_report:
summary = _english_plain(item.get("summary_en")) or "-"
else:
summary = item.get("summary") or item.get("phase") or item.get("type")
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"),
summary,
])
lines.extend(["## Dosha and Transit Summary", "", table(["Topic", "Detected / phase", "Summary"], rows), ""])
+74 -34
View File
@@ -23,9 +23,9 @@ for the public reader rather than exposing empty or machine-key rows.
from __future__ import annotations
try: # scripts/ on sys.path
from narayana_legacy_label import LEGACY_NOTE_ZH
from narayana_legacy_label import LEGACY_NOTE_EN, LEGACY_NOTE_ZH
except ImportError: # pragma: no cover - package import
from scripts.narayana_legacy_label import LEGACY_NOTE_ZH
from scripts.narayana_legacy_label import LEGACY_NOTE_EN, LEGACY_NOTE_ZH
import re
SIGNS: list[str] = []
@@ -974,11 +974,16 @@ def render_pl9_parity_markdown(packet: dict) -> str:
'| Panchanga | Value |',
'|-----------|-------|',
])
for key, label in (
panchanga_rows = (
('overall_quality_en', 'Overall quality'),
('auspicious_count', 'Auspicious elements'),
('total_elements', 'Total elements'),
) if _lang == 'en' else (
('overall_quality', '综合性质'),
('auspicious_count', '吉性要素数量'),
('total_elements', '要素总数'),
):
)
for key, label in panchanga_rows:
value = panchanga.get(key)
if value not in (None, ''):
lines.append(f"| {_cell(label)} | {_cell(value)} |")
@@ -1544,7 +1549,11 @@ def render_pl9_parity_markdown(packet: dict) -> str:
arudha_rows = _dict(arudha_pack.get('padas')) or arudha_pack
jaimini_special = _dict(jaimini.get('special_lagnas'))
if karaka_table or arudha_rows or jaimini_special:
lines.extend(['### Jaimini Karaka、Arudha Pada 与特殊 Lagna', ''])
lines.extend([
'### Jaimini Karaka, Arudha Pada and special Lagnas' if _lang == 'en'
else '### Jaimini Karaka、Arudha Pada 与特殊 Lagna',
'',
])
if karaka_table:
lines.extend([
'| Karaka | Planet | Degree | Ranking degree | Domain |',
@@ -1553,11 +1562,15 @@ def render_pl9_parity_markdown(packet: dict) -> str:
for name, row in karaka_table.items():
if not isinstance(row, dict):
continue
karaka_name = name if _lang == 'en' else (row.get('cn_name') or name)
domain = row.get('domain_en') if _lang == 'en' else row.get('domain')
if _lang == 'en' and domain and re.search(r'[\u4e00-\u9fff]', str(domain)):
domain = ''
lines.append(
f"| {_cell(row.get('cn_name') or name)} | {_cell(_token(row.get('planet')))} | "
f"| {_cell(karaka_name)} | {_cell(_token(row.get('planet')))} | "
f"{_cell(_degree(row.get('degree_in_sign') or row.get('sign_degree')))} | "
f"{_cell(_degree(row.get('effective_degree_in_sign', row.get('degree_in_sign'))))} | "
f"{_cell(row.get('domain'))} |"
f"{_cell(domain)} |"
)
lines.append('')
if arudha_rows or jaimini_special:
@@ -1686,7 +1699,8 @@ def render_pl9_parity_markdown(packet: dict) -> str:
vimsopaka = _dict(strengths.get('vimsopaka'))
if vimsopaka:
lines.extend([
'### p30-p60 Vimsopaka / Pushkara / Vargottama 辅助强度',
'### Vimsopaka / Pushkara / Vargottama supporting strength' if _lang == 'en'
else '### p30-p60 Vimsopaka / Pushkara / Vargottama 辅助强度',
'',
'| Planet | Vimsopaka | Grade | Pushkara Navamsa | Pushkara Bhaga | Vargottama |',
'|--------|-----------|-------|------------------|----------------|------------|',
@@ -1923,7 +1937,10 @@ def render_pl9_parity_markdown(packet: dict) -> str:
lines.extend([f'### p{page} {title_text}', ''])
if 'Narayana' in title_text:
# BUG-1215: the product Narayana is the legacy algorithm.
lines.extend([f'- 算法:{LEGACY_NOTE_ZH}', ''])
if _lang == 'en':
lines.extend([f'- Algorithm: {LEGACY_NOTE_EN}', ''])
else:
lines.extend([f'- 算法:{LEGACY_NOTE_ZH}', ''])
lines.extend([
'| 序号 | 主星/星座 | 起始 | 结束 | 年数 |',
'|------|-----------|------|------|------|',
@@ -1999,24 +2016,25 @@ def render_pl9_parity_markdown(packet: dict) -> str:
# Rath table already printed by the reference renderer. Display only.
narayana_packet = _dict(timing.get('narayana_dasha'))
if narayana_packet:
try:
from narayana_legacy_label import rath_report_lines_zh as _rath_lines
except ImportError:
from scripts.narayana_legacy_label import rath_report_lines_zh as _rath_lines
legacy_line = f'- 算法:{LEGACY_NOTE_ZH}'
if _lang == 'en':
try:
from narayana_legacy_label import rath_report_lines_en as _rath_lines
except ImportError:
from scripts.narayana_legacy_label import rath_report_lines_en as _rath_lines
legacy_line = f'- Algorithm: {LEGACY_NOTE_EN}'
alongside_prefix = '- Alongside:'
else:
try:
from narayana_legacy_label import rath_report_lines_zh as _rath_lines
except ImportError:
from scripts.narayana_legacy_label import rath_report_lines_zh as _rath_lines
legacy_line = f'- 算法:{LEGACY_NOTE_ZH}'
alongside_prefix = '- 并列:'
if legacy_line not in lines:
lines.extend(['### Narayana Dasha', '', legacy_line, ''])
rath_view = narayana_packet.get('rath')
rath_lines = [line for line in _rath_lines(rath_view) if line]
if _lang == 'en' and isinstance(rath_view, dict):
for row in _list(rath_view.get('mahadasha')):
if not isinstance(row, dict):
continue
zh_basis = row.get('dual_lord_basis')
en_basis = row.get('dual_lord_basis_en')
if zh_basis and en_basis:
rath_lines = [line.replace(str(zh_basis), str(en_basis)) for line in rath_lines]
if rath_lines and not any(str(line).startswith('- 并列:') for line in lines):
if rath_lines and not any(str(line).startswith(alongside_prefix) for line in lines):
lines.extend(rath_lines)
lines.append('')
source_dasha_pages = _list(source_visible.get('dasha_pages'))
@@ -2092,13 +2110,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
# Customer tables retain computed boundaries and reference points;
# print-layout rows deliberately blank repeated values.
kantaka_rows = _list(timeline.get('kantaka_rows')) or _list(display_rows.get('kantaka'))
sade_summary = sade_sati.get('summary_en') if _lang == 'en' else sade_sati.get('summary')
if _lang == 'en' and sade_summary and re.search(r'[\u4e00-\u9fff]', str(sade_summary)):
sade_summary = ''
lines.extend([
'### p121-p122 Sade Sati / Dhayya / Kantaka Saturn',
'',
f"- Moon sign:{_cell(_token(sade_sati.get('moon_sign')))}",
f"- 当前阶段:{_cell(sade_sati.get('phase') or sade_sati.get('status'))}",
f"- 是否 active:{_cell(sade_sati.get('is_active'))}",
f"- 摘要:{_cell(sade_sati.get('summary'))}",
f"- Moon sign: {_cell(_token(sade_sati.get('moon_sign')))}" if _lang == 'en'
else f"- Moon sign:{_cell(_token(sade_sati.get('moon_sign')))}",
f"- Current phase: {_cell(sade_sati.get('phase') or sade_sati.get('status'))}" if _lang == 'en'
else f"- 当前阶段:{_cell(sade_sati.get('phase') or sade_sati.get('status'))}",
f"- Active: {_cell(sade_sati.get('is_active'))}" if _lang == 'en'
else f"- 是否 active:{_cell(sade_sati.get('is_active'))}",
f"- Summary: {_cell(sade_summary)}" if _lang == 'en'
else f"- 摘要:{_cell(sade_sati.get('summary'))}",
'',
'| Phase | Sign |',
'|-------|------|',
@@ -2994,8 +3019,12 @@ def render_pl9_parity_markdown(packet: dict) -> str:
for fact_key in ('mars_house', 'mars_sign', 'sun_house', 'sun_sign', 'rahu_house', 'ketu_house', 'moon_sign', 'severity', 'type'):
if row.get(fact_key) not in (None, ''):
fact_bits.append(f"{fact_key}={_token(row.get(fact_key))}")
summary = row.get('summary_en') if _lang == 'en' else row.get('summary')
if _lang == 'en' and summary and re.search(r'[\u4e00-\u9fff]', str(summary)):
summary = ''
joiner = '; ' if _lang == 'en' else ';'
lines.append(
f"| {_cell(label)} | {_cell(detected)} | {_cell(';'.join(map(str, fact_bits)) or '-')} | {_cell(row.get('summary'))} |"
f"| {_cell(label)} | {_cell(detected)} | {_cell(joiner.join(map(str, fact_bits)) or '-')} | {_cell(summary)} |"
)
lines.append('')
dosha_rows = _list(dosha_pack.get('dosha_rows'))
@@ -3062,6 +3091,12 @@ def render_pl9_parity_markdown(packet: dict) -> str:
for row in detected_yogas:
if isinstance(row, dict):
grouped_yogas[_yoga_group_label(row)].append(row)
strength_en_map = {}
if _lang == 'en':
try:
from yoga_engine import STRENGTH_EN as strength_en_map
except ImportError:
from scripts.yoga_engine import STRENGTH_EN as strength_en_map
for group_title, group_rows in grouped_yogas.items():
if not group_rows:
continue
@@ -3074,15 +3109,20 @@ def render_pl9_parity_markdown(packet: dict) -> str:
for row in group_rows:
if _lang == 'en':
effects = row.get('effects_en') if isinstance(row.get('effects_en'), list) else []
if not effects and isinstance(row.get('effects'), list):
effects = row.get('effects')
yoga_name = row.get('name') or row.get('type') or row.get('name_cn')
combination = row.get('combination_en') or row.get('combination') or row.get('source_reference') or row.get('reason')
effects = [item for item in effects if not re.search(r'[\u4e00-\u9fff]', str(item))]
yoga_name = row.get('name') or row.get('type')
combination = row.get('combination_en')
if not combination or re.search(r'[\u4e00-\u9fff]', str(combination)):
combination = ''
strength = row.get('strength_en') or strength_en_map.get(str(row.get('strength') or ''))
if not strength or re.search(r'[\u4e00-\u9fff]', str(strength)):
strength = ''
else:
effects = row.get('effects') if isinstance(row.get('effects'), list) else []
yoga_name = row.get('name_cn') or row.get('name') or row.get('type')
combination = row.get('combination') or row.get('source_reference') or row.get('reason')
effect_text = _public_report_text(_brief(effects, 3) or row.get('interpretation') or row.get('summary'))
strength = row.get('strength')
effect_text = _public_report_text(_brief(effects, 3) or (None if _lang == 'en' else row.get('interpretation') or row.get('summary')))
try:
from pl9_language_terms import soften_lifespan_wording
except ModuleNotFoundError: # pragma: no cover
@@ -3090,7 +3130,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
effect_text = soften_lifespan_wording(str(effect_text or ''), _lang)
lines.append(
f"| {_cell(yoga_name)} | {_cell(row.get('category') or row.get('subtype'))} | "
f"{_cell(row.get('strength'))} | {_cell(combination)} | "
f"{_cell(strength)} | {_cell(combination)} | "
f"{_cell(effect_text)} |"
)
lines.append('')
+2
View File
@@ -135,6 +135,8 @@ CORE_PYTEST_TARGETS = [
"tests/test_serif_font_slices.py",
# Chart, report, and consult entries must not import PyJHora (BUG-1273).
"tests/test_runtime_jhora_import_scan.py",
# English edition han leak, 12-chart slice (issue #89 / BUG-1410). The 200-chart file stays off this list.
"tests/test_report_english_sweep_smoke.py",
]
RUNTIME_TRUTH_PYTEST_TARGETS = [
+4 -1
View File
@@ -156,7 +156,10 @@ OFFSET_NAME_EN = {0: "same house", 1: "2nd", 2: "3rd", 3: "4th (Kendra)", 4: "5t
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"}
STRENGTH_EN = {
"强": "Strong", "中": "Moderate", "弱": "Weak", "待评估": "To be assessed", "观察": "Watch",
"部分减弱": "partly weakened",
}
def _combo(cond: Dict, default_zh: str, **values) -> BiText:
+30 -1
View File
@@ -576,6 +576,7 @@ def calc_mangal_dosha(planets_data: Dict, mars_house: int) -> Dict:
bad_houses = [1, 2, 4, 7, 8, 12]
if mars_house not in bad_houses:
results['summary'] = '无Mangal Dosha(Mars不在煞宫)'
results['summary_en'] = 'No Mangal Dosha (Mars is not in a dosha house)'
return results
results['has_dosha'] = True
@@ -605,6 +606,10 @@ def calc_mangal_dosha(planets_data: Dict, mars_house: int) -> Dict:
results['mars_sign'] = mars_sign
results['summary'] = (f"{'无' if not results['has_dosha'] else '有'}Mangal Dosha——"
f"Mars在{mars_house}宫{mars_sign},严重程度:{results['severity']}")
present = 'No' if not results['has_dosha'] else 'Present'
results['summary_en'] = (
f"{present} Mangal Dosha — Mars in house {mars_house}, {mars_sign}; severity: {results['severity']}"
)
return results
@@ -626,6 +631,7 @@ def calc_kaal_sarp_dosha(planets_data: Dict) -> Dict:
if not isinstance(rahu_data, dict) or 'house' not in rahu_data:
results['summary'] = 'Rahu位置未知,无法判断Kaal Sarp Dosha'
results['summary_en'] = 'Rahu position unknown; Kaal Sarp Dosha cannot be judged'
return results
rahu_house = rahu_data['house']
@@ -655,14 +661,21 @@ def calc_kaal_sarp_dosha(planets_data: Dict) -> Dict:
results['has_dosha'] = True
results['type'] = 'full'
results['summary'] = f'完全Kaal Sarp Dosha——所有{total_planets}颗行星在Rahu({rahu_house})—Ketu轴线之间'
results['summary_en'] = (
f'Full Kaal Sarp Dosha — all {total_planets} planets lie between the Rahu ({rahu_house})-Ketu axis'
)
elif len(planets_in_axis) >= total_planets * 0.7:
results['has_dosha'] = True
results['type'] = 'partial'
results['summary'] = f'部分Kaal Sarp Dosha——{len(planets_in_axis)}/{total_planets}颗行星在轴线之间'
results['summary_en'] = (
f'Partial Kaal Sarp Dosha — {len(planets_in_axis)} of {total_planets} planets lie between the axis'
)
else:
results['has_dosha'] = False
results['type'] = 'none'
results['summary'] = '无Kaal Sarp Dosha'
results['summary_en'] = 'No Kaal Sarp Dosha'
results['planets_in_axis'] = planets_in_axis
results['planets_outside'] = planets_outside
@@ -682,13 +695,14 @@ def calc_pitra_dosha(planets_data: Dict, sun_house: int) -> Dict:
影响:父辈业力、祖先未解问题、子女健康/生育问题
"""
results = {'has_dosha': False, 'indicators': []}
results = {'has_dosha': False, 'indicators': [], 'indicators_en': []}
sun_data = planets_data.get('Sun', {})
rahu_data = planets_data.get('Rahu', {})
if not isinstance(sun_data, dict) or 'house' not in sun_data:
results['summary'] = 'Sun位置未知,无法判断Pitra Dosha'
results['summary_en'] = 'Sun position unknown; Pitra Dosha cannot be judged'
return results
sun_house = sun_data['house']
@@ -700,6 +714,9 @@ def calc_pitra_dosha(planets_data: Dict, sun_house: int) -> Dict:
if abs(sun_house - rahu_house) <= 1 or (sun_house == rahu_house):
results['has_dosha'] = True
results['indicators'].append(f'Sun({sun_house}宫)与Rahu({rahu_house}宫)合相/相邻')
results['indicators_en'].append(
f'Sun (house {sun_house}) conjunct or adjacent to Rahu (house {rahu_house})'
)
# 检查Sun受克(土星/火星在Sun宫或相邻宫)
for malefic in ['Saturn', 'Mars']:
@@ -709,13 +726,18 @@ def calc_pitra_dosha(planets_data: Dict, sun_house: int) -> Dict:
if abs(sun_house - m_house) <= 1:
results['has_dosha'] = True
results['indicators'].append(f'{malefic}({m_house}宫)克Sun({sun_house}宫)')
results['indicators_en'].append(
f'{malefic} (house {m_house}) afflicts Sun (house {sun_house})'
)
results['sun_house'] = sun_house
results['sun_sign'] = sun_sign
if not results['indicators']:
results['summary'] = '无Pitra Dosha明显迹象'
results['summary_en'] = 'No clear indication of Pitra Dosha'
else:
results['summary'] = f'有Pitra Dosha迹象:{"; ".join(results["indicators"][:2])}'
results['summary_en'] = 'Indications of Pitra Dosha: ' + '; '.join(results['indicators_en'][:2])
return results
@@ -737,6 +759,7 @@ def calc_sade_sati(moon_sign: str, current_year: int = 2026) -> Dict:
if not moon_sign or moon_sign not in SIGNS:
results['summary'] = 'Moon星座未知,无法判断Sade Sati'
results['summary_en'] = 'Moon sign unknown; Sade Sati cannot be judged'
return results
moon_idx = SIGNS.index(moon_sign)
@@ -754,6 +777,12 @@ def calc_sade_sati(moon_sign: str, current_year: int = 2026) -> Dict:
f'高峰期={results["phases"]["peak"]}(最困难),'
f'下降期={results["phases"]["setting"]};'
f'需结合Saturn实际位置判断是否在进行中')
results['summary_en'] = (
f"Sade Sati phase frame: rising={results['phases']['rising']}, "
f"peak={results['phases']['peak']} (most difficult), "
f"setting={results['phases']['setting']}; "
"judge from Saturn's actual position whether it is underway"
)
return results