File size: 10,965 Bytes
29f25be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
"""Package prepared IME inputs with provenance and a Mac export command."""

import argparse
import collections
import json
import re
import shutil
import zipfile
from pathlib import Path

import sentencepiece as spm

ROOT = Path(__file__).resolve().parents[2]


def write(path, data):
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--input", type=Path, default=ROOT / "artifacts/benchmarks/ime-expanded-v21-final"
    )
    parser.add_argument("--output", type=Path, default=ROOT / "handoff/ime-v21-3000-20261007")
    args = parser.parse_args()
    archive = args.output.with_suffix(".zip")
    if args.output.exists() or archive.exists():
        parser.error("Keep existing handoffs; use a fresh output.")
    rows = json.loads((args.input / "evaluation_items.json").read_text(encoding="utf-8"))
    manifest = json.loads((args.input / "manifest.json").read_text(encoding="utf-8"))
    splits = {
        role: json.loads((args.input / role / "evaluation_items.json").read_text(encoding="utf-8"))
        for role in ("development", "blind")
    }
    assert len(rows) == 3000 and len(splits["development"]) == 2000 and len(splits["blind"]) == 1000
    assert rows == splits["development"] + splits["blind"]
    for field in ("index", "input"):
        assert len({r[field] for r in rows}) == 3000
    for field in ("group_id", "source_url", "original_sentence"):
        assert len({r["provenance"][field] for r in rows}) == 3000
    for row in rows:
        p = row["provenance"]
        assert p["original_sentence"] == row["context_text"] + row["expected_output"][0]
        assert p["span_start"] == len(row["context_text"]) and p["span_end"] == len(
            p["original_sentence"]
        )
        assert re.fullmatch("[ァ-ヺー。、!?]+", row["input"])
        assert row["expected_output"] and row["review"]["not_formal_gold"]
    maximum = {}
    for version in ("v1", "v2"):
        tokenizer = spm.SentencePieceProcessor(
            model_file=str(ROOT / f"artifacts/tokenizers/ja-unigram-16k-{version}/tokenizer.model")
        )
        maximum[version] = max(
            len(tokenizer.encode(r["context_text"] + a)) + 1
            for r in rows
            for a in r["expected_output"]
        )
        assert maximum[version] <= 128
    for role, data in {"all": rows, **splits}.items():
        folder = args.input if role == "all" else args.input / role
        cli = json.loads((folder / "ajimee-input.json").read_text(encoding="utf-8"))
        mapping = json.loads((folder / "case-map.json").read_text(encoding="utf-8"))
        assert len(cli) == len(data) and len(mapping) == len(data)
        for case, converted in zip(data, cli):
            assert (
                converted["query"] == case["input"]
                and converted["left_context"] == case["context_text"]
            )
            assert (
                converted["answer"] == case["expected_output"]
                and converted["right_context"] is None
            )
    qa = {
        "cases": 3000,
        "development": 2000,
        "blind": 1000,
        "unique_ids": 3000,
        "unique_readings": 3000,
        "unique_source_groups": 3000,
        "unique_source_urls": 3000,
        "span_and_cli_mapping_checked": True,
        "maximum_joint_reference_tokens_including_bos": maximum,
        "candidate_token_lengths_checked": False,
        "actual_candidate_export_complete": False,
        "native_label_review_complete": False,
        "models_scored": False,
        "sources": dict(collections.Counter(r["provenance"]["source"] for r in rows)),
        "with_context": sum(bool(r["context_text"]) for r in rows),
        "validation_scope": "Actual 3000 files: counts, unique fields, spans, CLI mapping, reference token budgets. No repeated SHA256 or test suite.",
    }
    shutil.copytree(args.input, args.output)
    script = (ROOT / "scripts/benchmarks/export_azookey_v21.sh").read_text(encoding="utf-8")
    (args.output / "export_azookey.sh").write_bytes(script.encode("utf-8"))
    write(args.output / "handoff-validation.json", qa)
    manifest["handoff_validation"] = qa
    manifest["status"] = "candidate_collection_inputs_ready_labels_provisional"
    write(args.output / "manifest.json", manifest)
    (args.output / "NOTICE.md").write_text(
        """# Source attribution

This package contains derived conversion spans from the frozen VimeML corpus
validation and test splits. Source text is unchanged; kana and provisional
orthographic alternatives are derived. Each evaluation item records its original
sentence, source URL, document ID, local source file/row and conversion span.

- FineWeb2 Edu Japanese: Yuichi Tateno (2025), built on HuggingFaceFW FineWeb2.
  https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese
  Dataset card: https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese/raw/main/README.md
  Dataset license: Open Data Commons Attribution 1.0 (ODC-By), also subject to
  Common Crawl terms: https://commoncrawl.org/terms-of-use . Underlying web content
  retains its original rights; original website URLs are recorded per case.
- Tatoeba sentence contributors: https://tatoeba.org/
  Terms: https://tatoeba.org/en/terms_of_use ; default text license CC BY 2.0 FR.
  Per-sentence URLs/IDs are recorded so original contributors and licenses can be
  resolved; contributor names were not included in the local three-column TSV.

This is a private research handoff, not a publication of a fully adjudicated
benchmark. License/author attribution should follow the source records if shared
publicly. No corpus, model, SSH or W&B credentials are included.
""",
        encoding="utf-8",
    )
    readme = f"""# V2.1:3,000 条 AzooKey 候选收集输入

2026-10-07。本包是实际 JSON 输入,开发集 **2,000 条**,盲集 **1,000 条**。
可直接使用之前成功导出 AJIMEE 的 Mac 转换器 checkout。

## 一条命令导出

解压 zip,进入 `ime-v21-3000-20261007`,将下方路径换成你的转换器仓库路径:

```bash
bash export_azookey.sh "/实际路径/AzooKeyKanaKanjiConverter"
```

若使用此前指南的仓库:

```bash
bash export_azookey.sh "$HOME/Sources/AzooKeyKanaKanjiConverter-ajimee"
```

脚本按 development、blind 顺序导出真实候选;已成功完成且输入一致的部分会保留。
使用 `n_best=20`、`typo_mode=off`、不启用 Zenzai、不使用 `--stable`。
记录 converter commit、字典子模块版本、Swift 版本、flags、日志和完成标记。
转换器固定为 `d59a28e4c7ca049aef04f29a91eae9677a7753f2`,不会自动修改你的 checkout。
需要该 checkout 已有 `.build/release/CliTool`;如尚未构建,在转换器仓库执行:

```bash
swift build -c release --product CliTool -Xcxx -xobjective-c++
```

## 返回哪些文件

导出结束后,把整个 **`azookey-results` 目录**发回,两个 split 都保留:

```text
azookey-results/
  development/
    ajimee-input.json
    case-map.json
    azookey-candidates.json
    converter-version.txt
    dictionary-versions.txt
    swift-version.txt
    export-flags.txt
    export.log
    completed.txt
  blind/
    (同上)
```

脚本第二个参数可指定结果目录。若上次中断留下未标记完成的候选文件,脚本保留它并提示改用新目录,避免误覆盖。
根目录还有合并的 `ajimee-input.json`(3,000 条),方便检查;正式交接优先分别导出两套。

## 输入检查与用途

- 冻结 corpus validation 原句用于 development,test 原句用于 blind;未读训练 split 生成样本。
- 全部读音经 UniDic Lite + Sudachi 两词典、原句上下文读音一致性和词边界检查;避免从复合名词、片假名词内部截断。
- 3,000 个读音、源文档组、原句和来源 URL 各自唯一;剔除与现有 200/137 条完全相同的 query/context。
- 共 {qa["with_context"]:,} 条有左文、{3000 - qa["with_context"]:,} 条无左文;来源 FineWeb {qa["sources"].get("fineweb", 0):,} 条、Tatoeba {qa["sources"].get("tatoeba", 0):,} 条。
- 每条保留完整原句、转换区间和来源,开发/盲集按来源隔离;模型得分和候选召回不参与选样。
- 所有参考答案与左文联合分词,含 BOS 最大 V1={maximum["v1"]}、V2={maximum["v2"]} tokens,低于 context128。真实候选的 token 长度要等导出后再检查。

**当前标签是草稿,不是已经验收的正式 3,000 条 gold。** 双词典一致仍可能选错读音;网页原句可能有噪声,同读音表记不保证穷尽,尚未完成全部母语者审核。部分高风险多读音词和已发现的提取噪声已过滤,但不是同音词/专名等类别均衡的最终集。近重复来源也未全面审计。

返回真实候选后,再复核标签、隔离歧义、记录 Recall@20 和类别/候选数分层;保留全池与召回失败诊断,另报告 covered subset。
盲集在模型选择阶段不进行 LM 计分;只在开发集确定模型之后执行最终盲测。本次读取 test 文本仅准备输入,没有算 test BPC 或运行盲集模型。
原计划目标是 3,000 条**有效正式样本**,这次先交付 3,000 条候选收集输入;验收如有剔除,会从相同来源规则补齐后另建正式版本。

## 包内文件

每套有 `evaluation_items.json`、`ajimee-input.json`、`case-map.json`、`stats.json`。
`manifest.json` 记录方法、限制和版本;`handoff-validation.json` 是本次实际文件检查;`NOTICE.md` 和逐条 provenance 记录来源。
没有假候选,没有模型/大语料文件,也没有 SSH 或 W&B 凭据。

脚本已通过 Bash 语法检查;实际 macOS Swift 导出由你在现有 AzooKey 环境运行。
"""
    (args.output / "README_CN.md").write_text(readme, encoding="utf-8")
    with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as package:
        for path in sorted(args.output.rglob("*")):
            if path.is_file():
                package.write(path, path.relative_to(args.output.parent).as_posix())
    with zipfile.ZipFile(archive) as package:
        assert len(json.loads(package.read(args.output.name + "/ajimee-input.json"))) == 3000
        assert all(
            json.loads(package.read(args.output.name + "/" + role + "/ajimee-input.json"))
            == json.loads((args.output / role / "ajimee-input.json").read_text(encoding="utf-8"))
            for role in splits
        )
    print(
        json.dumps(
            {
                "directory": str(args.output),
                "archive": str(archive),
                "zip_bytes": archive.stat().st_size,
                "validation": qa,
            },
            ensure_ascii=False,
            indent=2,
        )
    )


if __name__ == "__main__":
    main()