Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ from a phone.
> [!IMPORTANT]
> This is a read-only match companion. It does not trade, access a Kalshi
> account, or provide betting advice. `position_team` is a manually entered
> preference used only to choose the post-match reaction. Kalshi data comes
> from its public REST API; ESPN data comes from publicly reachable,
> undocumented endpoints that may change or lag behind the broadcast.
> preference: the watcher describes its benefit or pressure around major ESPN
> events, results, and Kalshi moves from a confirmed matching position market,
> but never reads a real account. Kalshi data comes from its public REST API;
> ESPN data comes from publicly reachable, undocumented endpoints that may
> change or lag behind the broadcast.

## Features

- A persistent two-team probability bar with flags and a bottom market ticker.
- Goal, card, substitution, close-miss, match-state, and final-result reactions.
- Three switchable commentary styles: casual co-watching, balanced narration,
and professional play-by-play with ESPN-supplied detail.
- Separate support and position perspectives: routine play follows the team you
support, while major events add position impact and acknowledge conflicts.
- A phone setup page hosted by Stack-chan, with live Chinese/English switching.
- Double-tap head-touch and Power-button shortcuts for the setup QR code.
- Fixture discovery, adaptive polling, hot configuration reload, and quiet hours.
Expand Down Expand Up @@ -78,7 +82,8 @@ not the primary QR flow.
- `tools/` — the watcher, local setup service, macOS TTS server, replay tool,
serial helper, asset generator, and tests. The default HTTP workflow uses the
Python standard library; serial transport additionally requires `pyserial`.
- `config/` — the example watcher configuration and flag-pack definition.
- `config/` — the example watcher configuration, flag-pack definition, and
global ESPN player catalog.
- `docs/` — the [commentary-styles PRD](docs/commentary-styles-prd.md) and
version-specific upgrade notes, including the bilingual
[Matchday MOD 1.5.0 notes](docs/releases/1.5.0.md) and
Expand Down Expand Up @@ -112,6 +117,11 @@ update this watcher checkout and reinstall the Matchday mod. You do not need
to rebuild or reflash the official host firmware or replace the TTS module;
see the [1.4.0 release notes](docs/releases/1.4.0.md).

Support/position-aware wording, position-based market selection, and the global
player catalog are watcher-only updates. An installed 1.5.0 mod can stay in
place. Deploy both the watcher code and `config/espn_player_catalog.json`, then
restart the Python watcher process; the mod and host do not need reflashing.

### 1. Clone and prepare the upstream build environment

```sh
Expand Down Expand Up @@ -277,8 +287,9 @@ falls back to short tone patterns.
3. **Scan and choose.** Open the QR on a phone, choose 中文 or English, select a
match, your team (or Neutral), an optional pregame position (or No
position), and a commentary style, then tap **Start watching**. The
position is only a manual final result reaction preference; no account is
read.
Match Setup enables the Kalshi market for the selected position team; with
No position, it enables the first market in the match pair. The position is
still only a manual preference and no account is read.
4. **Wait for confirmation.** The page first says that it is waiting for the
watcher. The watcher validates the ESPN/Kalshi pairing, atomically updates
the local configuration, hot-reloads, and acknowledges the device. No
Expand Down Expand Up @@ -341,9 +352,20 @@ User-facing text accepts either a legacy string or a localized object:
}
```

The same localized leaf format works for `player_names`, `star_chants`, and
custom goal-signal speech. Missing English names fall back to ESPN's source
name. A legacy string is used verbatim in both modes.
The watcher first consults the global `config/espn_player_catalog.json`, keyed
by stable ESPN athlete IDs such as `espn:362150`. Entries contain formal
Chinese/English names, manually verified casual nicknames, a featured-player
flag, and an optional goal chant. `balanced`, `professional`, and every device
balloon always use the formal name; only casual speech may use a friendly
nickname. Legacy per-match `player_names` and `star_chants` remain supported
and override the catalog, and custom goal-signal speech keeps the same
localized-leaf format.

An unmatched player falls back to ESPN's original name; the watcher neither
transliterates nor invents a nickname. When an ESPN roster first appears or
changes, the watcher logs named and featured coverage plus the raw-name
fallback list so the catalog can be completed before a later match without
blocking commentary.

### Commentary styles

Expand All @@ -370,6 +392,15 @@ Across all three styles, balloons normally stay in the form “time +
player/team + event + score”; only punctuation and a small amount of wording
vary.

