File size: 8,421 Bytes
9be8fa9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee3a5e
 
 
9be8fa9
 
 
c228b1d
 
 
 
 
 
 
 
 
 
9be8fa9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee3a5e
 
 
 
 
 
c228b1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9be8fa9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""AIFlow Math Ink 0.6 LiteRT ๋ณ€ํ™˜์— ํ•„์š”ํ•œ ์ตœ์†Œ Colab ZIP์„ ๊ฒฐ์ •์ ์œผ๋กœ ๋งŒ๋“ ๋‹ค."""

from __future__ import annotations

import argparse
from datetime import datetime, timezone
from hashlib import sha256
import json
from pathlib import Path, PurePosixPath
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile, ZipInfo


PROJECT_ROOT = Path(__file__).parents[1]
MANIFEST_NAME_06 = "LITERT_COLAB_BUNDLE_MANIFEST.json"


def _sha256_bytes06(value: bytes) -> str:
    """ํ•„์š” ๋ณ€์ˆ˜: file bytes. ์ž‘๋™ ์›๋ฆฌ: bundle entry์™€ manifest ๊ฒ€์ฆ์šฉ SHA-256์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค."""

    return sha256(value).hexdigest()


def _bundle_files06(
    base_checkpoint: Path, adapter_checkpoint: Path, representative_inputs: Path,
) -> dict[str, Path]:
    """ํ•„์š” ๋ณ€์ˆ˜: seed-17 composite artifact. ์ž‘๋™ ์›๋ฆฌ: converter๊ฐ€ importํ•˜๋Š” ๋‹ซํžŒ ์ตœ์†Œ ํŒŒ์ผ ์ง‘ํ•ฉ์„ ๋งคํ•‘ํ•œ๋‹ค."""

    return {
        "pyproject.toml": PROJECT_ROOT / "pyproject.toml",
        "src/math_grid_drawer/__init__.py": PROJECT_ROOT / "research/colab/litert_bundle_package_init.py",
        "src/math_grid_drawer/research/__init__.py": PROJECT_ROOT / "src/math_grid_drawer/research/__init__.py",
        "src/math_grid_drawer/research/ink06_canonical.py": PROJECT_ROOT / "src/math_grid_drawer/research/ink06_canonical.py",
        "src/math_grid_drawer/research/ink06_export.py": PROJECT_ROOT / "src/math_grid_drawer/research/ink06_export.py",
        "src/math_grid_drawer/research/math_ink_06.py": PROJECT_ROOT / "src/math_grid_drawer/research/math_ink_06.py",
        "src/math_grid_drawer/research/raster_skeleton06.py": PROJECT_ROOT / "src/math_grid_drawer/research/raster_skeleton06.py",
        "src/math_grid_drawer/research/skeleton_adapter06.py": PROJECT_ROOT / "src/math_grid_drawer/research/skeleton_adapter06.py",
        "src/math_grid_drawer/research/trajectory_sequence.py": PROJECT_ROOT / "src/math_grid_drawer/research/trajectory_sequence.py",
        "scripts/export_math_ink_06_litert.py": PROJECT_ROOT / "scripts/export_math_ink_06_litert.py",
        "artifacts/base_378.pt": base_checkpoint,
        "artifacts/online_adapter.pt": adapter_checkpoint,
        "artifacts/representative_inputs.pt": representative_inputs,
    }


def build_litert_colab_bundle06(
    base_checkpoint: Path,
    adapter_checkpoint: Path,
    representative_inputs: Path,
    output: Path,
) -> dict:
    """ํ•„์š” ๋ณ€์ˆ˜: composite checkpointยท๋Œ€ํ‘œ ์ž…๋ ฅยทZIP ์ถœ๋ ฅ. ์ž‘๋™ ์›๋ฆฌ: ๊ฒฝ๋กœยทํฌ๊ธฐยทํ•ด์‹œ๊ฐ€ ๊ณ ์ •๋œ Linux ๋ณ€ํ™˜ bundle์„ ๋งŒ๋“ ๋‹ค."""

    files = _bundle_files06(base_checkpoint, adapter_checkpoint, representative_inputs)
    missing = [name for name, path in files.items() if not path.is_file()]
    if missing:
        raise FileNotFoundError(f"LiteRT bundle ํ•„์ˆ˜ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค: {missing}")
    entries = []
    payloads: dict[str, bytes] = {}
    for name, path in files.items():
        payload = path.read_bytes()
        payloads[name] = payload
        entries.append({
            "path": name, "bytes": len(payload), "sha256": _sha256_bytes06(payload),
        })
    manifest = {
        "schema": "aiflow-math-ink-06-litert-colab-bundle-v1",
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "track": "R_public_conversion_only",
        "p_student_included": False,
        "product_bundle": False,
        "seed": 17,
        "litert_torch_version": "0.9.1",
        "representative_samples": 76,
        "raster_output_contract": {
            "outputs": [
                {"index": 0, "name": "exact_logits", "shape": [1, 378]},
                {"index": 1, "name": "coordinates", "shape": [1, 4, 128, 2]},
                {"index": 2, "name": "state_logits", "shape": [1, 4, 128, 3]},
                {"index": 3, "name": "progress", "shape": [1, 4, 128]},
                {"index": 4, "name": "hypothesis_scores", "shape": [1, 4]},
            ],
            "direct_raster_label_shortcut": False,
        },
        "files": entries,
        "product_validation": False,
    }
    output.parent.mkdir(parents=True, exist_ok=True)
    temporary = output.with_suffix(output.suffix + ".part")
    with ZipFile(temporary, "w", allowZip64=True) as bundle:
        for name, payload in payloads.items():
            compression = ZIP_STORED if PurePosixPath(name).suffix in {".pt"} else ZIP_DEFLATED
            info = ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
            info.compress_type = compression
            info.external_attr = 0o644 << 16
            bundle.writestr(info, payload)
        info = ZipInfo(MANIFEST_NAME_06, date_time=(1980, 1, 1, 0, 0, 0))
        info.compress_type = ZIP_DEFLATED
        info.external_attr = 0o644 << 16
        bundle.writestr(
            info, json.dumps(manifest, ensure_ascii=False, indent=2).encode("utf-8") + b"\n",
        )
    temporary.replace(output)
    manifest["bundle"] = str(output)
    manifest["bundle_bytes"] = output.stat().st_size
    manifest["bundle_sha256"] = sha256(output.read_bytes()).hexdigest()
    return manifest


def verify_litert_colab_bundle06(bundle_path: Path) -> dict:
    """ํ•„์š” ๋ณ€์ˆ˜: ์ƒ์„ฑ ZIP. ์ž‘๋™ ์›๋ฆฌ: ์ถ”์ถœ ์—†์ด ๋ชจ๋“  entry์˜ ์•ˆ์ „ ๊ฒฝ๋กœยทํฌ๊ธฐยทSHA-256์„ ์žฌ๊ฒ€์ฆํ•œ๋‹ค."""

    failures = []
    with ZipFile(bundle_path) as bundle:
        manifest = json.loads(bundle.read(MANIFEST_NAME_06).decode("utf-8"))
        if (
            manifest.get("track") != "R_public_conversion_only"
            or manifest.get("p_student_included") is not False
            or manifest.get("product_bundle") is not False
        ):
            failures.append({"path": MANIFEST_NAME_06, "reason": "research_track_contract"})
        contract = manifest.get("raster_output_contract") or {}
        output_names = [
            str(row.get("name") or "")
            for row in contract.get("outputs", [])
        ]
        if output_names != [
            "exact_logits",
            "coordinates",
            "state_logits",
            "progress",
            "hypothesis_scores",
        ]:
            failures.append({"path": MANIFEST_NAME_06, "reason": "raster_output_contract"})
        if contract.get("direct_raster_label_shortcut") is not False:
            failures.append({"path": MANIFEST_NAME_06, "reason": "raster_shortcut_contract"})
        names = set(bundle.namelist())
        for row in manifest["files"]:
            name = str(row["path"])
            path = PurePosixPath(name)
            if path.is_absolute() or ".." in path.parts:
                failures.append({"path": name, "reason": "unsafe_path"})
            elif name not in names:
                failures.append({"path": name, "reason": "missing"})
            else:
                payload = bundle.read(name)
                if len(payload) != int(row["bytes"]):
                    failures.append({"path": name, "reason": "bytes"})
                elif _sha256_bytes06(payload) != str(row["sha256"]):
                    failures.append({"path": name, "reason": "sha256"})
    return {
        "schema": manifest["schema"], "files": len(manifest["files"]),
        "failures": failures, "passed": not failures, "product_validation": False,
    }


def main() -> None:
    """ํ•„์š” ๋ณ€์ˆ˜: CLI artifact ๊ฒฝ๋กœ. ์ž‘๋™ ์›๋ฆฌ: bundle ์ƒ์„ฑ ์งํ›„ archive ์ž์ฒด ๊ฒ€์ฆ๊นŒ์ง€ ์ˆ˜ํ–‰ํ•œ๋‹ค."""

    parser = argparse.ArgumentParser(description="Build Math Ink 0.6 LiteRT Colab bundle")
    parser.add_argument("--base-checkpoint", type=Path, required=True)
    parser.add_argument("--adapter-checkpoint", type=Path, required=True)
    parser.add_argument("--representative-inputs", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args()
    report = build_litert_colab_bundle06(
        args.base_checkpoint, args.adapter_checkpoint, args.representative_inputs, args.output,
    )
    report["verification"] = verify_litert_colab_bundle06(args.output)
    if not report["verification"]["passed"]:
        raise ValueError(f"LiteRT Colab bundle ๊ฒ€์ฆ ์‹คํŒจ: {report['verification']['failures']}")
    report_path = args.output.with_suffix(args.output.suffix + ".manifest.json")
    report_path.write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
    )
    print(json.dumps(report, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()