Skip to content

Commit 9454747

Browse files
ChiShengChenclaude
andcommitted
i18n: bilingual (English + 中文) content throughout
- add system_en (English name) to Chart schema + registry + all 11 adapters - bilingual LLM reading: interpret.py system prompt asks for EN then 中文 section - bilingual mock stub, README, web UI labels, API docstrings, field descriptions - divination terms (干支/卦名/宮名…) kept in original form as proper nouns - 12 tests pass; all 11 systems cast with EN + 中文 names Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ea05e21 commit 9454747

22 files changed

Lines changed: 179 additions & 152 deletions

.env.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# 算命 service config — copy to .env and fill in as needed.
1+
# Bazaar of Fates config — copy to .env and fill in as needed. / 複製成 .env 後填入。
22

3-
# 解讀 backend. "mock" (default) needs NO API key: every 命盤 still casts
4-
# deterministically, only the prose interpretation is a stubbed facts digest.
3+
# Reading backend / 解讀後端. "mock" (default) needs NO API key: every chart still
4+
# casts deterministically, only the prose reading is a stubbed facts digest.
55
LLM_BACKEND=mock
66
# LLM_BACKEND=anthropic
77
ANTHROPIC_API_KEY=

README.md

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,100 @@
1-
# 算命 · Divination Suite
1+
# Bazaar of Fates · 算命 Divination Suite
22

3-
十一套傳統命理系統,寫成**確定性 Python 排盤引擎**,前面接一層 LLM 解讀,後面只吃一個輸入:**生辰**(出生日期+時辰+出生地)。
3+
**Eleven traditional divination systems as deterministic Python engines**, behind one input — your **birth moment** (date + time + place) — each producing a reproducible chart plus an optional bilingual AI reading.
44

5-
> 起源:本專案的排盤引擎源自一個更大的量化研究專案,原本是當作「對照組/安慰劑」(把命理當成 date-keyed 訊號,跑無未來函數回測證明它們是雜訊)。這裡把**排盤數學**單獨抽出來,去掉所有交易/回測,還原成它本來的用途——**幫人算命**。排盤數學以 `scripts/sync_from_main.sh` 從母專案同步(單一真相源),算命產品殼(生辰輸入、API、解讀、前端)是本 repo 原生
5+
**十一套傳統命理系統**,寫成確定性 Python 排盤引擎,只吃一個輸入:**生辰**(出生日期+時辰+出生地),各自排出可重現的命盤+可選的雙語 AI 解讀
66

7-
## 十一系
7+
> **Origin / 起源.** These engines began life as *placebo controls* in a larger quant project — divination cast as date-keyed trading signals, run through a lookahead-free backtest to prove they were noise. Here the **chart math is lifted out**, all trading/backtest stripped, and restored to its real purpose: **telling fortunes**. The 排盤 math is synced from the parent monorepo (single source of truth) via `scripts/sync_from_main.sh`; the product shell (birth input, API, readings, web) is native to this repo.
8+
> 這些引擎原本是某量化專案裡的「對照組/安慰劑」(把命理當訊號跑無未來函數回測,證明它們是雜訊)。這裡把排盤數學抽出、去掉交易回測,還原它本來的用途——算命。
89
9-
| key | 系統 | 引擎核心 | 時辰敏感 | 出生地敏感 |
10-
|---|---|---|:--:|:--:|
11-
| `astrology` | 西洋占星 | `ephem` 黃道經度 | 月相 | 規劃中(上升/宮位)|
12-
| `bazi` | 八字(四柱)| JDN 校準干支 | ✅ 時柱 ||
13-
| `ziwei` | 紫微斗數 | 純 Python 排盤 | 規劃中(命宮)||
14-
| `iching` | 梅花易數 | 時間起卦 |||
15-
| `suimei` | 四柱推命(日)| 十二運星+天中殺 |||
16-
| `qizheng` | 七政四餘 | 真實天文經度 || 規劃中 |
17-
| `tieban` | 鐵板神數 | 起命數 |||
18-
| `qimen` | 奇門遁甲 | 八門九宮起局 |||
19-
| `liuren` | 大六壬 | 四課三傳 |||
20-
| `taiyi` | 太乙神數 | 太乙九宮 |||
21-
| `jyotish` | Jyotiṣa 吠陀占星 | 恆星黃道+Vimśottarī 大運 || 規劃中 |
10+
## The eleven systems / 十一系
2211