The supported team sets the everyday co-watching point of view. Routine shots,
saves, and corners do not force a position aside. Major ESPN events such as
goals, penalties, red cards, and results add whether the declared position
benefits or comes under pressure; aligned support and position are combined,
while a conflict explicitly separates the emotional and position outcomes.
Only a Kalshi market that Match Setup has matched to the selected position and
marked `tracks_position` uses position wording for price moves or suspected
goals. Suspected events still say “if confirmed” and await commentary.

The selected style also applies to match phases and results, Kalshi market
jumps, and suspected-goal alerts. It changes wording only: TTS voice and rate,
sound effects, celebrations, expressions, lights, priorities, and alert
Expand Down
36 changes: 29 additions & 7 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ Kalshi 晋级市场中的概率,跟随 ESPN 比分与文字直播,并通过

> [!IMPORTANT]
> 这是一个只读的比赛陪看工具。它不会交易、不会访问 Kalshi 账户,也不提供投注
> 建议。`position_team` 只是手动填写的偏好,仅用于选择终场反应。Kalshi 数据来自
> 其公开 REST API;ESPN 数据来自可公开访问但未正式文档化的接口,可能变更,也
> 可能落后于电视直播。
> 建议。`position_team` 只是手动填写的偏好:watcher 会在重大 ESPN 事件、赛果以及
> 已确认对应持仓的 Kalshi 市场变化中说明利好或利空,但不会读取真实账户。Kalshi
> 数据来自其公开 REST API;ESPN 数据来自可公开访问但未正式文档化的接口,可能
> 变更,也可能落后于电视直播。

## 功能

- 常驻的双方概率条、球队旗帜与底部市场 ticker。
- 对进球、红黄牌、换人、险情、比赛状态和终场结果做出反应。
- 三档可即时切换的播报语气:朋友陪看的 `casual`、自然播报的 `balanced`,以及
补充 ESPN 明确信息的专业解说 `professional`。
- 支持队与持仓队分开建模:普通攻防站在支持队视角,重大事件再自然补充持仓影响,
两者方向相反时也会明确表达冲突。
- 由 Stack-chan 自己托管的手机设置页,可即时切换中英文。
- 双击头顶触摸条或短按 Power 键唤出设置二维码。
- 自动发现比赛、自适应轮询、配置热更新与免打扰时段。
Expand Down Expand Up @@ -79,7 +82,7 @@ watcher 电脑上的 `:8788/setup` 只是可选的本机管理后备页,不是
- `tools/` — watcher、本地设置服务、macOS TTS 服务、比赛回放、串口辅助工具、
素材生成器与测试。默认 HTTP 流程只用 Python 标准库;串口模式另需
`pyserial`。
- `config/` — watcher 示例配置与旗帜包定义
- `config/` — watcher 示例配置、旗帜包定义与全局 ESPN 球员目录
- `docs/` — [三档播报语气 PRD](docs/commentary-styles-prd.md) 与各版本升级说明,
包括双语的 [Matchday MOD 1.5.0 说明](docs/releases/1.5.0.md)与
[1.4.0 说明](docs/releases/1.4.0.md)。
Expand Down Expand Up @@ -109,6 +112,10 @@ export STACKCHAN_DIR="$HOME/src/stack-chan"
重新安装 Matchday Mod;无需重新构建或烧录官方 host firmware,也无需更换 TTS
模块。详见 [1.4.0 版本说明](docs/releases/1.4.0.md)。

支持/持仓视角文案、按持仓选择市场和全局球员目录属于 watcher-only 更新:已有
1.5.0 Mod 可直接沿用。请同步更新 watcher 代码与
`config/espn_player_catalog.json`,然后重启 Python watcher 进程;无需刷写 Mod 或 host。

### 1. 克隆仓库并准备上游构建环境

```sh
Expand Down Expand Up @@ -262,7 +269,8 @@ curl --request POST --data-binary "say 比赛日准备好了" \
Power 键同样可以开关二维码。
3. **扫码选择比赛。** 用手机打开二维码,选择中文或 English、要看的比赛、支持
球队(也可中立)、可选的赛前持仓(也可“没买”)以及播报语气,再点“开始
看球”。赛前持仓只是手动的终场反应偏好,系统不会读取账户。
看球”。有持仓时,Match Setup 会启用该队对应的 Kalshi 市场;选择“没买”时
默认启用比赛双方列表中的第一个市场。持仓仍只是手动偏好,系统不会读取账户。
4. **等待确认。** 页面会先显示“已提交,等待 watcher”。watcher 会校验 ESPN 与
Kalshi 的双方匹配,原子更新本地配置、热重载并确认设备;无需重启 watcher 或
Stack-chan。
Expand Down Expand Up @@ -319,8 +327,16 @@ watcher 还可以每天主动提醒你扫码选比赛。相关配置为
}
```

