voice_audio_multi/tests/samsone/convert_gguf.py
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

164 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Convert a released SAMSONE checkpoint to a standalone GGUF."""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
import torch
from safetensors.torch import save_file
from transformers import AutoTokenizer
REPO = Path(__file__).resolve().parents[2]
CONVERTER = REPO / "build/debug/bin/audiocpp_gguf"
def build_token_map(tokenizer) -> dict[int, int]:
special_ids = set(tokenizer.all_special_ids)
output: dict[int, int] = {}
for token, original_id in sorted(tokenizer.get_vocab().items(), key=lambda item: item[1]):
cleaned = re.sub(r"[^A-Za-z0-9]", "", token)
keep = original_id in special_ids or (
not any(character.isupper() for character in cleaned)
and not re.search(r"[\sĠĊĉ]{4,}", token)
and not re.search(r"[^\x00-\x7FĠĊĉ\s]", token)
and not re.search(r"[-#]{3,}", token)
)
if keep:
output[len(output)] = original_id
return output
def converted_name(name: str) -> str | None:
name = name.removeprefix("audio_lm.")
if name == "text_model.embedding_layer.weight":
return None
if name.startswith("audio_encoder.encoder.encoder."):
suffix = name.removeprefix("audio_encoder.encoder.encoder.")
if suffix == "embed_positions.weight":
return "encoder.positional_embedding"
parts = suffix.split(".")
if parts[0] == "layers":
parts[0] = "blocks"
replacements = {
"self_attn": "attn",
"q_proj": "query",
"k_proj": "key",
"v_proj": "value",
"out_proj": "out",
"self_attn_layer_norm": "attn_ln",
"final_layer_norm": "mlp_ln",
"fc1": "mlp.0",
"fc2": "mlp.2",
}
parts = [replacements.get(part, part) for part in parts]
elif parts[0] == "layer_norm":
parts[0] = "ln_post"
return "encoder." + ".".join(parts)
if name.startswith("text_model.model.model."):
return "model." + name.removeprefix("text_model.model.model.")
if name.startswith("text_model.model.lm_head."):
return None
return name
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--checkpoint", type=Path, required=True)
parser.add_argument("--text-model-dir", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--type", choices=("bf16", "q8_0"), default="bf16")
args = parser.parse_args()
checkpoint = args.checkpoint.resolve()
text_model_dir = args.text_model_dir.resolve()
output = args.output.resolve()
state = torch.load(checkpoint, map_location="cpu", weights_only=True)["state_dict"]
if not state or not all(name.startswith("audio_lm.") for name in state):
raise RuntimeError("expected a released SAMSONE AudioLM checkpoint")
tensors = {}
for name, tensor in state.items():
target = converted_name(name)
if target is not None:
tensors[target] = tensor.contiguous()
tokenizer = AutoTokenizer.from_pretrained(text_model_dir)
token_map = build_token_map(tokenizer)
hidden = tensors["sep_token"].shape[0]
layers = sum(name.endswith(".self_attn.q_proj.weight") and name.startswith("model.layers.") for name in tensors)
q_shape = tensors["model.layers.0.self_attn.q_proj.weight"].shape
k_shape = tensors["model.layers.0.self_attn.k_proj.weight"].shape
intermediate = tensors["model.layers.0.mlp.gate_proj.weight"].shape[0]
text_config = json.loads((text_model_dir / "config.json").read_text())
original_eos = int(text_config["eos_token_id"])
original_to_pruned = {original: pruned for pruned, original in token_map.items()}
config = {
"model_type": "samsone",
"hidden_size": hidden,
"intermediate_size": intermediate,
"num_attention_heads": int(text_config["num_attention_heads"]),
"num_key_value_heads": int(k_shape[0] // (q_shape[0] // int(text_config["num_attention_heads"]))),
"num_hidden_layers": layers,
"vocab_size": len(token_map),
"max_position_embeddings": int(text_config["max_position_embeddings"]),
"rms_norm_eps": float(text_config["rms_norm_eps"]),
"rope_theta": float(text_config["rope_theta"]),
"eos_token_id": original_to_pruned[original_eos],
}
if tensors["model.embed_tokens.weight"].shape != (len(token_map), hidden):
raise RuntimeError("checkpoint vocabulary does not match the reconstructed token map")
subprocess.run(
["cmake", "--build", str(REPO / "build/debug"), "--target", "audiocpp_gguf", "-j", "8"],
check=True,
)
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="samsone-convert-") as temporary:
root = Path(temporary)
source = root / "model.safetensors"
save_file(tensors, source)
(root / "config.json").write_text(json.dumps(config, indent=2) + "\n")
(root / "token_map.json").write_text(json.dumps(token_map, sort_keys=True) + "\n")
for filename in ("tokenizer.json", "tokenizer_config.json"):
shutil.copyfile(text_model_dir / filename, root / filename)
fd, temporary_output = tempfile.mkstemp(prefix=output.stem + "-", suffix=".gguf", dir=output.parent)
os.close(fd)
temporary_output = Path(temporary_output)
try:
subprocess.run(
[
str(CONVERTER),
"--input", f"weights={source}",
"--root", str(root),
"--output", str(temporary_output),
"--type", args.type,
"--family", "samsone",
"--model-spec", str(REPO / "model_specs/samsone.json"),
"--overwrite",
],
check=True,
)
inspection = subprocess.run(
[str(CONVERTER), "--inspect", str(temporary_output)],
check=True,
capture_output=True,
text=True,
).stdout
if f"tensors={len(tensors)}" not in inspection.splitlines():
raise RuntimeError(f"SAMSONE GGUF tensor count mismatch:\n{inspection}")
os.replace(temporary_output, output)
print(inspection, end="")
finally:
temporary_output.unlink(missing_ok=True)
if __name__ == "__main__":
main()