Files
Jyotisha/scripts/report_render/text.py
T
jesse-uxandGrok (agent loop) dbba127e71 拆分报告渲染函数的第一阶段,把 34 个无外层依赖的辅助函数搬到 report_render。
同一份真实盘包在屏蔽与不屏蔽 PyJHora 下新旧渲染 sha 都一致,各 24 项且退出码 0。指定 pytest 233 通过、3 跳过。

Co-Authored-By: Grok (agent loop)
2026-10-15 20:59:11 +08:00

256 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Helpers moved unchanged out of render_pl9_markdown (phase 1)."""
import re
try:
from scripts.report_render.envelope import _unwrap
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
from report_render.envelope import _unwrap
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
SIGNS_CN = {'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座', 'Cancer': '巨蟹座', 'Leo': '狮子座', 'Virgo': '处女座', 'Libra': '天秤座', 'Scorpio': '天蝎座', 'Sagittarius': '射手座', 'Capricorn': '摩羯座', 'Aquarius': '水瓶座', 'Pisces': '双鱼座'}
PLANET_LABEL_CN = {
'Sun': '太阳',
'Moon': '月亮',
'Mars': '火星',
'Mercury': '水星',
'Jupiter': '木星',
'Venus': '金星',
'Saturn': '土星',
'Rahu': '北交点',
'Ketu': '南交点',
}
def _humanize_reader_token(token: str) -> str:
value = str(token or '').strip()
if not value:
return value
if value in SIGNS_CN:
return SIGNS_CN[value]
if value in PLANET_LABEL_CN:
return PLANET_LABEL_CN[value]
return value
def _humanize_reader_group(raw_group: str) -> str:
seen = set()
parts = []
for item in str(raw_group or '').split(','):
humanized = _humanize_reader_token(item)
if humanized and humanized not in seen:
seen.add(humanized)
parts.append(humanized)
return ' / '.join(parts)
def _humanize_tajika_candidate(name: str) -> str:
return {
'Ithasala_candidate': '趋近相位候选',
'Easarapha_candidate': '分离相位候选',
}.get(str(name or ''), '年度相位候选')
def _humanize_tajika_motion(motion: str) -> str:
return {'applying': '趋近', 'separating': '分离'}.get(str(motion or ''), str(motion or '-'))
def _display_rashi_from_value(raw_value):
if isinstance(raw_value, int) and 0 <= raw_value < len(SIGNS):
return _humanize_reader_token(SIGNS[raw_value])
return _humanize_reader_token(raw_value)
def _humanize_reader_summary_line(summary_line: str) -> str:
summary_line = _unwrap(summary_line)
if isinstance(summary_line, dict):
summary_line = (
summary_line.get('brief')
or summary_line.get('text')
or summary_line.get('summary')
or ''
)
text = str(summary_line or '').strip()
if not text:
return text
if text == "年度层已经有可读壳层,但仍需要带着冲突标签阅读。":
return "这一年的年度主线已经形成。"
if text == "This pack converts Dasha trigger facts into report-ready interpretation seeds without final event claims.":
return "这层已经把 Dasha 触发事实压成可读提示,但仍不直接升级为具体事件断语。"
if text == "Source: dasha_master_pack.interpretation_triggers.":
return "当前解释仍然直接来自 dasha_master_pack.interpretation_triggers 的证据层。"
if text == "All claims remain evidence-labeled and parameter-sensitive where the source pack is unclosed.":
return "只要 source pack 还没闭环,相关结论就继续保持 evidence-labeled 与 parameter_sensitive。"
if text == "Tajika Yogas: annual candidate structures visible":
return "Tajika Yogas 当前已经能读到年度候选交互结构。"
if text == "Sahams: annual sensitive points visible":
return "Sahams 当前已经能读到年度敏感点。"
match = re.match(r'^([^:]+):\s*conflict between\s+(.+?)\s+vs\s+(.+)$', text)
if match:
topic = match.group(1).strip()
left = _humanize_reader_group(match.group(2))
right = _humanize_reader_group(match.group(3))
topic_label = {
'Muntha': 'Muntha',
'Year Lord': 'Year Lord',
'Annual Asc': '返照上升',
}.get(topic, topic)
if left == right:
return f"{topic_label} 当前结果指向{left}。"
return f"{topic_label} 当前存在候选分歧:一组结果指向{left},另一组结果指向{right}。"
simple_match = re.match(r'^([^:]+):\s*(.+)$', text)
if simple_match:
topic = simple_match.group(1).strip()
value = _humanize_reader_group(simple_match.group(2))
if topic == 'Year Lord' and value:
return f"Year Lord 当前优先候选指向{value}。"
if topic == 'Muntha' and value:
return f"Muntha 当前优先候选指向{value}。"
if topic == 'Tajika Yogas' and 'annual candidate structures visible' in str(simple_match.group(2)).lower():
return "Tajika Yogas 当前已经能读到年度候选交互结构。"
if topic == 'Sahams' and 'annual sensitive points visible' in str(simple_match.group(2)).lower():
return "Sahams 当前已经能读到年度敏感点。"
return text
def _humanize_annual_highlight_line(highlight: str) -> str:
text = str(highlight or '').strip()
if not text:
return text
if text == "Solar Return / Tajika annual shell is available.":
return "本年度返照与 Tajika 时间结构已经生成。"
if text == "Muntha and Year Lord are visible, but Year Lord remains producer-disagree sensitive.":
return "Muntha 和 Year Lord 现在都已经可见,但 Year Lord 仍要带着分歧标签阅读。"
if text == "The report should read annual structure first, then the evidence appendix.":
return "这一层适合从年度结构展开,再回头核对证据附录。"
return text
def _humanize_annual_narrative_highlight(highlight: str) -> str:
return _humanize_annual_highlight_line(str(highlight)).rstrip('。')
def _humanize_domain_title(title: str) -> str:
text = str(title or '').strip()
if not text:
return text
mapping = {
"Career and public direction": "事业与公众方向",
"Wealth and growth opportunities": "财富与增长机会",
"Relationship and alliance themes": "关系与合作主题",
"Health and vitality caution": "健康与精力提醒",
"Risk, delay, and friction markers": "阻力、延迟与摩擦提示",
"Timing windows and cross-check needs": "时间窗口与交叉核对重点",
}
return mapping.get(text, text)
def _workbook_readable_label(worksheet_id: str) -> str:
if worksheet_id == 'birth_and_parameters':
return '出生与计算口径'
if worksheet_id == 'd1_rasi_bhava':
return '本命盘主结构'
if worksheet_id == 'strengths_and_scores':
return '力量与技术表'
if worksheet_id == 'divisional_and_special_charts':
return '分盘与特殊点位'
if worksheet_id == 'timing_and_predictive_systems':
return '时间系统与流年层'
if worksheet_id == 'advanced_systems':
return '进阶体系'
if worksheet_id == 'ai_and_audit':
return '审计与证据附录'
return '补充工作簿'
def _workbook_readable_sentence(worksheet_id: str) -> str:
if worksheet_id == 'birth_and_parameters':
return '出生参数、岁差、交点模式与计算口径已经固定,是整份报告的可信底座。'
if worksheet_id == 'd1_rasi_bhava':
return '本命盘主结构已经就位,适合先看性格主轴、房屋结构和基础趋势。'
if worksheet_id == 'strengths_and_scores':
return '力量、功能性吉凶与技术表可以直接用来判断支撑与压力来源。'
if worksheet_id == 'divisional_and_special_charts':
return '分盘与特殊点位可作为扩展阅读,适合补充看细节。'
if worksheet_id == 'timing_and_predictive_systems':
return '大运、行运与年度层已经可以形成时间主线。'
if worksheet_id == 'advanced_systems':
return 'Jaimini、KP、Yoga 等进阶体系可作为补充主轴。'
if worksheet_id == 'ai_and_audit':
return 'AI 读写、审计与证据层用于核对来源与边界,不直接替代正文。'
return f'{worksheet_id} 已纳入当前报告。'
def _workbook_reading_paragraph(worksheet_ids: list[str]) -> str:
parts = []
for worksheet_id in worksheet_ids:
sentence = _workbook_readable_sentence(worksheet_id).rstrip('。')
parts.append(f"先看{_workbook_readable_label(worksheet_id)},这里会告诉你{sentence}")
if not parts:
return '当前暂无可用工作簿阅读顺序。'
return ';'.join(parts) + '。'
def _md_cell(value) -> str:
text = str(value if value not in (None, '') else '-').replace('\n', ' ').strip()
return text.replace('|', '/')
def _render_compact_payload(value, depth: int = 0) -> str:
if value in (None, '', [], {}):
return '-'
if depth >= 2:
return _md_cell(type(value).__name__)
if isinstance(value, dict):
parts = []
for key in sorted(value, key=str):
if key in {
'narrative_items',
'domain_paragraphs',
'response_envelope',
'input_hash',
'profile_hash',
'raw_calculation',
'shared_full_report_authority',
}:
continue
parts.append(f"{key}={_render_compact_payload(value.get(key), depth + 1)}")
return '; '.join(parts) if parts else '-'
if isinstance(value, (list, tuple)):
items = [_render_compact_payload(item, depth + 1) for item in list(value)[:8]]
suffix = ' …' if len(value) > 8 else ''
return ', '.join(items) + suffix if items else '-'
return _md_cell(value)
def _sign_text(row: dict, sign_key: str = 'sign', cn_key: str = 'sign_cn') -> str:
if not isinstance(row, dict):
return '-'
sign = row.get(cn_key) or _humanize_reader_token(row.get(sign_key)) or row.get(sign_key)
return _md_cell(sign)
def _value_text(*values):
for value in values:
if value not in (None, '', [], {}):
return str(value)
return None
def _compose_narrative_paragraph(*parts: str | None) -> str | None:
cleaned = []
for part in parts:
text = str(part or '').strip()
if not text:
continue
text = text.rstrip()
if text.endswith('。'):
text = text[:-1]
cleaned.append(text)
if not cleaned:
return None
return ';'.join(cleaned) + '。'
def _planet_list_text(items: list[str], limit: int = 3) -> str | None:
names = []
seen = set()
for item in items:
humanized = _humanize_reader_token(item)
if humanized and humanized not in seen:
seen.add(humanized)
names.append(humanized)
if len(names) >= limit:
break
if not names:
return None
return '、'.join(names)
def _append_unique_text(items: list[str], value: str) -> None:
text = str(value or '').strip().rstrip('。')
if text and text not in items:
items.append(text)