`player_names`、`star_chants` 和自定义进球信号语音也支持相同格式。缺少英文名时
会回退到 ESPN 源名称;旧式字符串会在两种语言下原样使用。
watcher 会先查全局 `config/espn_player_catalog.json`。目录以稳定的 ESPN athlete
ID(例如 `espn:362150`)为主键,保存正式中英文名、经人工核实的 casual 昵称、
球星标记和可选进球口号。`balanced`、`professional` 与所有气泡始终使用正式名;
只有 casual 语音可以使用目录中的亲切昵称。旧配置里的 `player_names` 和
`star_chants` 继续有效,并优先于全局目录;自定义进球信号语音也仍支持上述本地化
格式。

无法匹配目录的球员会回退到 ESPN 原名,不音译、不猜昵称。ESPN roster 首次出现
或发生变化后,watcher 会在日志输出目录命中数、球星数和原名回退名单,便于赛前
补齐覆盖,而不会阻断播报。

### 三档播报语气

Expand All @@ -342,6 +358,12 @@ watcher 还可以每天主动提醒你扫码选比赛。相关配置为
三档气泡原则上都保持“时间 + 球员/球队 + 事件 + 比分”,只允许标点和少量措辞
差异。

支持队决定日常陪看视角;普通射门、扑救、角球等攻防事件不会牵强讨论仓位。进球、
点球、红牌和赛果等重大 ESPN 事件才会追加持仓利好/利空;支持与持仓同向时合并成
一句自然反应,方向冲突时分别说明“心里支持谁”和“仓位受益或承压”。只有与所选
持仓队对应且由 Match Setup 标记为 `tracks_position` 的 Kalshi 市场,盘口变化和
疑似进球提醒才会使用持仓措辞;疑似事件仍保留“如果属实”和等待确认。