23-
> **誠實標註**:八字/四柱推命已用 `時辰` 排時柱;占星/紫微/七政等的「時辰/出生地」欄位已在輸入層備好(`BirthInput`),但對應引擎的上升星座、命宮、宮位計算是**規劃中的引擎升級**——升級寫在母專案、再 sync 過來。
12+
| key | System | 系統 | engine core | time-aware 時辰 | place-aware 出生地 |
13+
|---|---|---|---|:--:|:--:|
14+
| `astrology` | Western Astrology | 西洋占星 | `ephem` ecliptic longitudes | moon phase | planned 規劃中 |
15+
| `bazi` | BaZi · Four Pillars | 八字(四柱)| JDN-anchored 干支 | ✅ hour pillar ||
16+
| `ziwei` | Zi Wei Dou Shu | 紫微斗數 | pure-Python 排盤 | planned 規劃中 ||
17+
| `iching` | Plum-Blossom I Ching | 梅花易數 | time-cast hexagram |||
18+
| `suimei` | Shichū-Suimei (JP) | 四柱推命(日)| 十二運星 + 天中殺 |||
19+
| `qizheng` | Seven Luminaries | 七政四餘 | real astronomical longitudes || planned 規劃中 |
20+
| `tieban` | Iron Plate | 鐵板神數 | 起命數 |||
21+
| `qimen` | Qi Men Dun Jia | 奇門遁甲 | 八門九宮起局 |||
22+
| `liuren` | Da Liu Ren | 大六壬 | 四課三傳 |||
23+
| `taiyi` | Tai Yi Shen Shu | 太乙神數 | 太乙九宮 |||
24+
| `jyotish` | Jyotiṣa (Vedic) | 吠陀占星 | sidereal + Vimśottarī daśā || planned 規劃中 |
2425

25-
## 快速開始
26+
> **Honest note / 誠實標註.** BaZi and Suimei already use `time` for the hour pillar; the time/place fields are carried on `BirthInput` for every system, but ascendant/house/命宮 computation in astrology · ziwei · qizheng · jyotish is a **planned engine upgrade** — to be written in the parent monorepo and synced over, not hand-patched here.
27+
28+
## Quickstart / 快速開始
2629

2730
```bash
2831
python -m venv .venv && source .venv/bin/activate
29-
pip install -e ".[dev,llm]" # llm 可省略;不裝就用 mock 解讀
32+
pip install -e ".[dev,llm]" # drop "llm" to stay on the mock reader / 省略 llm 即用 mock
3033
cp .env.example .env
3134

32-
# 後端 API
35+
# Backend API / 後端
3336
uvicorn fortune.api.main:app --reload
3437

35-
# 靜態算命頁(免 node build):用任意靜態伺服器開 web/index.html
38+
# Static fortune page, no node build / 靜態算命頁,免 node build
3639
python -m http.server 5500 --directory web
37-
# http://localhost:5500/ 填生辰 → 選命理系統 → 看命盤+解讀
40+
# open http://localhost:5500/ → fill birth → pick a system → see chart + reading
3841
```
3942

40-
不接 LLM 也能跑:每個命盤都會**確定性排出**,只有解讀文字是 mock 的事實摘要。設 `LLM_BACKEND=anthropic` + `ANTHROPIC_API_KEY` 即換成真正的 AI 解讀。
43+
Runs with **no LLM key**: every chart casts deterministically; only the prose reading is a mocked facts digest. Set `LLM_BACKEND=anthropic` + `ANTHROPIC_API_KEY` for a real **bilingual (English + 中文)** AI reading.
44+
不接 LLM 也能跑:命盤照排,只有解讀走 mock。設金鑰後即得真正的雙語 AI 解讀。
4145

