File size: 4,038 Bytes
0cc1b2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Bundle the small tokenizer/config assets needed for offline reconstruction."""

from __future__ import annotations

import argparse
import json
import shutil
from pathlib import Path

from transformers import AutoConfig

from model_registry import CHECKPOINTS, LANGUAGES, MODES, MODEL_SPECS, model_spec


SCRIPT_DIR = Path(__file__).resolve().parent
TOKENIZER_CANDIDATES = (
    "sentencepiece.bpe.model",
    "spm.model",
    "spiece.model",
    "vocab.txt",
    "tokenizer.json",
)
TOKENIZER_METADATA = (
    "tokenizer_config.json",
    "special_tokens_map.json",
    "added_tokens.json",
)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--base-model-root", type=Path, required=True)
    parser.add_argument(
        "--model-root", type=Path, default=SCRIPT_DIR.parent / "models"
    )
    parser.add_argument("--model", action="append", choices=sorted(MODEL_SPECS))
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    base_root = args.base_model_root.resolve()
    model_root = args.model_root.resolve()
    selected = set(args.model or MODEL_SPECS)

    for model_name in MODEL_SPECS:
        if model_name not in selected:
            continue
        if MODEL_SPECS[model_name]["backend"] == "bge":
            print(f"SKIP {model_name}: no releasable trained MLP head", flush=True)
            continue
        for language in LANGUAGES:
            if not any(
                CHECKPOINTS[(model_name, language, mode)]["available"] for mode in MODES
            ):
                continue
            spec = model_spec(model_name, language)
            source = base_root / spec["local_base_dir"]
            if not (source / "config.json").is_file():
                raise FileNotFoundError(
                    f"Missing local base-model config for {model_name}/{language}: {source}"
                )
            destination = model_root / model_name / language / "base_model"
            destination.mkdir(parents=True, exist_ok=True)
            config = AutoConfig.from_pretrained(source, local_files_only=True)
            config.save_pretrained(destination)
            # Prefer the compact native tokenizer model. Saving a fast tokenizer
            # with recent Transformers can expand it into a ~17 MB, million-line
            # JSON file; the native assets are both smaller and fully sufficient.
            tokenizer_asset = (
                "tokenizer.json"
                if model_name == "slavic-specific" and language == "slovenian"
                else next(
                    (name for name in TOKENIZER_CANDIDATES if (source / name).is_file()),
                    None,
                )
            )
            if tokenizer_asset is None:
                raise FileNotFoundError(f"No tokenizer asset found in {source}")
            keep = {tokenizer_asset, "config.json", "source.json"}
            for name in TOKENIZER_METADATA:
                if (source / name).is_file():
                    shutil.copy2(source / name, destination / name)
                    keep.add(name)
            shutil.copy2(source / tokenizer_asset, destination / tokenizer_asset)
            for candidate in (*TOKENIZER_CANDIDATES, *TOKENIZER_METADATA):
                stale = destination / candidate
                if stale.is_file() and candidate not in keep:
                    stale.unlink()
            metadata = {
                "model": model_name,
                "language": language,
                "upstream_base_model": spec["base_model"],
                "purpose": "Tokenizer and architecture configuration for offline checkpoint reconstruction",
                "contains_upstream_model_weights": False,
            }
            (destination / "source.json").write_text(
                json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
            )
            print(f"BUNDLED {model_name}/{language}: {destination}", flush=True)


if __name__ == "__main__":
    main()