语气也覆盖比赛阶段和赛果、Kalshi 盘口突变与疑似进球提醒,并且只改变文案:TTS
音色和语速、音效、庆祝动作、表情、灯效、优先级与提醒开关均保持不变。比赛中可从
任一设置页即时切换;新生成的提醒会立即使用新语气,但不会重播 ESPN 旧事件,也
Expand Down
161 changes: 161 additions & 0 deletions config/espn_player_catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
{
"schema_version": 1,
"players": {
"espn:45843": {
"aliases": ["Lionel Messi", "L. Messi"],
"display_name": {"zh": "梅西", "en": "Lionel Messi"},
"featured": true,
"goal_chant": {"zh": "{name}破门了!", "en": "{name} scores!"}
},
"espn:134947": {
"aliases": ["Kevin De Bruyne", "K. De Bruyne"],
"display_name": {"zh": "德布劳内", "en": "Kevin De Bruyne"},
"casual_name": {"zh": "丁丁", "en": "De Bruyne"},
"featured": true,
"goal_chant": {"zh": "{name}破门了!", "en": "{name} scores!"}
},
"espn:139437": {
"aliases": ["Romelu Lukaku", "R. Lukaku"],
"display_name": {"zh": "卢卡库", "en": "Romelu Lukaku"},
"featured": true
},
"espn:142200": {
"aliases": ["Harry Kane", "H. Kane"],
"display_name": {"zh": "凯恩", "en": "Harry Kane"},
"casual_name": {"zh": "飓风凯恩", "en": "Hurricane Kane"},
"featured": true,
"goal_chant": {"zh": "{name}破门了!", "en": "{name} scores!"}
},
"espn:149981": {
"aliases": ["Granit Xhaka", "G. Xhaka"],
"display_name": {"zh": "扎卡", "en": "Granit Xhaka"},
"featured": true
},
"espn:158626": {
"aliases": ["Emiliano Martínez", "E. Martínez", "Emiliano Martinez"],
"display_name": {"zh": "埃米利亚诺·马丁内斯", "en": "Emiliano Martinez"},
"featured": true
},
"espn:159443": {
"aliases": ["Jordan Pickford", "J. Pickford"],
"display_name": {"zh": "皮克福德", "en": "Jordan Pickford"},
"featured": true
},
"espn:174980": {
"aliases": ["Leandro Trossard", "L. Trossard"],
"display_name": {"zh": "特罗萨德", "en": "Leandro Trossard"},
"featured": true
},
"espn:188230": {
"aliases": ["Alexander Sørloth", "A. Sørloth", "Alexander Sorloth"],
"display_name": {"zh": "瑟洛特", "en": "Alexander Sorloth"},
"featured": true
},
"espn:193610": {
"aliases": ["Ørjan Nyland", "Ø. Nyland", "Orjan Nyland"],
"display_name": {"zh": "尼兰", "en": "Orjan Nyland"},
"featured": false
},
"espn:203669": {
"aliases": ["Martin Ødegaard", "M. Ødegaard", "Martin Odegaard"],
"display_name": {"zh": "厄德高", "en": "Martin Odegaard"},
"featured": true
},
"espn:214562": {
"aliases": ["Manuel Akanji", "M. Akanji"],
"display_name": {"zh": "阿坎吉", "en": "Manuel Akanji"},
"featured": true
},
"espn:219713": {
"aliases": ["Lautaro Martínez", "L. Martínez", "Lautaro Martinez"],
"display_name": {"zh": "劳塔罗·马丁内斯", "en": "Lautaro Martinez"},
"featured": true
},
"espn:227765": {
"aliases": ["Dani Olmo", "D. Olmo"],
"display_name": {"zh": "奥尔莫", "en": "Dani Olmo"},
"featured": true
},
"espn:229744": {
"aliases": ["Ousmane Dembélé", "O. Dembélé", "Ousmane Dembele"],
"display_name": {"zh": "登贝莱", "en": "Ousmane Dembele"},
"featured": true
},
"espn:230945": {
"aliases": ["Marcus Rashford", "M. Rashford"],
"display_name": {"zh": "拉什福德", "en": "Marcus Rashford"},
"casual_name": {"zh": "拉师傅", "en": "Rashford"},
"featured": true
},
"espn:231388": {
"aliases": ["Kylian Mbappé", "K. Mbappé", "Kylian Mbappe"],
"display_name": {"zh": "姆巴佩", "en": "Kylian Mbappe"},
"casual_name": {"zh": "姆总", "en": "Mbappe"},
"featured": true,
"goal_chant": {"zh": "{name}破门了!", "en": "{name} scores!"}
},
"espn:231828": {
"aliases": ["Rodri"],
"display_name": {"zh": "罗德里", "en": "Rodri"},
"featured": true
},
"espn:238262": {
"aliases": ["Declan Rice", "D. Rice"],
"display_name": {"zh": "赖斯", "en": "Declan Rice"},
"featured": true
},
"espn:250465": {
"aliases": ["Pedri"],
"display_name": {"zh": "佩德里", "en": "Pedri"},
"featured": true
},
"espn:253989": {
"aliases": ["Erling Haaland", "E. Haaland"],
"display_name": {"zh": "哈兰德", "en": "Erling Haaland"},
"casual_name": {"zh": "哈宝", "en": "Haaland"},
"featured": true,
"goal_chant": {"zh": "{name}破门了!", "en": "{name} scores!"}
},
"espn:277206": {
"aliases": ["Julián Álvarez", "J. Álvarez", "Julian Alvarez"],
"display_name": {"zh": "阿尔瓦雷斯", "en": "Julian Alvarez"},
"casual_name": {"zh": "小蜘蛛", "en": "Julian Alvarez"},
"featured": true,
"goal_chant": {"zh": "{name}破门了!", "en": "{name} scores!"}
},
"espn:280555": {
"aliases": ["Bukayo Saka", "B. Saka"],
"display_name": {"zh": "萨卡", "en": "Bukayo Saka"},
"casual_name": {"zh": "小辣椒萨卡", "en": "Little Chilli Saka"},
"featured": true,
"goal_chant": {"zh": "{name}破门了!", "en": "{name} scores!"}
},
"espn:283672": {
"aliases": ["Jérémy Doku", "J. Doku", "Jeremy Doku"],
"display_name": {"zh": "多库", "en": "Jeremy Doku"},
"featured": true
},
"espn:291281": {
"aliases": ["Jude Bellingham", "J. Bellingham"],
"display_name": {"zh": "贝林厄姆", "en": "Jude Bellingham"},
"featured": true
},
"espn:312146": {
"aliases": ["Nico Williams", "N. Williams"],
"display_name": {"zh": "尼科·威廉斯", "en": "Nico Williams"},
"featured": true
},
"espn:319368": {
"aliases": ["Antonio Nusa", "A. Nusa"],
"display_name": {"zh": "努萨", "en": "Antonio Nusa"},
"featured": true
},
"espn:362150": {
"aliases": ["Lamine Yamal", "L. Yamal"],
"display_name": {"zh": "亚马尔", "en": "Lamine Yamal"},
"casual_name": {"zh": "小孩哥", "en": "Yamal"},
"featured": true,
"goal_chant": {"zh": "{name}破门了!", "en": "{name} scores!"}
}
}
}
Loading
Loading