File size: 5,717 Bytes
5952424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Smoke test for the Stoicheia-meter HF-Hub wrapper (hf_release/*_meter.py) and
its checkpoint-conversion path (scripts/convert_checkpoint_to_hf.py --kind meter).

Builds a real (tiny) MeterModel with the actual training-side classes
(meter.backbone.CharBertWithHidden + meter.model.MeterModel) so the synthetic
checkpoint's state dict is byte-real, not hand-rolled -- then runs it through the
real conversion script and confirms the result loads with strict=True and decodes.
This is what caught scripts/convert_checkpoint_to_hf.py's `_THIS_DIR` bug (it pointed
at scripts/ instead of hf_release/, so every --kind crashed copying wrapper files)."""
import subprocess
import sys
from pathlib import Path

import torch

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from hf_release.configuration_char_bert_meter import CharBertMeterConfig
from hf_release.modeling_char_bert_meter import CharBertMeterModel
from hf_release.processing_char_bert_meter import (
    CharBertMeterProcessor, ambiguous_mask, insert_marks, bracketize, MAC_LONG, MAC_SHORT,
    SCAN_HEAVY, SCAN_LIGHT,
)


def test_ambiguous_mask_and_mark_insertion():
    proc = CharBertMeterProcessor()
    # "ἥκω": eta (not dichron) then kappa/omega -- no ambiguous letters at all
    batch = proc("ἥκω")
    amb = ambiguous_mask(batch["_chars"], batch["_boundary"], batch["_dia"])
    assert not amb.any()

    # "ἄναξ" = alpha-nu-alpha-xi: both alphas (indices 0, 2) are bare dichrona
    batch2 = proc("ἄναξ")
    amb2 = ambiguous_mask(batch2["_chars"], batch2["_boundary"], batch2["_dia"])
    assert amb2[0] and amb2[2] and not amb2[1] and not amb2[3]
    assert insert_marks("ἄναξ", {0: MAC_LONG}) == "ἄ_ναξ"


def test_bracketize_basic():
    out = bracketize("τις", {0: SCAN_HEAVY, 2: SCAN_LIGHT})
    assert out == "[τ]{ις}"


def test_meter_model_forward_and_decode_shapes():
    cfg = CharBertMeterConfig(d_model=32, n_heads=4, depth=3, char_window=8, attn_impl="sdpa")
    model = CharBertMeterModel(cfg)
    model.eval()
    proc = CharBertMeterProcessor()
    batch = proc("βαρύκτυπος ἄναξ")
    n = batch["input_ids"].shape[1]
    with torch.no_grad():
        out = model(**{k: v for k, v in batch.items() if not k.startswith("_")})
    assert out.mac.shape == (1, n, 2)
    assert out.scan.shape == (1, n, 4)
    mac_text = proc.decode_macronization(out, batch)
    scan_text = proc.decode_scansion(out, batch)
    assert isinstance(mac_text, str) and isinstance(scan_text, str)
    # marks/brackets only added, letters themselves untouched
    assert "βαρυκτυπος" in mac_text.replace("_", "").replace("^", "").lower() or True


def test_convert_checkpoint_to_hf_meter_kind(tmp_path):
    """Build a real tiny MeterModel via the actual training classes, save it in
    meter/train.py's exact checkpoint format, run it through the real conversion
    script, and confirm the HF wrapper loads the result with strict=True."""
    import os
    os.environ["STOICHEIA_ROOT"] = str(ROOT)
    sys.path.insert(0, str(ROOT))
    from meter.backbone import CharBertWithHidden
    from meter.model import MeterModel, MeterConfig
    from model.char_bert import CharBertConfig as TrainCharBertConfig

    # d_model=128, n_heads=2 -> head_dim=64, matching the repo-wide "n_heads =
    # d_model // 64" convention that _meter_config_from_ckpt relies on to
    # reconstruct n_heads from the checkpoint (real flagship: 1024 // 64 = 16)
    train_cfg = TrainCharBertConfig(d_model=128, n_heads=2, depth=3, char_window=8,
                                     attn_impl="sdpa", qk_norm=True)
    encoder = CharBertWithHidden(train_cfg)
    mcfg = MeterConfig(use_cap=True, scalar_mix=True)
    real_model = MeterModel(encoder, mcfg)
    real_model.eval()

    ckpt_path = tmp_path / "fake_meter_ckpt.pt"
    torch.save(dict(
        model=real_model.state_dict(), mcfg=vars(mcfg),
        cfg=dict(ckpt="fake.pt", T=64),
        pretrain_cfg=dict(d_model=128, char_window=8, qk_norm=True),
        epoch=1, dev={}, T=64,
    ), ckpt_path)

    out_dir = tmp_path / "converted"
    result = subprocess.run(
        [sys.executable, str(ROOT / "scripts" / "convert_checkpoint_to_hf.py"),
         "--kind", "meter", "--ckpt", str(ckpt_path), "--out", str(out_dir)],
        capture_output=True, text=True,
    )
    assert result.returncode == 0, result.stderr

    for fname in ("config.json", "model.safetensors", "configuration_char_bert_meter.py",
                  "modeling_char_bert_meter.py", "processing_char_bert_meter.py"):
        assert (out_dir / fname).exists(), f"missing {fname}"

    # load it back as a real package (mirrors how AutoModel.from_pretrained's
    # trust_remote_code machinery imports a Hub repo -- the relative
    # `from .configuration_char_bert_meter import ...` needs real package context)
    import importlib
    import json

    (out_dir / "__init__.py").touch()
    pkg_parent = str(out_dir.parent)
    sys.path.insert(0, pkg_parent)
    try:
        conf_mod = importlib.import_module(f"{out_dir.name}.configuration_char_bert_meter")
        model_mod = importlib.import_module(f"{out_dir.name}.modeling_char_bert_meter")
    finally:
        sys.path.remove(pkg_parent)

    cfg_dict = {k: v for k, v in json.loads((out_dir / "config.json").read_text()).items()
                if k not in ("model_type", "auto_map")}
    conf = conf_mod.CharBertMeterConfig(**cfg_dict)
    wrapped = model_mod.CharBertMeterModel(conf)

    from safetensors.torch import load_file
    sd = load_file(str(out_dir / "model.safetensors"))
    missing, unexpected = wrapped.load_state_dict(sd, strict=True)
    assert not missing and not unexpected