4246
## API
4347

44-
| method | path | 說明 |
48+
| method | path | |
4549
|---|---|---|
46-
| `GET` | `/systems` | 11 系清單+哪些可用 |
47-
| `POST` | `/cast/{system}` | 純命盤(不呼叫 LLM`Chart` |
48-
| `POST` | `/reading/{system}` | 命盤+解讀 `Reading` |
50+
| `GET` | `/systems` | the 11 systems + which cast cleanly / 11 系清單+可用狀態 |
51+
| `POST` | `/cast/{system}` | deterministic chart, no LLM `Chart` / 純命盤 |
52+
| `POST` | `/reading/{system}` | chart + bilingual reading `Reading` / 命盤+解讀 |
4953

5054
```bash
5155
curl -s localhost:8000/cast/bazi -H 'content-type: application/json' -d '{
52-
"name":"小美","birth_date":"1990-06-15","birth_time":"14:30",
53-
"gender":"","place":"台北","latitude":25.04,"longitude":121.56
56+
"name":"Mei","birth_date":"1990-06-15","birth_time":"14:30",
57+
"gender":"female","place":"Taipei","latitude":25.04,"longitude":121.56
5458
}' | jq .summary
5559
# "日主 辛金・身弱(喜生扶)・喜用 土、金"
5660
```
5761

58-
## 架構
62+
## Architecture / 架構
5963

6064
```
6165
fortune/
62-
birth.py 生辰輸入(唯一輸入)
63-
schemas.py Chart / Reading 統一封裝
64-
shared/ 原生:config / logging / llmmock+anthropic
65-
engines/<system>/ ← sync 自母專案的純排盤數學(勿手改,會被覆蓋)
66-
casting/<system>.py 每系 adapter:生辰 → 引擎函式 → Chart
67-
casting/__init__.py 11 系註冊表(惰性 import
68-
interpret.py 命盤事實 + 門派 prompt → LLM 解讀
66+
birth.py BirthInput — the single input / 生辰輸入
67+
schemas.py Chart / Reading envelope
68+
shared/ native: config / logging / llm (mock + anthropic)
69+
engines/<system>/ ← synced 排盤 math from the monorepo (do NOT hand-edit)
70+
casting/<system>.py per-system adapter: birth → engine fns → Chart
71+
casting/__init__.py registry of the 11 systems (lazy import)
72+
interpret.py chart facts + tradition prompt → bilingual reading
6973
api/main.py FastAPI
70-
prompts/<system>/ ← sync 自母專案的門派解讀 prompt
71-
web/ 靜態算命頁 + sync 來的命盤渲染元件
72-
scripts/sync_from_main.sh 重新同步排盤數學
74+
prompts/<system>/ ← synced reading prompts from the monorepo
75+
web/index.html static fortune page + synced chart renderers
76+
scripts/sync_from_main.sh re-sync the 排盤 math
7377
```
7478

75-
### 重新同步排盤數學
79+
### Re-syncing the engine math / 重新同步排盤數學
7680

77-
母專案改了某系的排盤邏輯後:
81+
After the parent monorepo changes a system's casting logic:
7882

7983
```bash
80-
scripts/sync_from_main.sh # 預設母專案 ~/Desktop/威鯨面試_LLMEng
84+
scripts/sync_from_main.sh # default monorepo ~/Desktop/威鯨面試_LLMEng
8185
scripts/sync_from_main.sh /path/to/monorepo
8286
```
8387

84-
只會覆蓋 `fortune/engines/*``prompts/*``web/app/_charts/*`(排盤數學與命盤視覺);
85-
生辰輸入、API、解讀、前端殼是原生的,**不會被動到**
88+
Only `fortune/engines/*`, `prompts/*`, `web/app/_charts/*` are overwritten (the 排盤 math + chart visuals). The birth input, API, readings, and web shell are native and **untouched**.
89+
只覆蓋排盤數學與命盤視覺;生辰輸入、API、解讀、前端殼不會被動到
8690

87-
## 測試
91+
## Tests / 測試
8892

8993
```bash
90-
pytest -q # 11 系各自從同一個生辰排出非空命盤 + mock 解讀
94+
pytest -q # all 11 systems cast a non-empty chart from one birth + a mock reading
9195
```
96+
97+
## License
98+
99+
For cultural, educational, and entertainment purposes. Divination is not a basis for financial, medical, or legal decisions.
100+
僅供文化、教育與娛樂用途;命理不應作為財務、醫療或法律決策的依據。

fortune/api/main.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
"""算命 service — FastAPI. One input (生辰), eleven 命理 systems, deterministic 命盤
2-
plus optional LLM 解讀.
1+
"""Bazaar of Fates — FastAPI 算命 service. One input (birth / 生辰), eleven divination
2+
systems, a deterministic chart (命盤) plus an optional bilingual LLM reading (解讀).
33
44
GET /systems → the 11 systems + which cast cleanly
5-
POST /cast/{system} → deterministic 命盤 (no LLM) → Chart
6-
POST /reading/{system} → 命盤 + 解讀 (LLM, mock by default) → Reading
5+
POST /cast/{system} → deterministic chart, no LLM → Chart
6+
POST /reading/{system} → chart + reading (LLM, mock by default) → Reading
77
"""
88

99
from __future__ import annotations
@@ -23,7 +23,7 @@
2323
log = get_logger("api")
2424
settings = get_settings()
2525

26-
app = FastAPI(title="算命 · Divination Suite", version="0.1.0")
26+
app = FastAPI(title="Bazaar of Fates · 算命 Divination Suite", version="0.1.0")
2727
app.add_middleware(
2828
CORSMiddleware,
2929
allow_origins=settings.cors_origin_list,
@@ -34,7 +34,7 @@
3434

3535
class ReadingRequest(BaseModel):
3636
birth: BirthInput
37-
focus: str | None = None # 命主想問的方向(事業/感情/健康…),可選
37+
focus: str | None = None # optional topic / 命主想問的方向(career, love, health…)
3838

3939

4040
@app.get("/health")
@@ -55,7 +55,7 @@ def cast(system: str, birth: BirthInput) -> Chart:
5555
raise HTTPException(404, str(e)) from e
5656
except Exception as e: # noqa: BLE001
5757
log.exception("cast_failed", system=system)
58-
raise HTTPException(500, f"{system} 排盤失敗:{e}") from e
58+
raise HTTPException(500, f"{system} cast failed / 排盤失敗:{e}") from e
5959

6060

6161
@app.post("/reading/{system}", response_model=Reading)
@@ -66,5 +66,5 @@ def reading(system: str, req: ReadingRequest) -> Reading:
6666
raise HTTPException(404, str(e)) from e
6767
except Exception as e: # noqa: BLE001
6868
log.exception("cast_failed", system=system)
69-
raise HTTPException(500, f"{system} 排盤失敗:{e}") from e
69+
raise HTTPException(500, f"{system} cast failed / 排盤失敗:{e}") from e
7070
return interpret(chart, focus=req.focus)

fortune/birth.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
"""生辰輸入 — the one input the whole 算命 service takes.
1+
"""Birth input / 生辰輸入 — the one input the whole service takes.
22
33
Replaces the monorepo's "stock listing date": a person's birth moment (date +
44
time-of-day + optional birthplace). Time-of-day drives the 時柱 / Moon position;
5-
birthplace (lat/lon) is what house-/ascendant-based systems (占星, Jyotiṣa) need —
6-
carried here so engines can use it as they grow into it.
5+
birthplace (lat/lon) is what house-/ascendant-based systems (astrology, Jyotiṣa)
6+
need — carried here so engines can use it as they grow into it.
7+
取代母專案的「股票上市日」:一個人的出生時刻(日期+時辰+出生地)。
78
"""
89

910
from __future__ import annotations
@@ -14,18 +15,18 @@
1415

1516

1617
class BirthInput(BaseModel):
17-
name: str | None = Field(default=None, description="稱呼(可選,只用於解讀文案)")
18-
birth_date: date = Field(..., description="出生日期")
18+
name: str | None = Field(default=None, description="Name, display only / 稱呼(可選)")
19+
birth_date: date = Field(..., description="Birth date / 出生日期")
1920
birth_time: time | None = Field(
20-
default=None, description="出生時刻(時辰)。未知時 時柱以正午估算"
21+
default=None, description="Birth time / 出生時刻(時辰);unknown → noon 時柱以正午估算"
2122
)
22-
gender: str | None = Field(default=None, description="性別(部分命理用神不同)")
23+
gender: str | None = Field(default=None, description="Gender / 性別(部分命理用神不同)")
2324

24-
# birthplace — optional, needed for ascendant/house systems
25-
place: str | None = Field(default=None, description="出生地名(顯示用)")
25+
# birthplace — optional, needed for ascendant/house systems / 出生地,宮位系統需要
26+
place: str | None = Field(default=None, description="Birthplace name / 出生地名(顯示用)")
2627
latitude: float | None = Field(default=None, ge=-90, le=90)
2728
longitude: float | None = Field(default=None, ge=-180, le=180)
28-
tz_offset_hours: float = Field(default=8.0, description="出生地時區(東八區=+8)")
29+
tz_offset_hours: float = Field(default=8.0, description="Timezone offset / 出生地時區(東八區=+8)")
2930

3031
@field_validator("birth_date")
3132
@classmethod
@@ -49,9 +50,9 @@ def dt(self) -> datetime:
4950
return datetime.combine(self.birth_date, t)
5051

5152
def label(self) -> str:
52-
who = self.name or "命主"
53+
who = self.name or "Querent 命主"
5354
when = self.birth_date.isoformat() + (
54-
f" {self.birth_time.strftime('%H:%M')}" if self.birth_time else " (時辰未知)"
55+
f" {self.birth_time.strftime('%H:%M')}" if self.birth_time else " (time unknown 時辰未知)"
5556
)
5657
where = f" · {self.place}" if self.place else ""
5758
return f"{who} · {when}{where}"

fortune/casting/__init__.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Registry of the 11 命理 systems → their cast(birth) adapter.
2+
十一套命理系統 → 各自的 cast(birth) 轉接器。
23
3-
Each value is (中文名, "module:function"). Imports are lazy so a half-wired engine
4-
never breaks the whole service — `systems()` reports which casters import cleanly.
4+
Each value is (English name, 中文名, "module:function"). Imports are lazy so a
5+
half-wired engine never breaks the whole service — `systems()` reports which
6+
casters import cleanly. / 惰性 import,未接好的引擎不會拖垮整個服務。
57
"""
68

79
from __future__ import annotations
@@ -11,19 +13,19 @@
1113
from fortune.birth import BirthInput
1214
from fortune.schemas import Chart
1315

14-
# key : (中文名, dotted "module:func")
15-
REGISTRY: dict[str, tuple[str, str]] = {
16-
"astrology": ("西洋占星", "fortune.casting.astrology:cast"),
17-
"bazi": ("八字(四柱)", "fortune.casting.bazi:cast"),
18-
"ziwei": ("紫微斗數", "fortune.casting.ziwei:cast"),
19-
"iching": ("梅花易數", "fortune.casting.iching:cast"),
20-
"suimei": ("四柱推命(日)", "fortune.casting.suimei:cast"),
21-
"qizheng": ("七政四餘", "fortune.casting.qizheng:cast"),
22-
"tieban": ("鐵板神數", "fortune.casting.tieban:cast"),
23-
"qimen": ("奇門遁甲", "fortune.casting.qimen:cast"),
24-
"liuren": ("大六壬", "fortune.casting.liuren:cast"),
25-
"taiyi": ("太乙神數", "fortune.casting.taiyi:cast"),
26-
"jyotish": ("Jyotiṣa(吠陀占星)", "fortune.casting.jyotish:cast"),
16+
# key : (English name, 中文名, dotted "module:func")
17+
REGISTRY: dict[str, tuple[str, str, str]] = {
18+
"astrology": ("Western Astrology", "西洋占星", "fortune.casting.astrology:cast"),
19+
"bazi": ("BaZi · Four Pillars", "八字(四柱)", "fortune.casting.bazi:cast"),
20+
"ziwei": ("Zi Wei Dou Shu · Purple Star", "紫微斗數", "fortune.casting.ziwei:cast"),
21+
"iching": ("Plum-Blossom I Ching", "梅花易數", "fortune.casting.iching:cast"),
22+
"suimei": ("Shichū-Suimei · JP Four Pillars", "四柱推命(日)", "fortune.casting.suimei:cast"),
23+
"qizheng": ("Qi Zheng Si Yu · Seven Luminaries", "七政四餘", "fortune.casting.qizheng:cast"),
24+
"tieban": ("Tie Ban Shen Shu · Iron Plate", "鐵板神數", "fortune.casting.tieban:cast"),
25+
"qimen": ("Qi Men Dun Jia", "奇門遁甲", "fortune.casting.qimen:cast"),
26+
"liuren": ("Da Liu Ren", "大六壬", "fortune.casting.liuren:cast"),
27+
"taiyi": ("Tai Yi Shen Shu", "太乙神數", "fortune.casting.taiyi:cast"),
28+
"jyotish": ("Jyotiṣa · Vedic Astrology", "Jyotiṣa(吠陀占星)", "fortune.casting.jyotish:cast"),
2729
}
2830

2931

@@ -35,17 +37,17 @@ def _resolve(spec: str):
3537
def cast(system: str, birth: BirthInput) -> Chart:
3638
if system not in REGISTRY:
3739
raise KeyError(f"unknown system: {system!r}. known: {', '.join(REGISTRY)}")
38-
return _resolve(REGISTRY[system][1])(birth)
40+
return _resolve(REGISTRY[system][2])(birth)
3941

4042

4143
def systems() -> list[dict[str, object]]:
42-
"""[{key, zh, available}] for the UI menu."""
44+
"""[{key, en, zh, available}] for the UI menu. / 給前端選單用。"""
4345
out: list[dict[str, object]] = []
44-
for key, (zh, spec) in REGISTRY.items():
46+
for key, (en, zh, spec) in REGISTRY.items():
4547
try:
4648
_resolve(spec)
4749
ok = True
4850
except Exception: # noqa: BLE001
4951
ok = False
50-
out.append({"key": key, "zh": zh, "available": ok})
52+
out.append({"key": key, "en": en, "zh": zh, "available": ok})
5153
return out

fortune/casting/astrology.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from fortune.engines.astrology import astro
1313
from fortune.schemas import Chart
1414

15-
KEY, ZH, ORB = "astrology", "西洋占星", 6.0
15+
KEY, ZH, EN, ORB = "astrology", "西洋占星", "Western Astrology", 6.0
1616

1717

1818
def cast(birth: BirthInput) -> Chart:
@@ -30,7 +30,7 @@ def cast(birth: BirthInput) -> Chart:
3030
f"・水星{'逆行' if readings.get('mercury_retrograde') == 'yes' else '順行'}"
3131
)
3232
return Chart(
33-
system=KEY, system_zh=ZH, subject=birth.label(), cast_at=birth.dt,
33+
system=KEY, system_en=EN, system_zh=ZH, subject=birth.label(), cast_at=birth.dt,
3434
chart={"planets": chart_rows, "aspects": astro.aspects_for(d, ORB)},
3535
reasoning_chain=astro.reasoning_chain(d, ORB),
3636
readings=readings,

0 commit comments

Comments
 (0)