makepad/libs/ai/models/speech/tools/ref_dump.py
Admin 7f59912916 libs/ai: one AI stack, replacing libs/ggml, llama, mlx, cuda, tts, voice2 and pbr_paint
The model code was spread across eight crates that had grown into each other:
ggml and cuda and mlx each owned part of a tensor runtime, llama and tts and
voice2 each owned part of a model, and libs/diffusion owned everything else.
They are now one tree with an explicit shape:

  libs/ai/cuda     — kernels and launch surface
  libs/ai/metal    — Metal shaders and the shim
  libs/ai/llm      — the language-model runtime (sessions, lanes, contexts,
                     the CUDA and Metal executors, the compiled Metal path)
  libs/ai/models/  — common, flux, h3, music, paint, speech, stems, vision

libs/diffusion is not deleted but demoted: what remains is the VALIDATOR
crate — several dozen `*_validate.rs` oracles that check a native
implementation against a reference, which is where they belong now that the
implementations live next door.

The functional work inside the move is mostly in the LLM runtime: N lanes that
draft while one verify batch serves all of them, per-slot prefill over a shared
folded attention arena, speculation that survives batching, and a scheduler
that reports rather than publishes. And in the CUDA build: a machine without
usable CUDA must still LINK (and say so), the default kernel arch is the
building machine's GPU, `NO_CUDA` forces the stub even where the toolkit
exists, and kernels compile in parallel with progress.

libs/video_flow is new here: classical optical flow estimation and the `mkfl`
motion-field payload — a flow field measured from a clip without a model,
which is what drives free-rate bounce-looping playback and the uprez/tween
enhance pipe.
2026-08-23 01:34:35 +02:00

109 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Expose Kokoro's internal ONNX tensors so the Rust port can be diffed stage by stage.
Without this, a wrong sign somewhere in the prosody predictor shows up only as
"the audio sounds a bit off" — 2,463 nodes downstream. With it, each stage is
checked where it is written.
# list candidate tensors
/tmp/kref/bin/python libs/tts/tools/ref_dump.py --list lstm
# dump matching tensors as .npy under refdump/
/tmp/kref/bin/python libs/tts/tools/ref_dump.py --dump text_encoder "Hello there."
"""
import os
import subprocess
import sys
import numpy as np
import onnx
import onnxruntime as ort
MODEL = "kokoro_ref.onnx"
VOICE = "af_heart.mkvoice"
OUT_DIR = "refdump"
def rust_tokens(text):
out = subprocess.run(
["cargo", "run", "--release", "--quiet",
"--manifest-path", "libs/tts/Cargo.toml", "--bin", "g2p_test", "--", "--ids", text],
capture_output=True, text=True, check=True,
)
return [int(x) for x in out.stdout.strip().split(",")]
def load_voice():
sys.path.insert(0, os.path.dirname(__file__))
from ref_infer import load_voice as load # reuse the container reader
return load(VOICE)
def candidates(model):
"""Every value produced by a node, in graph order."""
return [out for node in model.graph.node for out in node.output if out]
def main():
if len(sys.argv) < 3:
raise SystemExit(__doc__)
mode, pattern = sys.argv[1], sys.argv[2]
text = sys.argv[3] if len(sys.argv) > 3 else "Escape the Gummer."
model = onnx.load(MODEL)
names = [n for n in candidates(model) if pattern.lower() in n.lower()]
if mode == "--list":
print(f"{len(names)} tensors matching {pattern!r}:")
for name in names[:60]:
print(f" {name}")
if len(names) > 60:
print(f" ... {len(names)-60} more")
return
if mode != "--dump":
raise SystemExit(__doc__)
if not names:
raise SystemExit(f"nothing matches {pattern!r}")
# Promote the chosen tensors to graph outputs.
existing = {o.name for o in model.graph.output}
for name in names:
if name not in existing:
model.graph.output.extend([onnx.ValueInfoProto(name=name)])
ids = rust_tokens(text)
# `pack[len(ps) - 1]`, where len(ps) == len(ids) - 2 (the zero pads).
style = load_voice()[len(ids) - 3].astype(np.float32).reshape(1, 256)
session = ort.InferenceSession(
model.SerializeToString(), providers=["CPUExecutionProvider"]
)
wanted = [o.name for o in session.get_outputs()]
values = session.run(
wanted,
{
"input_ids": np.array([ids], dtype=np.int64),
"style": style,
"speed": np.array([1.0], dtype=np.float32),
},
)
os.makedirs(OUT_DIR, exist_ok=True)
print(f"text: {text}\ntokens: {len(ids)}\n")
for name, value in zip(wanted, values):
if name not in names:
continue
safe = name.strip("/").replace("/", "_")
np.save(f"{OUT_DIR}/{safe}.npy", value)
array = np.asarray(value)
print(f" {name}")
print(f" shape={array.shape} dtype={array.dtype} "
f"mean={array.mean():+.5f} std={array.std():.5f} "
f"min={array.min():+.4f} max={array.max():+.4f}")
if __name__ == "__main__":
main()