asterism-games 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +321 -3
- data/VERSION +1 -1
- data/bin/asterism-games +8 -0
- data/lib/asterism_games/ai.rb +64 -0
- data/lib/asterism_games/commentate/ai_comment.rb +633 -0
- data/lib/asterism_games/commentate/assets/TILES_LICENSE.txt +42 -0
- data/lib/asterism_games/commentate/assets/commentate.css +100 -0
- data/lib/asterism_games/commentate/assets/commentate.js +735 -0
- data/lib/asterism_games/commentate/assets/index.html +26 -0
- data/lib/asterism_games/commentate/assets/tiles_hand.bmp +0 -0
- data/lib/asterism_games/commentate/assets/tiles_river.bmp +0 -0
- data/lib/asterism_games/commentate/mahjong.rb +400 -0
- data/lib/asterism_games/commentate/mahjong_events.rb +320 -0
- data/lib/asterism_games/commentate/mahjong_phrases.rb +535 -0
- data/lib/asterism_games/commentate/mahjong_voice.yml +93 -0
- data/lib/asterism_games/commentate/server.rb +204 -0
- data/lib/asterism_games/commentate/voice.rb +394 -0
- data/lib/asterism_games/commentate/voicevox.rb +110 -0
- data/lib/asterism_games/commentate.rb +223 -0
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d10ded9e6550829d2169bb7be24ba662f11d69cec3eaf0fe676923104aa44727
|
|
4
|
+
data.tar.gz: '039b615f91c53f50c6bb10500b4e9516c7893bd213af57d1f7823c2bc67e84f5'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2571c2374914e2f9bb3d299e0482cd3f2f43b9c6ebe7fb4f5423b5c37c121cd5b06c06eb058ee91ecb5b05ddd0d6db22ce89e9b8b06eaf40cd3ab6ac6081f65
|
|
7
|
+
data.tar.gz: 260165d7df98b105ec8070f0c850d80f783b220cf4cf819c3a17de189c0ed7b0ccb78f43dc7ce29fe8f31f08aa4d4793c0eca1a2e600a265e52c61c67833852e
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,78 @@
|
|
|
3
3
|
Versions before 0.11.1 are noted in the README where each feature is
|
|
4
4
|
described ("games 0.10.0" and the like).
|
|
5
5
|
|
|
6
|
+
## 0.19.1
|
|
7
|
+
|
|
8
|
+
- The commentary tells where to get VOICEVOX when it finds no engine:
|
|
9
|
+
the terminal says it is a free app installed and started apart from the
|
|
10
|
+
gem (with its home page), the page's bar says the same in Japanese, and
|
|
11
|
+
`commentate --help` and the README's "The voice" say it up front.
|
|
12
|
+
|
|
13
|
+
## 0.19.0
|
|
14
|
+
|
|
15
|
+
- **Commentary** (README "Commentary"): `asterism-games commentate
|
|
16
|
+
mahjong` watches a table from its spectate key (0.18.0) and shows it in
|
|
17
|
+
the browser: a small web server on `127.0.0.1` (HTTP and Server-Sent
|
|
18
|
+
Events by hand on `socket`, no new gem; port 7470 or the next free one),
|
|
19
|
+
a page that draws the table in a canvas with the hand-drawn tile sheets
|
|
20
|
+
of Family mruby's mahjong app, and commentary in Japanese made by rules
|
|
21
|
+
from what changed between two views (a round's start, the oorasu,
|
|
22
|
+
riichi and its waits, calls, a dora or red five discarded, a new dora,
|
|
23
|
+
the wall at 10, a tenpai, a win with its yaku and points, a draw, the
|
|
24
|
+
lead changing hands, the end), shown as captions and read by the
|
|
25
|
+
browser's voice (Web Speech API) in a queue that drops the old small
|
|
26
|
+
lines first. Space pauses, M silences, H hides the hands; `--public`
|
|
27
|
+
keeps the hands from the page altogether; `--json` gives the lines on
|
|
28
|
+
standard output; `--table`, `--namespace` (a room's tables), `--port`,
|
|
29
|
+
`--no-open`. It only reads (never joins or calls a table) and goes on
|
|
30
|
+
when the host leaves and comes back.
|
|
31
|
+
- **The voice** (README "Commentary", "The voice"): the commentary speaks
|
|
32
|
+
with VOICEVOX on this machine (`--voicevox URL`, `VOICEVOX_URL`, default
|
|
33
|
+
`http://127.0.0.1:50021`), by default as ずんだもん (`--speaker`,
|
|
34
|
+
`--style`; the numbers looked up by name in `/speakers`). The fixed
|
|
35
|
+
phrases (riichi, the calls, ron and tsumo, the rounds, 満貫 to 役満, ...)
|
|
36
|
+
are WAV files made by the user's own engine from the gem's list
|
|
37
|
+
(`commentate/mahjong_voice.yml`) into
|
|
38
|
+
`$XDG_CACHE_HOME/asterism-games/voices/<speaker>-<style>/`, named by the
|
|
39
|
+
phrasing's digest (`asterism-games commentate voices --make`, and the
|
|
40
|
+
missing ones at the start; `voices --list` lists the characters). The
|
|
41
|
+
rest of each line is synthesised as it comes and served from the
|
|
42
|
+
command's web server, kept in a bounded cache. The page says it all in
|
|
43
|
+
one queue (the fixed phrase, then the line; captions as the voice
|
|
44
|
+
starts; the old small lines dropped first) and shows the credit
|
|
45
|
+
`VOICEVOX:ずんだもん`. ずんだもん's lines are in her tone (〜なのだ),
|
|
46
|
+
kept as a tone of the phrasings (`MahjongPhrases::TONES`). Without an
|
|
47
|
+
engine the browser's voice reads as before. The engine gets the speech
|
|
48
|
+
only (サンマン, 30フ 2ハン), with the words it misreads written out
|
|
49
|
+
(流局, 起家, 跳満: `MahjongPhrases::SPEECH_READ`). No sound is in the gem;
|
|
50
|
+
`net/http` only, no new gem.
|
|
51
|
+
- **The AI's one-liners** (README "Commentary", "The AI's one-liners"):
|
|
52
|
+
at a riichi, a win, a draw, the lead changing hands, the oorasu, the end
|
|
53
|
+
and after a quiet minute, Claude (`claude-haiku-5-5` by default,
|
|
54
|
+
`--ai-model`) adds a short line after the fixed commentary, in the
|
|
55
|
+
voice's tone, marked AI on the page. At most one call every 20 s (8 s
|
|
56
|
+
for a scene bigger than the last one asked about: a win after a riichi),
|
|
57
|
+
one at a time, only while a page is open. A quiet minute is about one
|
|
58
|
+
topic the command picks (the lead, a dama tenpai with the hands shown,
|
|
59
|
+
the dora, the wall, the last discard). Thinking off, effort low,
|
|
60
|
+
`max_tokens` 200; a cut answer is dropped with its stop reason in the
|
|
61
|
+
log. The fixed part of the ask (the role,
|
|
62
|
+
the rules in short, the tone, the length, whether the hands may be
|
|
63
|
+
told) is cached; the table now and the lines that just came follow.
|
|
64
|
+
With `--public` or a page's H the AI gets no hand. An answer whose round
|
|
65
|
+
or phase is over, or that comes after 15 s, is dropped; on the page an
|
|
66
|
+
AI's line comes after the fixed ones and goes first when the voice
|
|
67
|
+
falls behind. The money is counted from the tokens (cache writes and
|
|
68
|
+
reads too) in `$XDG_STATE_HOME/asterism-games/commentary_ai_usage.json`
|
|
69
|
+
(`AI::Budget`), shown in the bar, and capped at `--ai-daily-usd`
|
|
70
|
+
(default 3 USD a day). `ANTHROPIC_API_KEY`; the anthropic gem stays
|
|
71
|
+
outside the gem's dependencies (loaded only with a key);
|
|
72
|
+
`ASTERISM_GAMES_AI_FAKE=1` tries it without one. `--ai off` turns it off.
|
|
73
|
+
- The gem holds the page (HTML, JS, CSS) and the two tile sheets
|
|
74
|
+
`tiles_hand.bmp` and `tiles_river.bmp`, copied byte for byte from
|
|
75
|
+
fmruby-core; in the gem they are MIT (`TILES_LICENSE.txt`). The gem's
|
|
76
|
+
test checks they are there and unchanged.
|
|
77
|
+
|
|
6
78
|
## 0.18.0
|
|
7
79
|
|
|
8
80
|
- **Spectate** (README "Spectate"): the host puts a table's spectate view
|
data/README.md
CHANGED
|
@@ -60,9 +60,10 @@ the console ("Permanent tables"; the server's setup: docs/install.md).
|
|
|
60
60
|
| Path | What |
|
|
61
61
|
|---|---|
|
|
62
62
|
| `asterism-games.gemspec` | the gem: what it holds and needs ("The gem and the image") |
|
|
63
|
-
| `bin/asterism-games` | the host (loads the framework and the games); `status`; `play <game>`, a table in this terminal ("Playing in a terminal"); `help` |
|
|
63
|
+
| `bin/asterism-games` | the host (loads the framework and the games); `status`; `play <game>`, a table in this terminal ("Playing in a terminal"); `commentate <game>`, a table in the browser with commentary ("Commentary"); `help` |
|
|
64
64
|
| `bin/mahjong-bots` | players without screens, for trying a mahjong table (one process each; not in the gem) |
|
|
65
65
|
| `lib/asterism_games/play.rb`, `lib/asterism_games/play/` | the terminal clients ("Playing in a terminal"): the options, `othello.rb`, `mahjong.rb` with `mahjong_text.rb` (tiles as text, widths, reading the input) and `mahjong_screen.rb` (the screen); they need neither the framework nor riichi_engine |
|
|
66
|
+
| `lib/asterism_games/commentate.rb`, `lib/asterism_games/commentate/` | `asterism-games commentate mahjong` ("Commentary"): the events and their words (`mahjong_events.rb`, `mahjong_phrases.rb`), the hub and the runner (`mahjong.rb`), the web server (`server.rb`), the voice (`voicevox.rb`, VOICEVOX's HTTP; `voice.rb`, the fixed phrases' files and the lines' sound; `mahjong_voice.yml`, the fixed phrases), the page and the tile sheets (`assets/`) |
|
|
66
67
|
| `lib/asterism_games.rb` | the framework, without any game |
|
|
67
68
|
| `lib/asterism_games/game.rb` | the base of a game: the registry, the members and tokens, who joins in a room, the views, the time, saving ("Writing a game") |
|
|
68
69
|
| `lib/asterism_games/remote.rb` | the base of the exposed object: the methods and their numbers of arguments, the caller (`from:`), `join` / `mine` / `leave` |
|
|
@@ -89,7 +90,9 @@ the console ("Permanent tables"; the server's setup: docs/install.md).
|
|
|
89
90
|
game host of your own: the framework, the two sample games that need
|
|
90
91
|
nothing else (othello, the drawing board), `bin/asterism-games` (with
|
|
91
92
|
`play`, the terminal clients of othello and mahjong: a mahjong table needs
|
|
92
|
-
riichi_engine, its client does not
|
|
93
|
+
riichi_engine, its client does not; and `commentate`, a mahjong table watched
|
|
94
|
+
in the browser with commentary, with the tile sheets of Family mruby's
|
|
95
|
+
mahjong app). It depends on
|
|
93
96
|
the asterism gem only (0.7.1 or later in
|
|
94
97
|
0.x; what came after 0.7.1, such as `@status` and `on_app_join`, is used
|
|
95
98
|
when there). Not in it: mahjong (`app/`, it needs the riichi_engine fork
|
|
@@ -1463,6 +1466,290 @@ Typed (in a pipe or a file each line; on a terminal after `:`):
|
|
|
1463
1466
|
The host's answers are shown in the table app's words (`今はできない`,
|
|
1464
1467
|
`卓の主だけができる` ...).
|
|
1465
1468
|
|
|
1469
|
+
## Commentary
|
|
1470
|
+
|
|
1471
|
+
(games 0.19.0) `asterism-games commentate mahjong` watches a mahjong table
|
|
1472
|
+
from outside, the way a sports broadcast does: it reads the table's
|
|
1473
|
+
spectate key ("Spectate", with the four hands), draws the table in a
|
|
1474
|
+
browser with the tiles of Family mruby's mahjong app, and comments on what
|
|
1475
|
+
happens in Japanese, as captions under the table and in a voice: a
|
|
1476
|
+
VOICEVOX character's (ずんだもん by default) when a VOICEVOX engine runs on
|
|
1477
|
+
this machine, else the browser's own (the Web Speech API; "The voice"
|
|
1478
|
+
below). It only reads: it never joins the table or calls it. CRuby and
|
|
1479
|
+
the asterism gem only; it runs on WSL and on macOS.
|
|
1480
|
+
|
|
1481
|
+
```
|
|
1482
|
+
asterism-games commentate mahjong --router tcp/127.0.0.1:7447 # a host on this machine or the home network
|
|
1483
|
+
asterism-games commentate mahjong --credentials viewer.tar # a console's network (a viewer's or a spectator's .tar)
|
|
1484
|
+
asterism-games commentate mahjong --credentials viewer.tar --namespace room/01 # a room's tables
|
|
1485
|
+
asterism-games commentate mahjong --public # for a stream: the hands never reach the page
|
|
1486
|
+
asterism-games commentate mahjong --json | tee commentary.jsonl # the lines for another tool too
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
It starts a small web server on `127.0.0.1` (port 7470, the next free one
|
|
1490
|
+
when it is taken), prints the address and opens it in the browser (macOS
|
|
1491
|
+
`open`; WSL `wslview`, else Windows' `start`; elsewhere `xdg-open`).
|
|
1492
|
+
|
|
1493
|
+
| Option | |
|
|
1494
|
+
|---|---|
|
|
1495
|
+
| `--router LOCATOR`, `--credentials FILE` | as `play` (with neither, `ASTERISM_ROUTER` / `ASTERISM_CREDENTIALS`, all or nothing; with nothing, `tcp/127.0.0.1:7447`) |
|
|
1496
|
+
| `--namespace PREFIX` | a room's tables: `room/01` shows `room01-<table>` (the spectate keys are outside the room, so the connection has no namespace) |
|
|
1497
|
+
| `--table TABLE` | the table to show first: `mahjong1`, `games/mahjong1` or `games/spectate/mahjong1`; without it, every table that puts its spectate key is offered and the first one shown |
|
|
1498
|
+
| `--node NODE` | the node ID (default `commentate-<host>-<pid>`, or the `.tar`'s name) |
|
|
1499
|
+
| `--port N` | the page's port (default 7470) |
|
|
1500
|
+
| `--public` | hide the hands: the views sent to the page lose `hand` and `drawn`, and no line tells a hand |
|
|
1501
|
+
| `--no-open` | do not open the browser |
|
|
1502
|
+
| `--json` | each line of commentary as a JSON line on standard output (`at`, `table`, `type`, `prio`, `text`; an AI's line has `type` `ai` and `ai: true`) |
|
|
1503
|
+
| `--voicevox URL` | the VOICEVOX engine (default `VOICEVOX_URL`, else `http://127.0.0.1:50021`) |
|
|
1504
|
+
| `--speaker NAME`, `--style NAME` | the VOICEVOX character and its style (default `ずんだもん`, `ノーマル`) |
|
|
1505
|
+
| `--ai on\|off` | the AI's one-liners (default `on`; they need `ANTHROPIC_API_KEY` and the anthropic gem; "The AI's one-liners") |
|
|
1506
|
+
| `--ai-model MODEL` | the model they ask (default `claude-haiku-5-5`) |
|
|
1507
|
+
| `--ai-daily-usd USD` | what they may cost a day (default 3); then the fixed commentary alone |
|
|
1508
|
+
|
|
1509
|
+
- **The role.** The spectate keys are `asterism/<host>/spectate/<table>/state`.
|
|
1510
|
+
On a console's network a `viewer` (Read `asterism/**`) or a `spectator`
|
|
1511
|
+
(Read `asterism/*/spectate` and `asterism/*/spectate/**`) reads them; the
|
|
1512
|
+
players' role `games` does not. On a plain zenohd anyone does.
|
|
1513
|
+
- **The page.** A bar of states on top (the table, the voice, the delay the
|
|
1514
|
+
host puts on the table, a host that is not there), the table in a
|
|
1515
|
+
canvas (the round, honba, riichi sticks, the wall and the dora
|
|
1516
|
+
indicators; for each seat its wind, name, points, riichi, river with the
|
|
1517
|
+
riichi tile sideways and the called tiles pale, hand and drawn tile,
|
|
1518
|
+
melds; the seat to act framed), the captions below with the time of
|
|
1519
|
+
each, the newest at the bottom. Two seats to a row in a wide window, one
|
|
1520
|
+
in a narrow one (a phone). Keys: **Space** pauses (what comes meanwhile
|
|
1521
|
+
is held and shown when it goes on), **M** turns the voice off and on,
|
|
1522
|
+
**H** hides the hands on that page (backs instead of the tiles, and the
|
|
1523
|
+
lines that tell a hand left out). A table that several hosts put is
|
|
1524
|
+
chosen in the bar.
|
|
1525
|
+
- **The AI's one-liners.** Claude's short lines at the big moments,
|
|
1526
|
+
marked AI, after the fixed ones; the bar shows the model and today's
|
|
1527
|
+
money. "The AI's one-liners" below.
|
|
1528
|
+
- **The voice.** One queue on the page says the lines in turn, each
|
|
1529
|
+
caption coming up as its voice starts; when the voice falls behind, the
|
|
1530
|
+
old small lines are dropped (their captions still come up) and a win or
|
|
1531
|
+
a big moment is said next. With VOICEVOX, a line plays its fixed phrase
|
|
1532
|
+
first (リーチ, ポン, ロン, the round's name ...) and then the rest of it,
|
|
1533
|
+
synthesised meanwhile; without, the browser reads the lines with a
|
|
1534
|
+
Japanese voice when it has one (the bar says which voice, or that it has
|
|
1535
|
+
none). Browsers play sound only after the page has been clicked once:
|
|
1536
|
+
the page says so until then. "The voice" below.
|
|
1537
|
+
- **The commentary.** Made by rules from the difference between one view
|
|
1538
|
+
and the one before (pure functions, `commentate/mahjong_events.rb` and
|
|
1539
|
+
`mahjong_phrases.rb`, so the host can take them over later), each event
|
|
1540
|
+
in two to three phrasings chosen at random, the big moments in strong
|
|
1541
|
+
ones: the first look at the table, a round's start (the dealer and the
|
|
1542
|
+
dora; the game's first; the oorasu, and the oorasu going on), a discard
|
|
1543
|
+
of a dora or a red five, a call (チー, ポン, the kans), a new dora, the
|
|
1544
|
+
wall at 10, riichi with its waits, a tenpai (from the hand, once a round
|
|
1545
|
+
for each seat), a win with its yaku and points (満貫 and over: strong),
|
|
1546
|
+
a draw with who was tenpai, an abortive draw, the lead changing hands,
|
|
1547
|
+
the end with the ranks. The tiles and the yaku are read as a player
|
|
1548
|
+
says them (サンマン, リーチ, タンヤオ). With the hands hidden (`--public`,
|
|
1549
|
+
H) a tenpai is not said and a riichi is said without its waits.
|
|
1550
|
+
- **It does not stop.** A host that leaves (`on_leave`, or no put for 15 s)
|
|
1551
|
+
is shown as gone and comes back with its next put; a new round or a new
|
|
1552
|
+
game goes on as before. The delay of the table is the host's
|
|
1553
|
+
("Spectators"); the page shows it and adds none.
|
|
1554
|
+
- **The web server.** HTTP by hand on the standard library's `socket`
|
|
1555
|
+
(no gem): the page, its script and style, the two tile sheets and
|
|
1556
|
+
`/events`, a stream of Server-Sent Events (`status`, `state`, `line`)
|
|
1557
|
+
that pushes the table and the commentary. It listens on `127.0.0.1`
|
|
1558
|
+
only, answers GET (and HEAD) on those paths only, and refuses a request
|
|
1559
|
+
whose Host is not `127.0.0.1` or `localhost`. The page loads nothing from
|
|
1560
|
+
elsewhere.
|
|
1561
|
+
- **On a stream (OBS).** Add a Browser Source with the address
|
|
1562
|
+
(`http://127.0.0.1:7470/`), 1280 x 720 or larger. `?obs=1` leaves out the
|
|
1563
|
+
bar and the keys' line and makes the background transparent around the
|
|
1564
|
+
captions; `?hands=0` hides the hands on that page, `?voice=0` silences
|
|
1565
|
+
it, `?table=games/mahjong1` picks the table. Start the command with
|
|
1566
|
+
`--public` when the stream must never show a hand. OBS's browser does not
|
|
1567
|
+
always play the Web Speech voice into the stream: for the voice, capture
|
|
1568
|
+
a browser window and the desktop's sound instead.
|
|
1569
|
+
- **The tiles.** `lib/asterism_games/commentate/assets/tiles_hand.bmp` and
|
|
1570
|
+
`tiles_river.bmp` are the hand-drawn sheets of Family mruby's mahjong app
|
|
1571
|
+
(fmruby-core's `flash/app/game/mahjong/`), copied byte for byte; the
|
|
1572
|
+
browser reads the BMPs as they are and only makes their transparent
|
|
1573
|
+
colour (magenta) transparent when it draws them. In this gem they are
|
|
1574
|
+
under the MIT License like the rest of it, by the author's decision
|
|
1575
|
+
(`TILES_LICENSE.txt` next to them).
|
|
1576
|
+
|
|
1577
|
+
### The voice
|
|
1578
|
+
|
|
1579
|
+
(games 0.19.0) The commentary speaks with VOICEVOX,
|
|
1580
|
+
a free Japanese speech synthesiser, when its engine runs on this machine;
|
|
1581
|
+
the default character is ずんだもん (style ノーマル). Nothing of its voice is
|
|
1582
|
+
in this gem: the gem holds the words (`commentate/mahjong_voice.yml`) and
|
|
1583
|
+
the code that has your VOICEVOX say them. **VOICEVOX is a separate app
|
|
1584
|
+
you install and start yourself** (https://voicevox.hiroshiba.jp/, free);
|
|
1585
|
+
without it the commentary still runs, with the browser's voice. When it
|
|
1586
|
+
finds no engine, the command says so on the terminal, with where to get
|
|
1587
|
+
VOICEVOX, and so does the page's bar.
|
|
1588
|
+
|
|
1589
|
+
- **Finding the engine.** At its start the command asks
|
|
1590
|
+
`GET /version` of `--voicevox URL` (default `VOICEVOX_URL`, else
|
|
1591
|
+
`http://127.0.0.1:50021`). When nothing answers, the browser's voice
|
|
1592
|
+
reads, as before, the terminal says where to get VOICEVOX, and the
|
|
1593
|
+
page's bar says 「VOICEVOX が見つからない (無料のアプリ VOICEVOX を入れて
|
|
1594
|
+
起動すると、ずんだもんの声になる): ブラウザの声で読む」.
|
|
1595
|
+
- **The fixed phrases** (リーチ, チー, ポン, カン, ロン, ツモ, the rounds'
|
|
1596
|
+
names, 流局, 途中流局, 満貫 to 役満, オーラス, 逆転, 終局, 対局開始, and
|
|
1597
|
+
「実況を始めます」 when the page is clicked) are WAV files, played at
|
|
1598
|
+
once. The command makes the missing ones at its start (in the
|
|
1599
|
+
background: a phrase not made yet is skipped), or
|
|
1600
|
+
`asterism-games commentate voices --make` makes them all now (about 30
|
|
1601
|
+
files, half a minute on a CPU engine). They go to
|
|
1602
|
+
`$XDG_CACHE_HOME/asterism-games/voices/<speaker>-<style>/`
|
|
1603
|
+
(`~/.cache/asterism-games/voices/ずんだもん-ノーマル/` by default) with a
|
|
1604
|
+
`CREDITS.txt`; each is named by the digest of its phrasing, so a changed
|
|
1605
|
+
list makes the changed ones again and removes the old. **The directory
|
|
1606
|
+
can be deleted at any time**: it is made again.
|
|
1607
|
+
- **The rest of a line** is synthesised as the line comes (the engine's
|
|
1608
|
+
`/audio_query` and `/synthesis`) while its fixed phrase plays, and
|
|
1609
|
+
served to the page from the command's own web server at a temporary
|
|
1610
|
+
address (`/voice/<digest>.wav`; only lines the command made, never a
|
|
1611
|
+
text a page sends). A line that begins with its fixed phrase's word
|
|
1612
|
+
leaves it out (ロン! then 「CPU1、断么九 …」). The same line is
|
|
1613
|
+
synthesised once while it is kept (200 lines and 64 MB at most, the
|
|
1614
|
+
least recently used dropped first). A line the engine fails on is read
|
|
1615
|
+
by the browser's voice.
|
|
1616
|
+
- **What the engine reads** is always a line's speech, never its caption:
|
|
1617
|
+
the tiles, the yaku and the points as a player says them (サンマン,
|
|
1618
|
+
リーチ, 30フ 2ハン), and the words VOICEVOX would read wrongly written
|
|
1619
|
+
out (流局 リューキョク, 起家 チーチャ, 跳満 ハネマン;
|
|
1620
|
+
`MahjongPhrases::SPEECH_READ`, used for the fixed phrases too). No user
|
|
1621
|
+
dictionary is needed.
|
|
1622
|
+
- **Choosing a character.** `asterism-games commentate voices --list`
|
|
1623
|
+
lists the engine's characters and their styles; `--speaker NAME
|
|
1624
|
+
--style NAME` picks one by name (the engine's numbers are looked up,
|
|
1625
|
+
never assumed; a name that is not there is answered with the ones that
|
|
1626
|
+
are). ずんだもん speaks in her own tone: the lines and the fixed phrases
|
|
1627
|
+
end in 〜なのだ (「リーチなのだ!」「満貫、8000点なのだ!」); any other
|
|
1628
|
+
character speaks plainly. The tones are kept with the phrasings
|
|
1629
|
+
(`MahjongPhrases::TONES`), with a note on how to ask an AI to speak so.
|
|
1630
|
+
- **The page** plays the sound itself (never the PC with `paplay` or
|
|
1631
|
+
`afplay`), so a Browser Source in OBS carries it. **M** cuts the voice
|
|
1632
|
+
(the captions go on). When VOICEVOX speaks, the page shows the credit
|
|
1633
|
+
in its corner: `VOICEVOX:ずんだもん`.
|
|
1634
|
+
|
|
1635
|
+
Running VOICEVOX:
|
|
1636
|
+
|
|
1637
|
+
```
|
|
1638
|
+
# Windows (and WSL): install the VOICEVOX app on Windows and start it (its engine
|
|
1639
|
+
# listens on 127.0.0.1:50021). From WSL with networkingMode=mirrored (.wslconfig)
|
|
1640
|
+
# 127.0.0.1 reaches it; otherwise pass Windows' address:
|
|
1641
|
+
asterism-games commentate mahjong --voicevox http://$(ip route show default | awk '{print $3}'):50021
|
|
1642
|
+
|
|
1643
|
+
# macOS: install the VOICEVOX app and start it; the default URL reaches it.
|
|
1644
|
+
|
|
1645
|
+
# docker (Linux, WSL, macOS): the engine alone, CPU version
|
|
1646
|
+
docker run -d --name voicevox -p 127.0.0.1:50021:50021 voicevox/voicevox_engine:cpu-0.25.0
|
|
1647
|
+
asterism-games commentate voices --list
|
|
1648
|
+
asterism-games commentate voices --make # optional: the commentary makes them at its start too
|
|
1649
|
+
asterism-games commentate mahjong --speaker 四国めたん --style ノーマル
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
The engine takes a gigabyte or two of memory; one line of commentary
|
|
1653
|
+
takes it about a second or two on a laptop's CPU.
|
|
1654
|
+
|
|
1655
|
+
**VOICEVOX's terms, in short** (read the current terms of VOICEVOX and of
|
|
1656
|
+
your character before you publish anything; they can change):
|
|
1657
|
+
|
|
1658
|
+
- The software and the voices it makes may be used free of charge,
|
|
1659
|
+
commercially or not, **with a credit** naming the character, such as
|
|
1660
|
+
`VOICEVOX:ずんだもん`. Each character has terms of its own on top of
|
|
1661
|
+
VOICEVOX's (ずんだもん's: a credit where it can be found, in a video's
|
|
1662
|
+
description or on screen; no use against public order, for political or
|
|
1663
|
+
religious campaigns, for deception, and the like).
|
|
1664
|
+
- **When you stream or upload the commentary, put the credit in the
|
|
1665
|
+
description too** (the page shows it in its corner).
|
|
1666
|
+
- Whoever you give generated voices to must follow the same terms, so
|
|
1667
|
+
they cannot be mixed into an MIT package. That is why this gem ships no
|
|
1668
|
+
sound: each user makes the files with their own VOICEVOX, and they stay
|
|
1669
|
+
on that machine.
|
|
1670
|
+
|
|
1671
|
+
### The AI's one-liners
|
|
1672
|
+
|
|
1673
|
+
(games 0.19.0) At the big moments of a table Claude adds a short line of
|
|
1674
|
+
its own after the fixed commentary, in the same voice and tone (ずんだもん's
|
|
1675
|
+
〜なのだ with her voice), marked **AI** in the captions:
|
|
1676
|
+
「この待ち、山にまだ残ってるのだ。勝負はここからなのだ!」. It is also a
|
|
1677
|
+
small demonstration of an AI watching a table on the network and telling
|
|
1678
|
+
what it sees.
|
|
1679
|
+
|
|
1680
|
+
```
|
|
1681
|
+
export ANTHROPIC_API_KEY=sk-ant-... # your own key
|
|
1682
|
+
gem install anthropic # not a dependency of this gem
|
|
1683
|
+
asterism-games commentate mahjong # Claude Haiku, at most 3 USD a day
|
|
1684
|
+
asterism-games commentate mahjong --ai-model claude-sonnet-5-5 --ai-daily-usd 1
|
|
1685
|
+
asterism-games commentate mahjong --ai off # the fixed commentary alone
|
|
1686
|
+
```
|
|
1687
|
+
|
|
1688
|
+
- **The key and the gem.** The key is `ANTHROPIC_API_KEY`; the anthropic
|
|
1689
|
+
gem is loaded only when it is set, and is not a dependency of this gem.
|
|
1690
|
+
Without a key the page's bar says 「AI: off (キーなし)」; with a key and
|
|
1691
|
+
no gem, 「AI: off (anthropic の gem なし)」 and the command says to
|
|
1692
|
+
`gem install anthropic`. `--ai off` turns them off.
|
|
1693
|
+
- **The model.** `claude-haiku-5-5` by default (`--ai-model`), asked with
|
|
1694
|
+
thinking off at effort `low` and `max_tokens` 200 (two short sentences
|
|
1695
|
+
with room; an answer cut there is dropped and the log says its stop
|
|
1696
|
+
reason), one try with no retries, 8 s at most. Every call's line in the
|
|
1697
|
+
log has its seconds, stop reason, tokens and money.
|
|
1698
|
+
- **When.** A riichi, a win (a mangan or more counts as bigger), a draw or
|
|
1699
|
+
an abortive draw, the lead changing hands, the last round (オーラス),
|
|
1700
|
+
the end; and a quiet minute in the middle of a round, about one topic
|
|
1701
|
+
the command picks in turn (the leader's lead, a seat in tenpai without
|
|
1702
|
+
riichi when the hands are shown, the dora, the wall left, the last
|
|
1703
|
+
discard; never the same twice running). One call at a
|
|
1704
|
+
time and **at most one every 20 s**, or every 8 s for a scene bigger
|
|
1705
|
+
than the last one asked about (a win after a riichi, the end after a
|
|
1706
|
+
win; the ranks: the end, a big win and the lead changing hands, a win
|
|
1707
|
+
and the oorasu, a draw, a riichi, a quiet minute). A scene that comes
|
|
1708
|
+
sooner waits (the biggest of those waiting, for 15 s at most) and is let
|
|
1709
|
+
go when its round or phase is over. Calls are made only while a page is
|
|
1710
|
+
open (or with `--json`): nobody watching, nothing is spent. A tonpuu of
|
|
1711
|
+
four CPUs gets about two a round, and a line for its wins.
|
|
1712
|
+
- **What it is told.** A fixed part, the same to the byte every time and
|
|
1713
|
+
cached (prompt caching: the calls after the first read it at a tenth of
|
|
1714
|
+
the price): the commentator's role, mahjong's rules in short, the tone,
|
|
1715
|
+
the length (40 characters, one or two sentences, the line only: no
|
|
1716
|
+
preamble, quotes or emoji) and whether it may speak of the hands. Then
|
|
1717
|
+
the table now (the round, honba, riichi sticks, the wall, the dora; each
|
|
1718
|
+
seat's name, wind, points, riichi, melds and last discards; with the
|
|
1719
|
+
hands shown, each hand and its waits; a win's yaku and points, the
|
|
1720
|
+
ranks at the end), the scene, the fixed lines that just came, and its
|
|
1721
|
+
own last lines (not to repeat itself).
|
|
1722
|
+
- **The hands hidden.** With `--public`, and while a page has **H** on,
|
|
1723
|
+
the AI is not told the hands (nor the waits, nor who is tenpai): it
|
|
1724
|
+
speaks from what everyone can see. A line made while the hands were
|
|
1725
|
+
shown is not shown on a page that hides them.
|
|
1726
|
+
- **Late lines are dropped.** An answer that comes when its round or
|
|
1727
|
+
phase is over, or more than 15 s after it was asked, is not said. On
|
|
1728
|
+
the page an AI's line waits behind the fixed lines (its scene's own come
|
|
1729
|
+
first; a round's start waits behind it), and is dropped when a new scene
|
|
1730
|
+
comes before it is said (a riichi, a win, a draw), first when the voice
|
|
1731
|
+
falls behind, and after 40 s in the queue.
|
|
1732
|
+
- **The voice.** The caption is the line as it came; the voice reads it
|
|
1733
|
+
through the same readings as the fixed lines (`SPEECH_READ` and the
|
|
1734
|
+
tiles: 三萬 サンマン, 流局 リューキョク).
|
|
1735
|
+
- **What it costs.** Counted from the tokens the API reports (input,
|
|
1736
|
+
output, the cache's writes and reads) at the model's published price
|
|
1737
|
+
(Haiku 5.5: 0.10 USD a million input tokens, 0.50 output, 0.125 a cache
|
|
1738
|
+
write, 0.01 a cache read), in
|
|
1739
|
+
`$XDG_STATE_HOME/asterism-games/commentary_ai_usage.json`
|
|
1740
|
+
(`~/.local/state/...` by default; counts and money only, no words). The
|
|
1741
|
+
bar shows today's money and the limit; when today's money reaches
|
|
1742
|
+
`--ai-daily-usd` (default 3 USD) no call is made and the fixed
|
|
1743
|
+
commentary goes on alone; the next day (local date) starts at 0. A
|
|
1744
|
+
call costs about a hundredth of a cent with Haiku (some 1,000 cached
|
|
1745
|
+
tokens, a few hundred new ones, a line out), so a tonpuu costs well
|
|
1746
|
+
under a cent.
|
|
1747
|
+
- **Failures** (no answer in time, an error, a refusal) are one line in
|
|
1748
|
+
the command's output and nothing is said; the key and the words are
|
|
1749
|
+
never written out. `ASTERISM_GAMES_AI_FAKE=1` puts a fake in Claude's
|
|
1750
|
+
place (no API is called; its money is counted apart, in
|
|
1751
|
+
`commentary_ai_usage.fake.json`), to try the page without a key.
|
|
1752
|
+
|
|
1466
1753
|
## Playing from a program
|
|
1467
1754
|
|
|
1468
1755
|
The mahjong table also speaks to programs (games 0.10.0): an AI assistant
|
|
@@ -1901,6 +2188,34 @@ ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 bundle exec rake test # also through a
|
|
|
1901
2188
|
tonpuu game against three CPUs each to the end from scripted input
|
|
1902
2189
|
(letters, tiles, calls, passes, OKs; `PLAY_TRANSCRIPT_DIR` keeps what
|
|
1903
2190
|
they showed).
|
|
2191
|
+
- `test_commentate.rb`: `asterism-games commentate` (in `rake test:gem`
|
|
2192
|
+
too): the waits of a hand (seven pairs, thirteen orphans, with melds),
|
|
2193
|
+
the dora of an indicator, each event from views made by hand (a round's
|
|
2194
|
+
start, the oorasu, riichi with its waits, calls and an added kan, a dora
|
|
2195
|
+
and a red five discarded, the wall, a new dora, a tenpai once a round, a
|
|
2196
|
+
big win and the lead changing hands, a draw, an abortive draw, the end)
|
|
2197
|
+
and their words, a recorded tonpuu of four CPUs
|
|
2198
|
+
(`test/fixtures/commentate_tonpuu.json.gz`) through the events and
|
|
2199
|
+
phrases, nothing of a hand with the hands hidden, the hub (the pages'
|
|
2200
|
+
frames, `--public`, `--json`, a page that opens late, a host that leaves
|
|
2201
|
+
and comes back), the web server (the page, the sheets byte for byte,
|
|
2202
|
+
404 / 405 / a foreign Host refused, 127.0.0.1 only, the events stream, a
|
|
2203
|
+
port taken), the options and `--help`; with the table there, a CPU
|
|
2204
|
+
round played here through the hub.
|
|
2205
|
+
- `test_commentate_voice.rb`: the commentary's voice (in `rake test:gem`
|
|
2206
|
+
too), against a small VOICEVOX engine started in the test: a character
|
|
2207
|
+
and a style by name (and the names offered when one is not there), the
|
|
2208
|
+
synthesis, no engine (the browser's voice, nothing made), the fixed
|
|
2209
|
+
phrases made once and named by their digest, made again when the list
|
|
2210
|
+
changes (the old files removed), the cache directory, every clip a line
|
|
2211
|
+
asks for in the list in both tones, a line synthesised once and kept
|
|
2212
|
+
(the least recently used dropped, made again when asked), a failing
|
|
2213
|
+
engine, the words a fixed phrase said left out of the line, ずんだもん's
|
|
2214
|
+
tone (every phrasing, every line of the recorded game, the clips that
|
|
2215
|
+
tell no hand), the hub's lines with their sound, the web server's
|
|
2216
|
+
`/clip/` and `/voice/` (only what the list and the hub name), the
|
|
2217
|
+
missing phrases made at the start, the options and `voices --list |
|
|
2218
|
+
--make`.
|
|
1904
2219
|
- `test_spectate.rb`: the spectate view (README "Spectate"; its
|
|
1905
2220
|
framework part in `rake test:gem` too): nothing by default (othello, the
|
|
1906
2221
|
board), the keys of the permanent tables and of a room, what the rooms
|
|
@@ -2094,4 +2409,7 @@ each result that long.
|
|
|
2094
2409
|
|
|
2095
2410
|
## License
|
|
2096
2411
|
|
|
2097
|
-
MIT (LICENSE).
|
|
2412
|
+
MIT (LICENSE). The tile sheets of the commentary page
|
|
2413
|
+
(`lib/asterism_games/commentate/assets/tiles_*.bmp`, the hand-drawn tiles
|
|
2414
|
+
of Family mruby's mahjong app) are under the MIT License in this gem too
|
|
2415
|
+
(`TILES_LICENSE.txt`).
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.19.1
|
data/bin/asterism-games
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
# # --idle: exit 0 when nobody sits at a table
|
|
14
14
|
# bin/asterism-games play <game> [options] # play at a table in this terminal (othello,
|
|
15
15
|
# # mahjong; README "Playing in a terminal")
|
|
16
|
+
# bin/asterism-games commentate <game> [options]
|
|
17
|
+
# # watch a table in a browser with commentary
|
|
18
|
+
# # (mahjong; README "Commentary")
|
|
16
19
|
# bin/asterism-games help # this text (also --help, -h)
|
|
17
20
|
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
18
21
|
|
|
@@ -26,6 +29,8 @@ USAGE = <<~TEXT
|
|
|
26
29
|
(--idle: exit 0 when nobody sits at a table)
|
|
27
30
|
asterism-games play <game> [options] play at a table in this terminal
|
|
28
31
|
(othello, mahjong; play --help for the options)
|
|
32
|
+
asterism-games commentate <game> [options] watch a table in a browser, with commentary in
|
|
33
|
+
captions and voice (mahjong; commentate --help)
|
|
29
34
|
asterism-games help this text (also --help, -h)
|
|
30
35
|
TEXT
|
|
31
36
|
|
|
@@ -36,6 +41,9 @@ when "help", "--help", "-h"
|
|
|
36
41
|
when "play"
|
|
37
42
|
require "asterism_games/play"
|
|
38
43
|
exit AsterismGames::Play.main(ARGV.drop(1))
|
|
44
|
+
when "commentate"
|
|
45
|
+
require "asterism_games/commentate"
|
|
46
|
+
exit AsterismGames::Commentate.main(ARGV.drop(1))
|
|
39
47
|
when "status"
|
|
40
48
|
nil
|
|
41
49
|
else
|
data/lib/asterism_games/ai.rb
CHANGED
|
@@ -380,6 +380,70 @@ module AsterismGames
|
|
|
380
380
|
end
|
|
381
381
|
end
|
|
382
382
|
|
|
383
|
+
# A line an AI said (the commentary's one-liners, README "Commentary",
|
|
384
|
+
# "The AI's one-liners"): its text, the tokens as the API counted them
|
|
385
|
+
# ({"in", "out", "cache_write", "cache_read"}), how long it took and
|
|
386
|
+
# why it stopped (the API's stop_reason, e.g. "end_turn").
|
|
387
|
+
Said = Struct.new(:text, :tokens, :ms, :stop, keyword_init: true)
|
|
388
|
+
|
|
389
|
+
# Claude asked for a short line of free text (the commentary's
|
|
390
|
+
# one-liners): the fixed part as the system prompt (cached, so the
|
|
391
|
+
# calls after the first read it from the cache), the changing part as
|
|
392
|
+
# the user's message, the answer the text itself. One try, no retries:
|
|
393
|
+
# a line that comes late is worth nothing. The anthropic gem is
|
|
394
|
+
# loaded here, only when there is a key (it is not a dependency).
|
|
395
|
+
#
|
|
396
|
+
# Thinking is off and effort low: the answer is one or two sentences
|
|
397
|
+
# of 40 characters, and with adaptive thinking Haiku 5.5 spent up to
|
|
398
|
+
# 600 output tokens and 7 s thinking on an open question (the quiet
|
|
399
|
+
# scene). Off is accepted at effort low to high; the request has no
|
|
400
|
+
# tools and no structured output, so none of its limits applies.
|
|
401
|
+
# MAX_TOKENS fits two sentences of 60 characters (about 100 tokens)
|
|
402
|
+
# twice over; an answer cut there is dropped, its stop reason logged.
|
|
403
|
+
class Commentator
|
|
404
|
+
MAX_TOKENS = 200
|
|
405
|
+
THINKING = { type: :disabled }.freeze
|
|
406
|
+
|
|
407
|
+
attr_reader :model
|
|
408
|
+
|
|
409
|
+
def initialize(key, model:, effort: "low", base_url: nil)
|
|
410
|
+
require "anthropic"
|
|
411
|
+
kw = { api_key: key, max_retries: 0 }
|
|
412
|
+
kw[:base_url] = base_url if base_url
|
|
413
|
+
@client = ::Anthropic::Client.new(**kw)
|
|
414
|
+
@model = model
|
|
415
|
+
@effort = effort
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def say(system:, user:, timeout: 8)
|
|
419
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
420
|
+
msg = @client.messages.create(
|
|
421
|
+
model: @model.to_sym,
|
|
422
|
+
max_tokens: MAX_TOKENS,
|
|
423
|
+
system_: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
|
|
424
|
+
thinking: THINKING,
|
|
425
|
+
output_config: { effort: @effort.to_sym },
|
|
426
|
+
messages: [{ role: "user", content: user }],
|
|
427
|
+
request_options: { timeout: timeout }
|
|
428
|
+
)
|
|
429
|
+
u = msg.usage
|
|
430
|
+
tokens = { "in" => u.input_tokens.to_i, "out" => u.output_tokens.to_i,
|
|
431
|
+
"cache_write" => u.cache_creation_input_tokens.to_i, "cache_read" => u.cache_read_input_tokens.to_i }
|
|
432
|
+
stop = msg.stop_reason.to_s
|
|
433
|
+
raise TokenError.new("claude: refused (stop_reason refusal)", tokens) if stop == "refusal"
|
|
434
|
+
unless %w[end_turn stop_sequence].include?(stop)
|
|
435
|
+
raise TokenError.new("claude: cut (stop_reason #{stop}, out #{tokens['out']})", tokens)
|
|
436
|
+
end
|
|
437
|
+
text = msg.content.select { |b| b.type == :text }.map(&:text).join
|
|
438
|
+
Said.new(text: text, tokens: tokens, stop: stop,
|
|
439
|
+
ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round)
|
|
440
|
+
rescue ::Anthropic::Errors::APIStatusError => e
|
|
441
|
+
raise Error, "claude: HTTP #{e.status} #{e.class.name.split('::').last}"
|
|
442
|
+
rescue ::Anthropic::Errors::APIConnectionError => e
|
|
443
|
+
raise Error, "claude: #{e.class.name.split('::').last}"
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
383
447
|
# A failure after the API counted tokens (they still cost).
|
|
384
448
|
class TokenError < Error
|
|
385
449
|
attr_reader :tokens
|