Incremental Paipu Pipeline¶
tools/paipu_pipeline.py builds a token-cache from raw Tenhou paipu in a
fully resumable and self-healing way. It is driven by a single
append-only manifest (manifest.jsonl) that records every state
transition for every game id, so any run can be safely interrupted and
restarted.
Layout on disk¶
<work>/
manifest.jsonl # append-only event log
game_ids_<year>.txt # extracted from the year zip
xml/<game_id>.txt # raw paipu (named .txt because
# PaipuReplay requires it)
cache/
index.json # rebuilt from per-shard meta.json
shard_00000/{tokens,scalars,...}.npy
shard_00000/meta.json
...
Subcommands¶
Command |
Purpose |
|---|---|
|
full pipeline: sanity check → adopt unknown XMLs → download missing ids → encode → rebuild cache index |
|
dry-run sanity check (use |
|
summarise manifest counts |
|
regenerate |
Common flags:
--work <dir>: working directory (required)--year <YYYY>: download / use the year zip fromtenhou.net/sc/raw--max-new <N>: cap how many new paipu to download in one invocation--shard-rows <N>: rows per shard (default 65536)--delay <sec>: sleep between downloads (be polite)
Manifest schema¶
Each line is a JSON object. The most recent record per gid wins.
{ "ts": "...", "gid": "...", "status": "downloaded",
"sha256": "...", "size": 14246 }
{ "ts": "...", "gid": "...", "status": "encoded",
"shard": "shard_00000", "n_samples": 2387 }
{ "ts": "...", "gid": "...", "status": "duplicate",
"sha256": "...", "alias_of": "<other-gid>" }
{ "ts": "...", "gid": "...", "status": "corrupt" }
{ "ts": "...", "gid": "...", "status": "failed", "error": "..." }
Statuses:
downloaded— XML present and sha-verified, not yet encodedencoded— encoded into a shard (terminal happy path)duplicate— same sha256 as another gid; XML deletedcorrupt— sha mismatch or bad payload (queued for re-download)failed— download or encoding error
Atomic encoding¶
The encode stage buffers samples per shard and writes them via
pymahjong.rl.cache.ShardWriter. The manifest encoded event is
appended after the shard file is closed (and meta.json written),
so:
A crash mid-shard leaves the affected paipu as
downloadedand they will be re-encoded next run.A successfully closed shard is durable even if the process dies before exit.
After repair (e.g. corrupt XML re-fetched), the new content is encoded
into a new shard. The previous shard may now contain stale rows whose
paipu are no longer associated with it; cache/index.json only counts
samples claimed by current encoded events, but the on-disk shard rows
remain. A future compact command may rewrite shards to drop stale
rows. For training this is harmless: the loader uses index.json.
Sanity check categories¶
Every run/check invocation prints:
Field |
Meaning |
|---|---|
|
XML file exists and sha matches manifest |
|
XML exists but sha differs from manifest |
|
manifest says |
|
XML exists but no manifest record (will be adopted) |
|
|
|
shard dir exists but no manifest record points to it |
--repair will:
delete any XML whose sha mismatches the recorded one and emit a
corruptevent so it gets re-fetched on the nextrun;leave shards untouched (they are addressed by re-encode, not delete).
Typical flows¶
Build a fresh cache from a year zip:
python tools/paipu_pipeline.py run --work data/tenhou --year 2024
Resume after a crash:
python tools/paipu_pipeline.py run --work data/tenhou --year 2024
# sanity check finds in-flight state, encode picks up where it left off
Recover from disk corruption:
python tools/paipu_pipeline.py check --work data/tenhou --repair
python tools/paipu_pipeline.py run --work data/tenhou --year 2024
Use hand-curated XMLs only:
mkdir -p data/tenhou/xml
cp my_paipu/*.txt data/tenhou/xml/ # name them <gid>.txt
python tools/paipu_pipeline.py run --work data/tenhou
# adopt step picks them up; download step is a no-op