From 59cf0fb72bae3a4a7915ce39e2937a4ee9682d08 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Fri, 28 Aug 2026 19:49:35 +0000 Subject: [PATCH 01/17] docs: design ROCm release and container support Assisted-by: Codex:GPT-5 [Codex] --- ...26-08-28-rocm-release-and-docker-design.md | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-28-rocm-release-and-docker-design.md diff --git a/docs/superpowers/specs/2026-08-28-rocm-release-and-docker-design.md b/docs/superpowers/specs/2026-08-28-rocm-release-and-docker-design.md new file mode 100644 index 0000000..2f203e7 --- /dev/null +++ b/docs/superpowers/specs/2026-08-28-rocm-release-and-docker-design.md @@ -0,0 +1,273 @@ +# ROCm release binaries and Docker images + +Date: 2026-08-28 + +## Goal + +Make ROCm a first-class published backend alongside CPU, Vulkan, CUDA, and +Metal. Each tagged release will provide Linux x64 ROCm bundles for the CLI and +server and for the shared C API. The container workflow will publish matching +ROCm CLI and server images. Documentation will describe the supported AMD GPU +targets, host requirements, build flags, artifact names, container tags, and +device passthrough. + +The initial runtime and validation target is ROCm 7.2.4 on Ubuntu 24.04. The +hardware gate is the Ryzen AI Max+ 395 / Radeon 8060S (`gfx1151`) available as +`strix:gpu0` through `rc`. + +## Current evidence + +Clean commit `f469a57` builds with the pinned ggml v0.13.0 HIP backend when +configured for `gfx1151`. On Strix Halo with ROCm 7.2.4, the 110M F16 TDT model +produced the exact reference transcript. + +The warmed transcription measurements for `tests/fixtures/speech.wav` were: + +| Backend | Processing time | Transcript | +| --- | ---: | --- | +| ROCm 7.2.4 | 34.505 ms | Exact reference | +| Vulkan / RADV | 53.914 ms | Exact reference | +| CPU, 8 threads | 68.015 ms | Exact reference | + +These measurements establish viability and guide documentation. They are not a +performance threshold in CI because runner load and driver versions vary. + +## Approaches considered + +### One fat ROCm bundle that uses the host ROCm runtime + +Build one Linux x64 HIP backend containing code objects for a curated set of +AMD architectures. Package parakeet and ggml, but require a compatible ROCm +userspace installation on the host. + +This is the selected approach. It gives users one clearly named artifact and +keeps the release download reasonably sized. It follows the shape of upstream +ggml/llama.cpp ROCm releases. + +### Fully self-contained ROCm release bundle + +Bundle the HIP runtime, hipBLAS, rocBLAS, rocSOLVER, hipBLASLt, and all +architecture databases. This would make the tarball several gigabytes and +couple it tightly to a driver/runtime combination. The installed ROCm 7.2.4 +development stack used for the probe occupied more than 8 GB; hipBLASLt alone +occupied about 4.5 GB. This option is rejected for release tarballs. + +### Separate Radeon and Instinct bundles + +Split code objects and runtime guidance into consumer/APU and datacenter +artifacts. This reduces each individual HIP library but multiplies assets, +documentation paths, and support ambiguity. It remains a fallback only if the +fat HIP artifact exceeds GitHub artifact or release limits. + +## Supported GPU targets + +The Linux x64 release and Docker images will build the following HIP targets: + +```text +gfx908;gfx90a;gfx942;gfx1030;gfx1100;gfx1101;gfx1102;gfx1150;gfx1151;gfx1200;gfx1201 +``` + +This covers the currently relevant ROCm-supported AMD Instinct generations, +RDNA2/RDNA3/RDNA4 Radeon GPUs, and Ryzen AI APUs including Strix Halo. The list +matches the broad target set used by upstream llama.cpp ROCm containers. It is +passed explicitly through `GPU_TARGETS`; builds must not depend on compiler +auto-detection from the GPU-less GitHub runner. + +ROCm images and release artifacts are Linux x86-64 only. CPU and Vulkan remain +the portable choices for AMD hardware outside this target list or on other +operating systems. + +## Build configuration + +The release and Docker builds use: + +```text +-DPARAKEET_GGML_HIP=ON +-DGPU_TARGETS= +-DGGML_HIP_NO_VMM=ON +-DGGML_NATIVE=OFF +``` + +`GGML_HIP_NO_VMM=ON` is required for predictable behavior on Strix Halo, whose +HIP device reports no VMM support. The existing persistent `ggml_gallocr` fast +path and zero-copy device weight realization remain unchanged. + +The HIP compiler and ROCm root are set explicitly when CMake cannot infer them: + +```text +-DCMAKE_HIP_COMPILER=/opt/rocm/lib/llvm/bin/clang++ +-DCMAKE_HIP_COMPILER_ROCM_ROOT=/opt/rocm-7.2.4 +``` + +The pinned ggml HIP backend does not support a fully static ggml build. ROCm +artifacts therefore ship the required ggml shared libraries next to the +executables or `libparakeet.so`, with an `$ORIGIN` runtime search path. This is +an implementation detail of the bundle; the public CLI, server, and C API stay +unchanged. + +## Release assets + +Add one `rocm` / `x64` entry to the Linux release matrix. Use an Ubuntu 24.04 +runner and install the ROCm 7.2.4 HIP compiler, device libraries, hipBLAS, and +rocBLAS development packages from AMD's official repository. + +Tagged releases and manual workflow runs produce: + +```text +parakeet--bin-linux-rocm-x64.tar.gz +parakeet--lib-linux-rocm-x64.tar.gz +``` + +The binary bundle contains: + +- `parakeet-cli` +- `parakeet-server` +- the ggml base, CPU, and HIP shared libraries required by the executables +- `LICENSE` +- `README.md` + +The library bundle contains: + +- `libparakeet.so` +- the ggml base, CPU, and HIP shared libraries required by `libparakeet.so` +- `include/parakeet_capi.h` +- `LICENSE` +- `README.md` + +Both bundles require a compatible ROCm 7.2 userspace installation on the host. +Packaging verifies this boundary with `ldd` from outside the build tree. Every +non-system dependency must resolve either from the bundle or from the +documented ROCm runtime. + +The existing release upload job needs no new publication mechanism: the ROCm +matrix entry emits the same binary and library artifact outputs as the other +Linux backends. + +## Docker images + +Extend the existing Docker matrix with a `rocm` variant for Linux `amd64`. +ROCm is not added to the arm64 matrix. + +The existing image names gain the following tags: + +```text +ghcr.io/mudler/parakeet.cpp-cli:latest-rocm +ghcr.io/mudler/parakeet.cpp-server:latest-rocm +ghcr.io/mudler/parakeet.cpp-cli:-rocm +ghcr.io/mudler/parakeet.cpp-server:-rocm +ghcr.io/mudler/parakeet.cpp-cli:sha--rocm +ghcr.io/mudler/parakeet.cpp-server:sha--rocm +``` + +CPU retains the unsuffixed `latest` tag. CUDA retains `latest-cuda`. + +The Docker build keeps `ubuntu:24.04` as its build and runtime base and +registers AMD's official ROCm 7.2.4 package repository. The build stage installs +the same minimal development set validated on Strix Halo: `hipcc`, `hip-dev`, +`rocm-device-libs`, `hipblas-dev`, and `rocblas-dev`. The ROCm runtime stage +installs `rocm-hip-runtime` and `rocm-hip-libraries`, then receives the staged +ggml shared libraries. Unlike the thin release tarball, the ROCm container is +turnkey once the host kernel driver exposes the GPU devices. + +Users run the image with at least: + +```text +--device=/dev/kfd --device=/dev/dri --group-add video +``` + +Models and audio remain external. The CLI image keeps the `parakeet-cli` +entrypoint, and the server image keeps `parakeet-server --host 0.0.0.0`, exactly +as the CPU and CUDA variants do. + +Pull requests continue building only the CPU Docker variants. ROCm joins CUDA +on pushes to `master`, version tags, and manual dispatch because its multi-GPU +code generation and base image are expensive. The merge job creates a +single-platform `linux/amd64` manifest for each ROCm CLI/server tag, using the +same digest and metadata flow as the other variants. + +## Validation + +### Build-time validation + +The release and container workflows must: + +1. Configure HIP with the explicit target list and `GGML_HIP_NO_VMM=ON`. +2. Build the CLI and server. +3. Build the shared C API library in a separate tree. +4. Run the existing usage-banner smoke checks. +5. Inspect staged binaries and libraries with `ldd` after moving them outside + the build tree. +6. Fail if a required ggml library is missing from the package. + +GitHub-hosted runners do not need an AMD GPU. They compile code objects for the +explicit architecture list and perform non-device packaging checks. + +### Strix Halo hardware gate + +Before merging, copy or check out the implementation branch into the shared +`rc` workspace and run all GPU work through `rc run -d strix:gpu0`. Never run +directly on the GPU host without a lease. + +Validate the release-style build, extracted bundles, and Docker images: + +1. `rocminfo` reports `gfx1151`. +2. parakeet selects `ROCm0`, not CPU or Vulkan. +3. Model-independent tests pass. +4. The extracted CLI bundle transcribes the 110M F16 anchor model and produces + the exact reference transcript. +5. The extracted server bundle serves an OpenAI-compatible transcription + request with the same transcript. +6. The extracted shared-library bundle passes a C-API load, transcribe, and + free smoke test. +7. The CLI ROCm image produces the reference transcript with `/dev/kfd` and + `/dev/dri` passed through. +8. The server ROCm image returns the reference transcript over HTTP. +9. Warmed ROCm, Vulkan, and CPU timings are recorded for documentation and + regression context. + +The transcript is a correctness gate. Timing is informational unless a later +performance specification introduces a stable threshold. + +## Documentation + +Update `README.md` to include: + +- `rocm` in the Linux x64 release matrix +- the two ROCm release asset names +- the supported HIP target list and representative GPU families +- ROCm 7.2 host runtime requirements and an official installation link +- a source-build example with the HIP CMake flags +- automatic `ROCm0` selection and `PARAKEET_DEVICE=ROCm0` override +- the `latest-rocm`, versioned, and commit Docker tags +- Docker device-passthrough examples for both CLI and server +- the Strix Halo correctness and indicative performance results + +Update `examples/server/README.md` with the ROCm server image tag, device +passthrough, and model mounting/fetching examples. + +Comments in `.github/workflows/release.yml`, `.github/workflows/docker.yml`, +and `Dockerfile` must describe CPU, CUDA, and ROCm behavior accurately after +the matrix expansion. + +## Error handling and compatibility + +- If no compiled HIP code object matches the user's GPU, ggml will fail at + device execution. Documentation directs unsupported GPUs to Vulkan or CPU. +- If ROCm userspace is absent for a release tarball, the dynamic loader error + is expected; documentation lists the runtime prerequisite. +- Docker users must expose `/dev/kfd` and `/dev/dri`. Documentation calls this + out next to every ROCm run example. +- `PARAKEET_DEVICE=cpu` continues to force CPU even in a ROCm build. +- The public C and C++ APIs, model format, decoder behavior, and ABI version do + not change. +- No changes may replace the persistent allocator, introduce per-call weight + copies, or route supported HIP graphs through the scheduler fast path. + +## Out of scope + +- Windows HIP/ROCm release artifacts +- ROCm on arm64 +- Installing or replacing the host kernel driver +- Bundling a multi-gigabyte ROCm userspace into release tarballs +- Backend-specific kernel optimization or rocWMMA tuning +- Changing decoding, model conversion, quantization, or the public API From 852c05a989b4f2f80af900acff0bdb828764d234 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 26 Sep 2026 11:05:12 +0000 Subject: [PATCH 02/17] feat(converter): add diarization model support Convert Nemotron-3-Diarization (.nemo) to GGUF with arch="diarization". The converter loads state_dict + config directly from the .nemo tar, bypassing SortformerEncLabelModel.restore_from() (which fails on NeMo versions that don't support self_attention_model='rope'). ModelLoader reads diarization-specific KV pairs (parakeet.diar.*, parakeet.encoder.self_attention_model, rope_base, rotary_fraction) and accepts models with no vocabulary (arch="diarization"). Model::load rejects diarization arch so the C-API falls through to DiarizationModel::load. test_model_loader skips vocab checks for diarization models. --- scripts/convert_parakeet_to_gguf.py | 307 ++++++++++++++++++++++++++-- src/model.cpp | 5 + src/model_loader.cpp | 20 +- src/model_loader.hpp | 20 ++ tests/test_model_loader.cpp | 7 +- 5 files changed, 339 insertions(+), 20 deletions(-) diff --git a/scripts/convert_parakeet_to_gguf.py b/scripts/convert_parakeet_to_gguf.py index 4c9ed45..11c6f12 100644 --- a/scripts/convert_parakeet_to_gguf.py +++ b/scripts/convert_parakeet_to_gguf.py @@ -41,6 +41,85 @@ print("PARAKEET_CONVERT_DEPS_MISSING", file=sys.stderr) sys.exit(2) +# SortformerEncLabelModel import is optional — the installed NeMo may be too old +# to support self_attention_model='rope'. The converter detects diarization from +# the .nemo tar's model_config.yaml and loads state_dict directly, bypassing the +# model class entirely. +SortformerEncLabelModel = None + +import io +import tarfile + +try: + import torch +except ImportError as e: # pragma: no cover - env guard + print(f"converter: missing dependency 'torch': {e}", file=sys.stderr) + print("PARAKEET_CONVERT_DEPS_MISSING", file=sys.stderr) + sys.exit(2) + +try: + import yaml +except ImportError as e: # pragma: no cover - env guard + print(f"converter: missing dependency 'pyyaml': {e}", file=sys.stderr) + print("PARAKEET_CONVERT_DEPS_MISSING", file=sys.stderr) + sys.exit(2) + + +def _load_diarization_from_tar(nemo_path): + """Load state_dict + config from a .nemo (POSIX tar) for diarization models. + + The .nemo tar contains model_config.yaml + model_weights.ckpt. We load + the state_dict directly with torch.load(weights_only=True) and parse the + YAML config, bypassing SortformerEncLabelModel.restore_from() which + fails on NeMo versions that don't support self_attention_model='rope'. + """ + with tarfile.open(nemo_path, "r") as tar: + # Find model_weights.ckpt and model_config.yaml + weight_names = [m.name for m in tar.getmembers() if "weights" in m.name] + config_names = [m.name for m in tar.getmembers() if "config" in m.name and m.name.endswith((".yaml", ".yml"))] + if not weight_names or not config_names: + raise ValueError(f"could not find weights/config in {nemo_path}") + # Extract weights + w_member = tar.extractfile(weight_names[0]) + buf = io.BytesIO(w_member.read()) + state_dict = torch.load(buf, map_location="cpu", weights_only=True) + # Extract config + c_member = tar.extractfile(config_names[0]) + cfg = yaml.safe_load(c_member) + return state_dict, cfg + + +def _is_diarization_nemo(nemo_path): + """Peek at a .nemo tar to check if it's a diarization model.""" + try: + with tarfile.open(nemo_path, "r") as tar: + config_names = [m.name for m in tar.getmembers() + if "config" in m.name and m.name.endswith((".yaml", ".yml"))] + if not config_names: + return False + c_member = tar.extractfile(config_names[0]) + cfg = yaml.safe_load(c_member) + # Diarization models have sortformer_modules or model.sortformer_modules + return "sortformer_modules" in cfg or ( + "model" in cfg and "sortformer_modules" in cfg.get("model", {}) + ) + except Exception: + return False + + +def _get_cfg_value(cfg, dotted_key, default=None): + """Get a value from a nested dict using dotted notation (a.b.c).""" + keys = dotted_key.split(".") + v = cfg + for k in keys: + if isinstance(v, dict): + v = v.get(k, default) + else: + return default + if v is None: + return default + return v + def _get(cfg, key, default=None): """Read ``key`` from an OmegaConf node or plain object, tolerating both.""" @@ -51,9 +130,16 @@ def _get(cfg, key, default=None): def detect_arch(m): - """Map a NeMo model to one of ctc/rnnt/tdt/hybrid_rnnt_ctc/hybrid_tdt_ctc.""" + """Map a NeMo model to one of ctc/rnnt/tdt/hybrid_rnnt_ctc/hybrid_tdt_ctc/diarization.""" + # Diarization model (SortformerEncLabelModel): has sortformer_modules, no + # tokenizer/vocab, no joint/CTC decoder — output is speaker sigmoid logits. + if SortformerEncLabelModel is not None and isinstance(m, SortformerEncLabelModel): + return "diarization" + # Fallback: detect by state_dict keys (works even if the import above failed) + sd = m.state_dict() + if any(k.startswith("sortformer_modules.") for k in sd) and not hasattr(m, "tokenizer"): + return "diarization" cfg = m.cfg - # An aux_ctc *config* block is necessary but not sufficient for a hybrid # model: prompt-conditioned RNNT checkpoints (nemotron) carry an unconfigured # aux_ctc stub (num_classes=-1, empty vocabulary) but NO ctc decoder and zero # ctc_decoder.* weights -- NeMo initializes them RNNT-only. Require an actual @@ -119,6 +205,30 @@ def prompt_config(cfg): # stays F32 -- it is intentionally NOT in this allowlist. r"^joint\.enc\.weight$", r"^joint\.pred\.weight$", + # Diarization speaker head linear weights (sortformer_modules). The + # encoder_proj (512->192), first_hidden_to_hidden (192->192), and + # single_hidden_to_spks (192->8) are all pure ggml_mul_mat inputs. + r"^sortformer_modules\.encoder_proj\.weight$", + r"^sortformer_modules\.first_hidden_to_hidden\.weight$", + r"^sortformer_modules\.single_hidden_to_spks\.weight$", + # Diarization transformer encoder linear weights (pre-LN RoPE Transformer). + # Fused QKV (w_qkv), attention output projection (out_proj), and FFN + # up/down linears (ffn.net.0, ffn.net.3) are all pure ggml_mul_mat inputs. + # FeatureStacking projection (encoder.pre_encode.proj) is also pure linear. + r"^encoder\.layers\.\d+\.attn\.w_qkv\.weight$", + r"^encoder\.layers\.\d+\.attn\.out_proj\.weight$", + r"^encoder\.layers\.\d+\.ffn\.net\.\d+\.weight$", + r"^encoder\.pre_encode\.proj\.weight$", +] + +# Weight names that are safe to skip (unused at inference) for diarization models. +DIAIRIZATION_SKIP = [ + r"^encoder\.pos_enc\.", # RoPE, no positional embedding table + r"^hidden_to_spks", # frozen/unused head variant + r"^spec_augmentation", # training-time augmentation + r"^loss", # training loss modules + r"^sortformer_modules\.hidden_to_spks", # unused 384->8 head + r"^sortformer_modules\.transformer_encoder", # None for this model ] _QUANTIZABLE_RE = [re.compile(p) for p in _QUANTIZABLE_PATTERNS] @@ -160,14 +270,148 @@ def main(): args = ap.parse_args() is_local = pathlib.Path(args.model).exists() - try: - if is_local: - m = ASRModel.restore_from(args.model, map_location="cpu") - else: - m = ASRModel.from_pretrained(args.model, map_location="cpu") - except Exception as e: # pragma: no cover - network/cache guard - print(f"PARAKEET_MODEL_UNAVAILABLE: {e}", file=sys.stderr) - sys.exit(2) + + # ------------------------------------------------------------------ + # Diarization path: load state_dict + config directly from the .nemo + # tar, bypassing SortformerEncLabelModel.restore_from() (which fails on + # NeMo versions that don't support self_attention_model='rope'). + # ------------------------------------------------------------------ + is_diar = is_local and args.model.endswith(".nemo") and _is_diarization_nemo(args.model) + + if is_diar: + sd, model_cfg = _load_diarization_from_tar(args.model) + arch = "diarization" + + w = gguf.GGUFWriter(args.output, "parakeet") + w.add_string("general.name", args.model) + w.add_string("parakeet.arch", arch) + + enc_cfg = model_cfg.get("encoder", {}) + sf_cfg = model_cfg.get("sortformer_modules", {}) + pre_cfg = model_cfg.get("preprocessor", {}) + + # Encoder KVs + d_model = int(_get_cfg_value(enc_cfg, "d_model", 512)) + n_layers = int(_get_cfg_value(enc_cfg, "n_layers", 31)) + n_heads = int(_get_cfg_value(enc_cfg, "n_heads", 8)) + ff_exp = float(_get_cfg_value(enc_cfg, "ff_expansion", 4.0)) + ff_dim = int(d_model * ff_exp) + sub_factor = int(_get_cfg_value(enc_cfg, "subsampling_factor", 8)) + + w.add_uint32("parakeet.encoder.feat_in", int(_get_cfg_value(enc_cfg, "feat_in", 128))) + w.add_uint32("parakeet.encoder.d_model", d_model) + w.add_uint32("parakeet.encoder.n_layers", n_layers) + w.add_uint32("parakeet.encoder.n_heads", n_heads) + w.add_uint32("parakeet.encoder.ff_dim", ff_dim) + w.add_uint32("parakeet.encoder.conv_kernel", 0) # N/A for transformer + w.add_string("parakeet.encoder.conv_norm_type", "layer_norm") + w.add_uint32("parakeet.encoder.subsampling_factor", sub_factor) + w.add_uint32("parakeet.encoder.subsampling_conv_channels", 0) + w.add_bool("parakeet.encoder.xscaling", + bool(_get_cfg_value(enc_cfg, "xscaling", False))) + w.add_uint32("parakeet.encoder.pos_emb_max_len", + int(_get_cfg_value(enc_cfg, "pos_emb_max_len", 5000))) + w.add_bool("parakeet.encoder.use_bias", + bool(_get_cfg_value(enc_cfg, "use_bias", False))) + + # Transformer-specific KVs (RoPE attention) + w.add_string("parakeet.encoder.self_attention_model", + str(_get_cfg_value(enc_cfg, "self_attention_model", "rope"))) + w.add_bool("parakeet.encoder.qkv_bias", + bool(_get_cfg_value(enc_cfg, "qkv_bias", False))) + w.add_bool("parakeet.encoder.pre_block_norm", + bool(_get_cfg_value(enc_cfg, "pre_block_norm", True))) + w.add_float32("parakeet.encoder.rope_base", + float(_get_cfg_value(enc_cfg, "rope_base", 10000.0))) + w.add_float32("parakeet.encoder.rotary_fraction", 1.0) + + # Preprocessor KVs (from flat config — no featurizer object) + sr = int(_get_cfg_value(pre_cfg, "sample_rate", 16000)) + n_mels = int(_get_cfg_value(pre_cfg, "features", 128)) + n_fft = int(_get_cfg_value(pre_cfg, "n_fft", 512)) + win_size = float(_get_cfg_value(pre_cfg, "window_size", 0.025)) + win_stride = float(_get_cfg_value(pre_cfg, "window_stride", 0.01)) + win_length = int(round(win_size * sr)) + hop_length = int(round(win_stride * sr)) + + w.add_uint32("parakeet.preprocessor.sample_rate", sr) + w.add_uint32("parakeet.preprocessor.n_mels", n_mels) + w.add_uint32("parakeet.preprocessor.n_fft", n_fft) + w.add_uint32("parakeet.preprocessor.win_length", win_length) + w.add_uint32("parakeet.preprocessor.hop_length", hop_length) + w.add_float32("parakeet.preprocessor.preemph", + float(_get_cfg_value(pre_cfg, "preemph", 0.97))) + w.add_float32("parakeet.preprocessor.mag_power", 2.0) + w.add_string("parakeet.preprocessor.normalize", + str(_get_cfg_value(pre_cfg, "normalize", "NA"))) + w.add_float32("parakeet.preprocessor.log_zero_guard", 2 ** -24) + + # Diarization head KVs + tf_d_model = int(_get_cfg_value(sf_cfg, "tf_d_model", 192)) + n_spk = int(_get_cfg_value(sf_cfg, "num_spks", 8)) + upsample = sub_factor # high_resolution → 10ms output + + w.add_uint32("parakeet.diar.n_speakers", n_spk) + w.add_uint32("parakeet.diar.tf_d_model", tf_d_model) + w.add_uint32("parakeet.diar.upsample_factor", upsample) + w.add_float32("parakeet.diar.frame_resolution_sec", 0.01) + w.add_float32("parakeet.diar.onset_threshold", 0.5) + w.add_float32("parakeet.diar.offset_threshold", 0.5) + + # Write tensors from state_dict + written = 0 + quantized = 0 + skip_patterns = [re.compile(p) for p in DIAIRIZATION_SKIP] + for name, t in sd.items(): + if any(p.search(name) for p in skip_patterns): + continue + if not hasattr(t, "detach"): + continue + arr = t.detach().cpu().float().numpy() + if arr.ndim == 0: + continue + arr = np.ascontiguousarray(arr, dtype=np.float32) + ggml_ne = list(arr.shape[::-1]) + qtype = should_quantize(name, ggml_ne, args.dtype) + if qtype is None: + w.add_tensor(name, arr) + else: + raw = gguf.quantize(arr, qtype) + w.add_tensor(name, raw, raw_shape=raw.shape, raw_dtype=qtype) + quantized += 1 + written += 1 + + w.write_header_to_file() + w.write_kv_data_to_file() + w.write_tensors_to_file() + w.close() + print( + f"wrote {args.output}: arch={arch} tensors={written} " + f"dtype={args.dtype} quantized={quantized}" + ) + return + + # ------------------------------------------------------------------ + # ASR path: load via NeMo model class (as before) + # ------------------------------------------------------------------ + m = None + if SortformerEncLabelModel is not None: + try: + if is_local: + m = SortformerEncLabelModel.restore_from(args.model, map_location="cpu") + else: + m = SortformerEncLabelModel.from_pretrained(args.model, map_location="cpu") + except Exception: + m = None # not a diarization model, fall through to ASRModel + if m is None: + try: + if is_local: + m = ASRModel.restore_from(args.model, map_location="cpu") + else: + m = ASRModel.from_pretrained(args.model, map_location="cpu") + except Exception as e: # pragma: no cover - network/cache guard + print(f"PARAKEET_MODEL_UNAVAILABLE: {e}", file=sys.stderr) + sys.exit(2) m.eval() arch = detect_arch(m) @@ -294,12 +538,34 @@ def _int_list(v): w.add_float32("parakeet.preprocessor.log_zero_guard", float(lzg) if isinstance(lzg, (int, float)) else 2 ** -24) - # vocab / tokenizer - vocab = int(m.tokenizer.vocab_size) - w.add_uint32("parakeet.vocab_size", vocab) - w.add_uint32("parakeet.blank_id", vocab) # blank always == vocab_size - pieces = [m.tokenizer.ids_to_tokens([i])[0] for i in range(vocab)] - w.add_array("parakeet.tokenizer.pieces", [str(p) for p in pieces]) + # vocab / tokenizer (ASR models only — diarization has no tokenizer) + vocab = 0 + if arch != "diarization": + vocab = int(m.tokenizer.vocab_size) + w.add_uint32("parakeet.vocab_size", vocab) + w.add_uint32("parakeet.blank_id", vocab) # blank always == vocab_size + pieces = [m.tokenizer.ids_to_tokens([i])[0] for i in range(vocab)] + w.add_array("parakeet.tokenizer.pieces", [str(p) for p in pieces]) + + # diarization config (SortformerEncLabelModel) + if arch == "diarization": + sf = m.sortformer_modules + # Speaker head dimensions + tf_d_model = int(sf.tf_d_model) if hasattr(sf, "tf_d_model") else 192 + n_spk = int(sf.n_speakers) if hasattr(sf, "n_speakers") else 8 + # Upsample factor = subsampling_factor (high_resolution=True → 10ms frames) + upsample = int(_get(enc, "subsampling_factor", 8)) + # Thresholds from cfg or NeMo defaults + diar_cfg = _get(cfg, "diarizer", {}) or {} + cfg_clustering = _get(diar_cfg, "clustering", {}) or {} + onset = float(_get(diar_cfg, "onset", 0.5)) + offset = float(_get(diar_cfg, "offset", 0.5)) + w.add_uint32("parakeet.diar.n_speakers", n_spk) + w.add_uint32("parakeet.diar.tf_d_model", tf_d_model) + w.add_uint32("parakeet.diar.upsample_factor", upsample) + w.add_float32("parakeet.diar.frame_resolution_sec", 0.01) + w.add_float32("parakeet.diar.onset_threshold", onset) + w.add_float32("parakeet.diar.offset_threshold", offset) # transducer config if arch in ("rnnt", "tdt", "hybrid_rnnt_ctc", "hybrid_tdt_ctc"): @@ -333,7 +599,15 @@ def _int_list(v): written = 0 quantized = 0 keep_buffers = {"preprocessor.featurizer.fb", "preprocessor.featurizer.window"} + # Frozen/unused weights to skip (diarization: hidden_to_spks is a frozen + # placeholder that is never called in offline inference). + skip_names = set() + if arch == "diarization": + skip_names.add("sortformer_modules.hidden_to_spks.weight") + skip_names.add("sortformer_modules.hidden_to_spks.bias") for name, t in sd.items(): + if name in skip_names: + continue if name.startswith("preprocessor.") and name not in keep_buffers: continue # skip preprocessor internals except fb/window if not hasattr(t, "detach"): @@ -365,6 +639,5 @@ def _int_list(v): f"dtype={args.dtype} quantized={quantized}" ) - if __name__ == "__main__": main() diff --git a/src/model.cpp b/src/model.cpp index b9d812a..80a4120 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -45,6 +45,11 @@ std::unique_ptr Model::load(const std::string& gguf_path) { if (!m->loader_.load(gguf_path)) { return nullptr; } + // Model is the ASR entry point — reject diarization models so the C-API + // can fall through to DiarizationModel::load. + if (m->loader_.config().arch == "diarization") { + return nullptr; + } // Give the weights a CPU backend buffer ONCE so graphs reference them // directly as leaves (zero per-call copy). Done at load (vs. lazily on first // clone_weight) so the cost is paid up front, not per utterance. diff --git a/src/model_loader.cpp b/src/model_loader.cpp index 9d3bfe4..f7d37f8 100644 --- a/src/model_loader.cpp +++ b/src/model_loader.cpp @@ -154,6 +154,13 @@ bool ModelLoader::load(const std::string& path){ // encoder.use_bias: false for nemotron (the attention/FFN linear projections // carry no bias tensor). Defaults true so existing models are unaffected. cfg_.use_bias = kv_bool(gguf_, "parakeet.encoder.use_bias", true); + // Transformer encoder config (diarization models with RoPE attention). + // Absent for ASR (FastConformer) models → safe defaults. + cfg_.self_attention_model = kv_str(gguf_, "parakeet.encoder.self_attention_model", ""); + cfg_.qkv_bias = kv_bool(gguf_, "parakeet.encoder.qkv_bias", false); + cfg_.pre_block_norm = kv_bool(gguf_, "parakeet.encoder.pre_block_norm", true); + cfg_.rope_base = kv_f32(gguf_, "parakeet.encoder.rope_base", 10000.0f); + cfg_.rotary_fraction = kv_f32(gguf_, "parakeet.encoder.rotary_fraction", 1.0f); // Prompt conditioning (multilingual nemotron). Orthogonal capability flag; // absent -> present=false and the engine skips the prompt stage entirely. cfg_.prompt.present = kv_bool(gguf_, "parakeet.prompt.present", false); @@ -190,6 +197,17 @@ bool ModelLoader::load(const std::string& path){ cfg_.max_symbols = kv_u32(gguf_, "parakeet.decoding.max_symbols", 10); cfg_.vocab_size = kv_u32(gguf_, "parakeet.vocab_size"); cfg_.blank_id = kv_u32(gguf_, "parakeet.blank_id"); + // diarization config (absent for ASR models → present=false) + if (gguf_find_key(gguf_, "parakeet.diar.n_speakers") >= 0) { + auto& d = cfg_.diarization; + d.present = true; + d.n_speakers = kv_u32(gguf_, "parakeet.diar.n_speakers"); + d.tf_d_model = kv_u32(gguf_, "parakeet.diar.tf_d_model"); + d.upsample_factor = kv_u32(gguf_, "parakeet.diar.upsample_factor"); + d.frame_resolution_sec = kv_f32(gguf_, "parakeet.diar.frame_resolution_sec", 0.01f); + d.onset_threshold = kv_f32(gguf_, "parakeet.diar.onset_threshold", 0.5f); + d.offset_threshold = kv_f32(gguf_, "parakeet.diar.offset_threshold", 0.5f); + } // durations array (stored as INT32 by the converter) { int64_t id = gguf_find_key(gguf_, "parakeet.tdt.durations"); if(id>=0 && gguf_get_arr_type(gguf_,id)==GGUF_TYPE_INT32){ @@ -207,7 +225,7 @@ bool ModelLoader::load(const std::string& path){ const int64_t nt = gguf_get_n_tensors(gguf_); for(int64_t i=0;i0 && cfg_.vocab_size>0; + return cfg_.d_model>0 && (cfg_.vocab_size>0 || cfg_.arch=="diarization"); } ggml_tensor* ModelLoader::tensor(const std::string& n) const { auto it = tensors_.find(n); return it==tensors_.end()? nullptr : it->second; diff --git a/src/model_loader.hpp b/src/model_loader.hpp index f181f24..0d35636 100644 --- a/src/model_loader.hpp +++ b/src/model_loader.hpp @@ -71,6 +71,26 @@ struct ParakeetConfig { // vocab uint32_t vocab_size=0, blank_id=0; std::vector tokenizer_pieces; + // diarization (SortformerEncLabelModel). present=false for ASR models. + // The diarization head sits after the transformer encoder: encoder_proj + // → subpixel_upsample → speaker sigmoid head. See docs/diarization-plan.md. + struct DiarizationCfg { + bool present=false; + uint32_t n_speakers=0; // max speakers (8 for Nemotron-3-Diarization) + uint32_t tf_d_model=0; // sortformer hidden dim (192) + uint32_t upsample_factor=0; // = subsampling_factor (8 → 10ms frames) + float frame_resolution_sec=0.01f; // output frame duration + float onset_threshold=0.5f; // hysteresis onset + float offset_threshold=0.5f; // hysteresis offset + } diarization; + // Diarization encoder config (Nemotron-3-Diarization uses a TransformerEncoder + // with RoPE, not a FastConformer). These are read from parakeet.encoder.* KVs + // but only meaningful when arch == "diarization". + std::string self_attention_model; // "rope" for Nemotron-3-Diarization + bool qkv_bias=false; // QKV projection bias (false) + bool pre_block_norm=true; // embed_norm before blocks (true) + float rope_base=10000.0f; // RoPE theta + float rotary_fraction=1.0f; // fraction of head_dim rotated }; class ModelLoader { public: diff --git a/tests/test_model_loader.cpp b/tests/test_model_loader.cpp index 04df63b..d6c5f48 100644 --- a/tests/test_model_loader.cpp +++ b/tests/test_model_loader.cpp @@ -18,8 +18,11 @@ int main() { const pk::ParakeetConfig& c = ml.config(); if (c.arch.empty()) { std::fprintf(stderr, "empty arch\n"); return 1; } if (c.d_model == 0 || c.n_layers == 0 || c.n_heads == 0) { std::fprintf(stderr, "bad encoder dims\n"); return 1; } - if (c.vocab_size == 0) { std::fprintf(stderr, "bad vocab\n"); return 1; } - if (c.blank_id != c.vocab_size) { std::fprintf(stderr, "blank!=vocab\n"); return 1; } + // Diarization models have no vocab (no text output); skip vocab checks. + if (c.arch != "diarization") { + if (c.vocab_size == 0) { std::fprintf(stderr, "bad vocab\n"); return 1; } + if (c.blank_id != c.vocab_size) { std::fprintf(stderr, "blank!=vocab\n"); return 1; } + } // mel filterbank tensor must be present if (ml.tensor("preprocessor.featurizer.fb") == nullptr) { std::fprintf(stderr, "no fb\n"); return 1; } // first conformer layer norm must be present (verbatim name) From 3cbbde8147ca676dd7801f20a3c9ebe6095c03cb Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 26 Sep 2026 11:06:31 +0000 Subject: [PATCH 03/17] feat(diarization): add encoder and speaker head DiarizationEncoder: 31-layer pre-LN RoPE Transformer (d_model=512, 8 heads, head_dim=64, ff_dim=2048). Uses ggml_flash_attn_ext for attention (CPU). FeatureStacking 8x subsampling in pre_encode. Key ggml workarounds: - flash_attn_ext returns [hd,H,T,1] not [hd,T,H,1]: no permute, just cont + reshape_2d - 3D ggml_cont after ggml_permute is broken (creates view not copy): use 2D transpose + cont instead DiarizationHead: encoder_proj (512->192), subpixel Conv1D 8x upsampling, sigmoid speaker head (192->8). Subpixel reshape uses reshape_3d -> reshape_2d -> 2D transpose+cont to avoid the 3D cont bug. Output is [n_spk, T_out] row-major via cont(transpose(sig)). Parity vs PyTorch reference: probs max_diff=0.0097 (F16 noise). --- src/diarization_encoder.cpp | 222 ++++++++++++++++++++++++++++++++++++ src/diarization_encoder.hpp | 49 ++++++++ src/diarization_head.cpp | 168 +++++++++++++++++++++++++++ src/diarization_head.hpp | 41 +++++++ 4 files changed, 480 insertions(+) create mode 100644 src/diarization_encoder.cpp create mode 100644 src/diarization_encoder.hpp create mode 100644 src/diarization_head.cpp create mode 100644 src/diarization_head.hpp diff --git a/src/diarization_encoder.cpp b/src/diarization_encoder.cpp new file mode 100644 index 0000000..6a03ef4 --- /dev/null +++ b/src/diarization_encoder.cpp @@ -0,0 +1,222 @@ +#include "diarization_encoder.hpp" +#include "backend.hpp" +#include "graph_builder.hpp" +#include "ggml_graph.hpp" +#include "ggml.h" + +#include +#include +#include +#include + +namespace pk { + +// ============================================================================ +// DiarizationEncoder — pre-LN RoPE Transformer for Nemotron-3-Diarization. +// +// mel [n_mels=128, T] +// → FeatureStacking: transpose, pad, reshape [1024, T/8], Linear(1024→512) +// → embed_norm: LayerNorm(512, eps=1e-5) +// → 31× TransformerBlock (pre-norm): +// x = x + attn(norm1(x)) +// x = x + ffn(norm2(x)) +// → final_norm: LayerNorm(512, eps=1e-5) +// → transpose → output [d_model, T_enc] (channels-first: enc_out[c*Tp+t]) +// ============================================================================ + +DiarizationEncoder::DiarizationEncoder(const ModelLoader& ml) + : ml_(ml) { + const auto& cfg = ml.config(); + d_model_ = (int)cfg.d_model; + n_layers_ = (int)cfg.n_layers; + n_heads_ = (int)cfg.n_heads; + head_dim_ = d_model_ / n_heads_; + ff_dim_ = (int)cfg.ff_dim; + subsampling_factor_ = (int)cfg.subsampling_factor; + n_mels_ = (int)cfg.n_mels; + qkv_bias_ = cfg.use_bias; + pre_block_norm_ = true; + rope_base_ = 10000.0f; + rotary_fraction_ = 1.0f; + ln_eps_ = 1e-5f; + + assert(n_layers_ > 0 && d_model_ > 0); + assert(subsampling_factor_ > 0); + assert(head_dim_ * n_heads_ == d_model_); +} + +void DiarizationEncoder::forward(const std::vector& mel, int n_mels, int T, + std::vector& enc_out, + int& d_model, int& T_enc) const { + assert(n_mels == n_mels_); + assert((int)mel.size() == n_mels * T); + + const int factor = subsampling_factor_; // 8 + const int pad = (factor - (T % factor)) % factor; + const int T_padded = T + pad; + const int Tp = T_padded / factor; + + std::vector positions(Tp); + for (int i = 0; i < Tp; ++i) positions[i] = i; + + const ModelLoader& ml = ml_; + const int d = d_model_; + const int H = n_heads_; + const int hd = head_dim_; + const int nls = n_layers_; + const float ln_eps = ln_eps_; + const float rope_base = rope_base_; + const int n_rot = (int)(hd * rotary_fraction_); + + pk::ensure_weights_realized(ml); + GraphInputPool pool; + + bool ok = pk::run_graph(0, 0, + [&](ggml_context* ctx) -> ggml_tensor* { + // --- 1. FeatureStacking --- + // mel is [n_mels, T] row-major: mel[m*T + t] + // Copy into padded buffer [n_mels, T_padded] + int64_t mel_ne[2] = {T_padded, n_mels}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels * T_padded); + for (int m = 0; m < n_mels; ++m) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)m * T_padded + t] = mel[(size_t)m * T + t]; + + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + // mel_t: ne[0]=T_padded, ne[1]=n_mels + + // NeMo FeatureStacking transposes [C, T] → [T, C] before reshape. + // After transpose: ne[0]=n_mels, ne[1]=T_padded (time-major) + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + + // Reshape [T_padded, n_mels] → [n_mels*factor, Tp] = [1024, Tp] + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels * factor, Tp); + + // Linear(1024 → 512, no bias) + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); // [d_model, Tp] + + // --- 2. embed_norm --- + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + ggml_tensor* y = ggml_norm(ctx, x, ln_eps); + x = ggml_add(ctx, ggml_mul(ctx, y, g), b); + } + + // --- 3. Position tensor for RoPE --- + int64_t pos_ne[1] = {Tp}; + ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, + positions.data(), (size_t)Tp * sizeof(int32_t)); + + // --- 4. N × TransformerBlock (pre-norm) --- + for (int i = 0; i < nls; ++i) { + std::string base = "encoder.layers." + std::to_string(i) + "."; + + // norm1 + { + ggml_tensor* g = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* b = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_add(ctx, ggml_mul(ctx, h, g), b); + + // Fused QKV (no bias) + ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); + ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); // [3*d, Tp] + qkv = ggml_cont(ctx, qkv); + qkv = ggml_reshape_3d(ctx, qkv, d, 3, Tp); // [d, 3, Tp] + + // Split Q, K, V + size_t ts = (size_t)3 * d * sizeof(float); + ggml_tensor* q = ggml_view_2d(ctx, qkv, d, Tp, ts, 0); + ggml_tensor* k = ggml_view_2d(ctx, qkv, d, Tp, ts, (size_t)d * sizeof(float)); + ggml_tensor* v = ggml_view_2d(ctx, qkv, d, Tp, ts, (size_t)2 * d * sizeof(float)); + + // Reshape to [hd, H, Tp] + q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), hd, H, Tp); + k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), hd, H, Tp); + v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), hd, H, Tp); + + // RoPE (GPT-NeoX) + q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, + GGML_ROPE_TYPE_NEOX, 0, rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, + GGML_ROPE_TYPE_NEOX, 0, rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + // Permute to [hd, Tp, H] (head in batch dim) + q = ggml_permute(ctx, q, 0, 2, 1, 3); + k = ggml_permute(ctx, k, 0, 2, 1, 3); + v = ggml_permute(ctx, v, 0, 2, 1, 3); + + // flash_attn_ext: q [hd, Tp, H], k [hd, Tp, H], v [hd, Tp, H] + // Result: ne = [hd, H, Tp, 1] (ggml.c line 5357-5358) + // Memory order: flat[t*H*hd + h*hd + d] = [T, H, hd] + // This is ALREADY the correct PyTorch merge order + // (attn.transpose(1,2).contiguous().view(B,T,d_model)). + // Do NOT permute — just cont + reshape. + float scale = 1.0f / std::sqrt((float)hd); + ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, + scale, 0.0f, 0.0f); + attn = ggml_cont(ctx, attn); + attn = ggml_reshape_2d(ctx, attn, (int64_t)d, (int64_t)Tp); + + // out_proj (with bias) + ggml_tensor* op_w = pk::clone_weight(ctx, ml, (base + "attn.out_proj.weight").c_str()); + attn = ggml_mul_mat(ctx, op_w, attn); + ggml_tensor* op_b = pk::clone_weight_opt(ctx, ml, (base + "attn.out_proj.bias").c_str()); + if (op_b) attn = ggml_add(ctx, attn, op_b); + + // Residual + x = ggml_add(ctx, x, attn); + } + + // norm2 + FFN + { + ggml_tensor* g = pk::clone_weight(ctx, ml, (base + "norm2.weight").c_str()); + ggml_tensor* b = pk::clone_weight(ctx, ml, (base + "norm2.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_add(ctx, ggml_mul(ctx, h, g), b); + + // FFN: Linear(d→ff, bias) → GELU → Linear(ff→d, bias) + ggml_tensor* f0_w = pk::clone_weight(ctx, ml, (base + "ffn.net.0.weight").c_str()); + h = ggml_mul_mat(ctx, f0_w, h); + ggml_tensor* f0_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.0.bias").c_str()); + if (f0_b) h = ggml_add(ctx, h, f0_b); + h = ggml_gelu(ctx, h); + + ggml_tensor* f3_w = pk::clone_weight(ctx, ml, (base + "ffn.net.3.weight").c_str()); + h = ggml_mul_mat(ctx, f3_w, h); + ggml_tensor* f3_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.3.bias").c_str()); + if (f3_b) h = ggml_add(ctx, h, f3_b); + + x = ggml_add(ctx, x, h); + } + } + + // --- 5. final_norm --- + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.final_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.final_norm.bias"); + ggml_tensor* y = ggml_norm(ctx, x, ln_eps); + x = ggml_add(ctx, ggml_mul(ctx, y, g), b); + } + + // --- 6. Transpose to channels-first --- + // x: [d_model, Tp] (ne[0]=d, ne[1]=Tp) + // ggml flat: [d0_t0, d1_t0, ..., d511_t0, d0_t1, ...] = time-major + // We need channels-first: enc_out[c*Tp + t] + // transpose → [Tp, d_model], cont → flat[c*Tp + t] ✓ + x = ggml_cont(ctx, ggml_transpose(ctx, x)); + return x; + }, enc_out); + + assert(ok && "diarization encoder graph failed"); + (void)ok; + + d_model = d_model_; + T_enc = Tp; +} + +} // namespace pk diff --git a/src/diarization_encoder.hpp b/src/diarization_encoder.hpp new file mode 100644 index 0000000..1a927e4 --- /dev/null +++ b/src/diarization_encoder.hpp @@ -0,0 +1,49 @@ +#pragma once +#include "model_loader.hpp" +#include + +struct ggml_context; +struct ggml_tensor; + +namespace pk { + +// DiarizationEncoder — pre-LN RoPE Transformer encoder for Nemotron-3-Diarization. +// +// This is NOT the FastConformer encoder used by ASR. Nemotron-3-Diarization uses +// a TransformerEncoder (not ConformerEncoder) with: +// - FeatureStacking subsampling (8× stack → Linear, no bias) +// - Pre-block LayerNorm (embed_norm) +// - N × TransformerBlock (pre-norm): x = x + attn(norm1(x)); x = x + ffn(norm2(x)) +// - MultiHeadAttention: fused QKV (no bias) → RoPE → flash_attn → out_proj (bias) +// - FeedForward: Linear → GELU → Linear (both with bias) +// - Post-block LayerNorm (final_norm) +// - RoPE: GPT-NeoX convention (GGML_ROPE_TYPE_NEOX), theta=10000, rotary_fraction=1.0 +// +// Input: mel features [n_mels, T] (row-major: mel[m*T + t]) +// Output: enc_out [d_model, T_enc] (row-major: enc_out[c*T_enc + t], channels-first) +class DiarizationEncoder { +public: + explicit DiarizationEncoder(const ModelLoader& ml); + + // mel: row-major [n_mels, T] — mel[m*T + t] + // enc_out: row-major [d_model, T_enc] — enc_out[c*T_enc + t] (channels-first) + void forward(const std::vector& mel, int n_mels, int T, + std::vector& enc_out, int& d_model, int& T_enc) const; + +private: + const ModelLoader& ml_; + int d_model_; // encoder d_model (512) + int n_layers_; // number of transformer blocks (31) + int n_heads_; // attention heads (8) + int head_dim_; // d_model / n_heads (64) + int ff_dim_; // feed-forward inner dim (2048) + int subsampling_factor_; // FeatureStacking factor (8) + int n_mels_; // mel features (128) + bool qkv_bias_; // QKV projection bias (false) + bool pre_block_norm_; // apply embed_norm before blocks (true) + float rope_base_; // RoPE theta (10000.0) + float rotary_fraction_; // fraction of head_dim rotated (1.0) + float ln_eps_; // LayerNorm epsilon (1e-5) +}; + +} // namespace pk diff --git a/src/diarization_head.cpp b/src/diarization_head.cpp new file mode 100644 index 0000000..d5c435d --- /dev/null +++ b/src/diarization_head.cpp @@ -0,0 +1,168 @@ +#include "diarization_head.hpp" +#include "ggml_graph.hpp" +#include "backend.hpp" +#include "ggml.h" +#include +#include +#include +#include + +namespace pk { + +DiarizationHead::DiarizationHead(const ModelLoader& ml) : ml_(ml) { + const auto& cfg = ml.config(); + d_model_ = (int)cfg.d_model; + tf_d_model_ = (int)cfg.diarization.tf_d_model; + n_spk_ = (int)cfg.diarization.n_speakers; + upsample_ = (int)cfg.diarization.upsample_factor; +} + +void DiarizationHead::forward(const std::vector& enc_out, int d_model, int T_enc, + std::vector& probs, int& n_spk, int& T_out) const { + assert(d_model == d_model_); + assert((int)enc_out.size() == d_model * T_enc); + + n_spk = n_spk_; + const int up = upsample_; + T_out = T_enc * up; + + // Memory budget: encoder_proj [d_model, tf], subpixel conv [tf*up, tf, 3], + // intermediate [tf*up, T_enc], speaker linears, output [n_spk, T_out]. + const size_t mem_bytes = + (size_t)128 * 1024 * 1024 + + (size_t)(d_model * tf_d_model_ + tf_d_model_ * up * tf_d_model_ * 3 + + tf_d_model_ * up * T_enc + tf_d_model_ * T_out + + n_spk_ * T_out) * sizeof(float) * 4; + + const ModelLoader& ml = ml_; + const int tf = tf_d_model_; + const int ns = n_spk_; + + pk::ensure_weights_realized(ml); + + bool ok = pk::run_graph(mem_bytes, /*n_threads=*/4, + [&](ggml_context* ctx) -> ggml_tensor* { + // ---- Input: enc_out [d_model, T_enc] row-major, enc_out[c*T_enc + t] + // ggml: ne[0]=T_enc (fastest), ne[1]=d_model + int64_t xt_ne[2] = {T_enc, d_model}; + ggml_tensor* xt = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, xt_ne, + enc_out.data(), (size_t)d_model * T_enc * sizeof(float)); + + // ---- encoder_proj: Linear(d_model → tf_d_model) ---- + // Weight: PyTorch [tf, d_model] → ggml ne[0]=d_model, ne[1]=tf + ggml_tensor* ep_w = ml.tensor("sortformer_modules.encoder_proj.weight"); + if (!ep_w) throw std::runtime_error("parakeet: missing sortformer_modules.encoder_proj.weight"); + ggml_tensor* W_proj = ggml_reshape_2d(ctx, ep_w, d_model, tf); + + // Transpose xt so ne[0]=d_model (contraction dim) + ggml_tensor* xt_t = ggml_cont(ctx, ggml_transpose(ctx, xt)); + // xt_t: ne[0]=d_model, ne[1]=T_enc + + ggml_tensor* proj = ggml_mul_mat(ctx, W_proj, xt_t); + // proj: ne[0]=tf, ne[1]=T_enc + + // Add encoder_proj bias [tf] + ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); + if (ep_b) proj = ggml_add(ctx, proj, ep_b); + + // ---- subpixel_upsample: Conv1d(tf → tf*up, k=3, pad=1) + bias ---- + // Conv1d expects data as ne[0]=T, ne[1]=IC, ne[2]=N + // proj is ne[0]=tf, ne[1]=T_enc → need transpose + ggml_tensor* conv_in = ggml_cont(ctx, ggml_transpose(ctx, proj)); + // conv_in: ne[0]=T_enc, ne[1]=tf + conv_in = ggml_reshape_3d(ctx, conv_in, T_enc, tf, 1); + // conv_in: ne[0]=T_enc, ne[1]=tf, ne[2]=1 + + // Conv1d weight: GGUF stores ne=[k, IC, OC]=[3, 192, 1536] (converter + // already wrote it in ggml layout). Use directly. + ggml_tensor* spk_w = ml.tensor("sortformer_modules.subpixel_upsample.weight"); + if (!spk_w) throw std::runtime_error("parakeet: missing sortformer_modules.subpixel_upsample.weight"); + + // ggml's CPU im2col expects the conv kernel to be F16 (assertion + // in ggml_compute_forward_im2col_f16). Cast it explicitly. + spk_w = ggml_cast(ctx, spk_w, GGML_TYPE_F16); + // Also cast the input to F16 for the im2col path + conv_in = ggml_cast(ctx, conv_in, GGML_TYPE_F16); + + // ggml_conv_1d(ctx, kernel, data, stride=1, pad=1, dilation=1) + ggml_tensor* conv_out = ggml_conv_1d(ctx, spk_w, conv_in, 1, 1, 1); + // conv_out: ne[0]=T_enc, ne[1]=tf*up, ne[2]=1 + // Data layout: flat[t + c*T_enc] (ne[0]=T_enc fastest) + + // Reshape to 2D (keep ne[0]=T_enc, ne[1]=tf*up) + conv_out = ggml_reshape_2d(ctx, conv_out, T_enc, tf * up); + // ne[0]=T_enc, ne[1]=tf*up, data: flat[t + c*T_enc] + + // Add subpixel bias [tf*up] directly. Reshape to [1, tf*up] + // so ggml_add broadcasts over ne[0]=T_enc. + ggml_tensor* spk_b = ml.tensor("sortformer_modules.subpixel_upsample.bias"); + if (spk_b) { + ggml_tensor* spk_b_2d = ggml_reshape_2d(ctx, spk_b, 1, tf * up); + conv_out = ggml_add(ctx, conv_out, spk_b_2d); + } + + // Subpixel reshape: conv_out is ne=[T_enc, tf*up], data: flat[t + c*T_enc]. + // + // Reference PyTorch: x.view(B, C//up, up, T) then x.view(B, C//up, up*T) + // → up_pk[h, t'] = conv[h*up+u, t] where t' = u*T + t + // + // In ggml (column-major, ne[0] fastest): + // 1. reshape_3d(T_enc, up, tf): ne=[T_enc, up, tf] + // element(t,u,h) = flat[t + u*T_enc + h*up*T_enc] = flat[t + c*T_enc] ✓ + // 2. reshape_2d(T_out, tf): ne=[T_out, tf] + // element(t',h) = flat[t' + h*T_out] where t' = t + u*T_enc ✓ + // 3. transpose: ne=[tf, T_out] + // 4. cont: copies to flat[h + t'*tf] (2D cont works correctly) + // + // NOTE: 3D permute+cont is BROKEN in this ggml backend — the cont op + // does not actually rearrange data for 3D tensors. Using 2D + // transpose+cont avoids this bug. + ggml_tensor* upsampled = ggml_reshape_3d(ctx, conv_out, T_enc, up, tf); + upsampled = ggml_reshape_2d(ctx, upsampled, T_enc * up, tf); + upsampled = ggml_cont(ctx, ggml_transpose(ctx, upsampled)); + // ne[0]=tf, ne[1]=T_out + + // ---- forward_speaker_logits: relu → Linear(tf→tf) → relu → Linear(tf→ns) → sigmoid ---- + + // First ReLU + ggml_tensor* h = ggml_relu(ctx, upsampled); + + // first_hidden_to_hidden: Linear(tf → tf) + ggml_tensor* fh_w = ml.tensor("sortformer_modules.first_hidden_to_hidden.weight"); + if (!fh_w) throw std::runtime_error("parakeet: missing sortformer_modules.first_hidden_to_hidden.weight"); + ggml_tensor* W1 = ggml_reshape_2d(ctx, fh_w, tf, tf); + // W1: ne[0]=tf, ne[1]=tf. h: ne[0]=tf, ne[1]=T_out + h = ggml_mul_mat(ctx, W1, h); + // h: ne[0]=tf, ne[1]=T_out + ggml_tensor* fh_b = ml.tensor("sortformer_modules.first_hidden_to_hidden.bias"); + if (fh_b) h = ggml_add(ctx, h, fh_b); + + // Second ReLU + h = ggml_relu(ctx, h); + + // single_hidden_to_spks: Linear(tf → ns) + ggml_tensor* ss_w = ml.tensor("sortformer_modules.single_hidden_to_spks.weight"); + if (!ss_w) throw std::runtime_error("parakeet: missing sortformer_modules.single_hidden_to_spks.weight"); + ggml_tensor* W2 = ggml_reshape_2d(ctx, ss_w, tf, ns); + // W2: ne[0]=tf, ne[1]=ns. h: ne[0]=tf, ne[1]=T_out + h = ggml_mul_mat(ctx, W2, h); + // h: ne[0]=ns, ne[1]=T_out + ggml_tensor* ss_b = ml.tensor("sortformer_modules.single_hidden_to_spks.bias"); + if (ss_b) h = ggml_add(ctx, h, ss_b); + + // Sigmoid → speaker probabilities + ggml_tensor* sig = ggml_sigmoid(ctx, h); + // sig: ne[0]=ns, ne[1]=T_out → memory: probs[t*ns + s] + // postprocess expects row-major: probs[s*T_out + t] + // Transpose to ne=[T_out, ns] + cont → flat[t + s*T_out] + sig = ggml_cont(ctx, ggml_transpose(ctx, sig)); + + return sig; + }, + probs); + + assert(ok && "diarization_head graph failed"); + (void)ok; +} + +} // namespace pk diff --git a/src/diarization_head.hpp b/src/diarization_head.hpp new file mode 100644 index 0000000..29068a3 --- /dev/null +++ b/src/diarization_head.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "model_loader.hpp" +#include + +namespace pk { + +// Diarization head — NeMo SortformerModules forward_speaker_logits + upsample. +// +// Architecture (offline path, transformer_encoder is None for Nemotron-3): +// enc_out [d_model, T_enc] (channels-first, from FastConformer encoder) +// → encoder_proj: Linear(d_model → tf_d_model) → [tf_d_model, T_enc] +// → subpixel_upsample: Conv1d(tf_d_model → tf_d_model*upsample, k=3, pad=1) +// → reshape → [tf_d_model, T_enc * upsample] +// → relu → first_hidden_to_hidden: Linear(tf_d_model → tf_d_model) +// → relu → single_hidden_to_spks: Linear(tf_d_model → n_speakers) +// → sigmoid +// → probs [n_speakers, T_enc * upsample] +// +// Weight names (verbatim from state dict): +// sortformer_modules.encoder_proj.{weight,bias} +// sortformer_modules.subpixel_upsample.{weight,bias} +// sortformer_modules.first_hidden_to_hidden.{weight,bias} +// sortformer_modules.single_hidden_to_spks.{weight,bias} +class DiarizationHead { +public: + explicit DiarizationHead(const ModelLoader& ml); + + // enc_out: row-major [d_model, T_enc] — enc_out[c*T_enc + t] (channels-first) + // probs: row-major [n_speakers, T_out] — probs[s*T_out + t] (post-sigmoid) + void forward(const std::vector& enc_out, int d_model, int T_enc, + std::vector& probs, int& n_spk, int& T_out) const; + +private: + const ModelLoader& ml_; + int d_model_; // encoder d_model (512) + int tf_d_model_; // sortformer hidden (192) + int n_spk_; // number of speakers (8) + int upsample_; // upsample factor (8) +}; + +} // namespace pk From 5b3a17287e4ee98e6faf472566055363a3f95177 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 26 Sep 2026 11:08:51 +0000 Subject: [PATCH 04/17] feat(diarization): add DiarizationModel and SAS merge layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DiarizationModel: loads diarization GGUF, runs encoder + head, applies hysteresis thresholding (onset=0.5, offset=0.5) to produce SpeakerSegments with speaker labels and time ranges. SAS merge (sas_merge.hpp/.cpp): merge_asr_diarization assigns each ASR word to the speaker with the largest temporal overlap. Dominant speaker wins ties (first by start time, then by speaker index). group_speaker_words groups consecutive same-speaker words into SpeakerUtterances, splitting on speaker change or >0.5s gap. Words with no overlapping segment get speaker=-1. Parity: 584 segments (ours) vs 581 (reference) — 0.5% difference. --- src/diarization.cpp | 162 ++++++++++++++++++++++++++++++++++++++++++++ src/diarization.hpp | 72 ++++++++++++++++++++ src/sas_merge.cpp | 91 +++++++++++++++++++++++++ src/sas_merge.hpp | 52 ++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 src/diarization.cpp create mode 100644 src/diarization.hpp create mode 100644 src/sas_merge.cpp create mode 100644 src/sas_merge.hpp diff --git a/src/diarization.cpp b/src/diarization.cpp new file mode 100644 index 0000000..544d845 --- /dev/null +++ b/src/diarization.cpp @@ -0,0 +1,162 @@ +#include "diarization.hpp" + +#include "audio_io.hpp" +#include "backend.hpp" +#include "ggml_graph.hpp" + +#include +#include +#include +#include + +namespace pk { + +std::unique_ptr DiarizationModel::load(const std::string& path) { + // unique_ptr via private ctor: construct then load. + std::unique_ptr m(new (std::nothrow) DiarizationModel()); + if (!m) return nullptr; + if (!m->loader_.load(path)) return nullptr; + + const auto& cfg = m->loader_.config(); + if (cfg.arch != "diarization") { + // Not a diarization model — caller should use Model for ASR. + return nullptr; + } + if (!cfg.diarization.present) { + return nullptr; + } + + // Give the weights a backend buffer ONCE so graphs reference them + // directly as leaves (zero per-call copy), same as Model::load. + ensure_weights_realized(m->loader_); + + // Construct the component objects (lightweight views over the ModelLoader). + m->mel_ = std::make_unique(m->loader_); + m->encoder_ = std::make_unique(m->loader_); + m->head_ = std::make_unique(m->loader_); + + return m; +} + +DiarizationResult DiarizationModel::diarize_path(const std::string& wav_path) { + Audio audio; + if (!load_audio_16k_mono(wav_path, audio)) { + throw std::runtime_error("parakeet: failed to load audio: " + wav_path); + } + // load_audio_16k_mono already resamples to 16 kHz mono. + return run(audio.samples); +} + +DiarizationResult DiarizationModel::diarize_pcm( + const std::vector& samples, int sample_rate) { + if (sample_rate <= 0) { + throw std::runtime_error("parakeet: invalid sample_rate"); + } + if (sample_rate == 16000) { + return run(samples); + } + std::vector pcm16k = resample_linear(samples, sample_rate, 16000); + return run(pcm16k); +} + +DiarizationResult DiarizationModel::run(const std::vector& samples) { + const ParakeetConfig& cfg = loader_.config(); + + // 1. Log-mel front end → feats [n_mels, T] + std::vector feats; + int n_mels = 0, T = 0; + mel_->compute(samples, feats, n_mels, T); + + // 2. Diarization encoder → enc_out [d_model, T_enc] (channels-first) + std::vector enc_out; + int d_model = 0, T_enc = 0; + encoder_->forward(feats, n_mels, T, enc_out, d_model, T_enc); + + // 3. Diarization head → probs [n_spk, T_out] (post-sigmoid) + std::vector probs; + int n_spk = 0, T_out = 0; + head_->forward(enc_out, d_model, T_enc, probs, n_spk, T_out); + + // 4. Post-process → speaker segments + std::vector segs = postprocess( + probs, n_spk, T_out, + cfg.diarization.frame_resolution_sec, + cfg.diarization.onset_threshold, + cfg.diarization.offset_threshold); + + DiarizationResult result; + result.segments = std::move(segs); + result.n_speakers = n_spk; + return result; +} + +std::vector DiarizationModel::postprocess( + const std::vector& probs, int n_spk, int T_out, + float frame_sec, float onset, float offset) const { + + // NeMo predlist_to_timestamps (from nemo.collections.asr.parts.utils.vad_utils): + // + // 1. Hysteresis binarization per speaker: + // - OFF → ON when prob >= onset + // - ON → OFF when prob < offset + // When onset == offset (0.5 for Nemotron-3), this is a simple threshold. + // + // 2. Extract contiguous ON segments per speaker. + // + // 3. min_duration_on / min_duration_off filtering (defaults 0.0 → no-op). + // + // 4. merge_overlap_segment: merges same-speaker segments that overlap + // (from padded/chunked inference). Offline single-pass produces no + // overlaps, so this is a no-op here. + // + // 5. Round timestamps to 2 decimal places. + + // probs is row-major [n_spk, T_out]: probs[s * T_out + t] + std::vector segments; + + for (int s = 0; s < n_spk; ++s) { + const float* p = probs.data() + (size_t)s * T_out; + + bool active = false; + int start_frame = 0; + + for (int t = 0; t < T_out; ++t) { + const bool on = (p[t] >= onset); + if (on && !active) { + // Hysteresis: OFF → ON at onset threshold + start_frame = t; + active = true; + } else if (!on && active) { + // Hysteresis: ON → OFF when prob drops below offset. + // With onset == offset, p[t] < offset ⟺ p[t] < onset ⟺ !on. + float start_sec = start_frame * frame_sec; + float end_sec = t * frame_sec; + segments.push_back({s, start_sec, end_sec}); + active = false; + } + } + // Close any segment still open at the end of the audio. + if (active) { + float start_sec = start_frame * frame_sec; + float end_sec = T_out * frame_sec; + segments.push_back({s, start_sec, end_sec}); + } + } + + // Sort by start time, then by speaker (NeMo returns segments in start order). + std::sort(segments.begin(), segments.end(), + [](const SpeakerSegment& a, const SpeakerSegment& b) { + if (a.start != b.start) return a.start < b.start; + return a.speaker < b.speaker; + }); + + // Round to 2 decimal places (NeMo uses round(ts, 2)). + for (auto& seg : segments) { + seg.start = std::round(seg.start * 100.0f) / 100.0f; + seg.end = std::round(seg.end * 100.0f) / 100.0f; + } + + return segments; +} + +} // namespace pk diff --git a/src/diarization.hpp b/src/diarization.hpp new file mode 100644 index 0000000..44bc6a3 --- /dev/null +++ b/src/diarization.hpp @@ -0,0 +1,72 @@ +#pragma once +#include "model_loader.hpp" +#include "diarization_head.hpp" +#include "diarization_encoder.hpp" +#include "mel.hpp" +#include +#include +#include + +namespace pk { + +// A speaker segment: which speaker was active, and when. +// Timestamps are in seconds, matching ASR Word timestamps for Phase 3 (SAS). +struct SpeakerSegment { + int speaker; // 0-indexed speaker ID (0..n_speakers-1) + float start; // segment start in seconds + float end; // segment end in seconds +}; + +// Diarization result: list of speaker segments + metadata. +struct DiarizationResult { + std::vector segments; + int n_speakers; // max speakers the model supports +}; + +// DiarizationModel — offline speaker diarization ("who spoke when"). +// +// Composes a DiarizationEncoder (pre-LN RoPE Transformer for Nemotron-3- +// Diarization) with a DiarizationHead (sortformer speaker sigmoid head). +// The mel frontend is shared with the ASR path. +// +// The model is loaded from a GGUF with arch="diarization". The C++ loader +// reuses ModelLoader (shared with ASR) and constructs a DiarizationEncoder + +// MelFrontend + DiarizationHead — no CTC/RNNT decoder. +class DiarizationModel { +public: + // Load a diarization GGUF. Returns nullptr on failure. + static std::unique_ptr load(const std::string& path); + + // Diarize an audio file (any format audio_io supports). Returns segments. + DiarizationResult diarize_path(const std::string& wav_path); + + // Diarize raw PCM samples (mono float, any sample rate — resampled to 16k). + DiarizationResult diarize_pcm(const std::vector& samples, + int sample_rate); + + const ParakeetConfig& config() const { return loader_.config(); } + const ModelLoader& loader() const { return loader_; } + +private: + DiarizationModel() = default; + + // Internal: run the full pipeline (mel → encoder → head → postprocess) + // on already-16kHz PCM. + DiarizationResult run(const std::vector& samples); + + // Post-process per-frame speaker probabilities into speaker segments. + // Matches NeMo's predlist_to_timestamps: hysteresis binarization + // (onset/offset thresholds), segment extraction, min_duration filtering, + // merge overlapping segments, round to 2 decimal places. + std::vector postprocess(const std::vector& probs, + int n_spk, int T_out, + float frame_sec, + float onset, float offset) const; + + ModelLoader loader_; + std::unique_ptr mel_; + std::unique_ptr encoder_; + std::unique_ptr head_; +}; + +} // namespace pk diff --git a/src/sas_merge.cpp b/src/sas_merge.cpp new file mode 100644 index 0000000..bc05a0a --- /dev/null +++ b/src/sas_merge.cpp @@ -0,0 +1,91 @@ +#include "sas_merge.hpp" + +#include +#include + +namespace pk { + +std::vector merge_asr_diarization( + const std::vector& words, + const std::vector& segs) +{ + std::vector result; + result.reserve(words.size()); + + // Sort segments by start time so we can advance a cursor. + // (They typically arrive already sorted, but don't assume it.) + std::vector sorted_segs = segs; + std::sort(sorted_segs.begin(), sorted_segs.end(), + [](const SpeakerSegment& a, const SpeakerSegment& b) { + if (a.start != b.start) return a.start < b.start; + return a.speaker < b.speaker; + }); + + for (const auto& w : words) { + // Find the dominant speaker: the one with the largest overlap + // between [w.start, w.end] and [seg.start, seg.end]. + int best_speaker = -1; + float best_overlap = 0.0f; + + for (const auto& seg : sorted_segs) { + if (seg.end <= w.start) continue; // segment ends before word + if (seg.start >= w.end) break; // segment starts after word + + float overlap = std::min(w.end, seg.end) - std::max(w.start, seg.start); + if (overlap > best_overlap) { + best_overlap = overlap; + best_speaker = seg.speaker; + } + } + + SpeakerWord sw; + sw.speaker = best_speaker; + sw.text = w.text; + sw.start = w.start; + sw.end = w.end; + sw.conf = w.conf; + result.push_back(sw); + } + + return result; +} + +std::vector group_speaker_words( + const std::vector& swords, + float max_gap_sec) +{ + std::vector result; + if (swords.empty()) return result; + + SpeakerUtterance cur; + cur.speaker = swords[0].speaker; + cur.text = swords[0].text; + cur.start = swords[0].start; + cur.end = swords[0].end; + cur.conf = swords[0].conf; + + for (size_t i = 1; i < swords.size(); ++i) { + const auto& w = swords[i]; + float gap = w.start - cur.end; + + if (w.speaker == cur.speaker && gap <= max_gap_sec) { + // Extend current utterance + cur.text += " " + w.text; + cur.end = w.end; + cur.conf = std::min(cur.conf, w.conf); + } else { + // Flush and start new utterance + result.push_back(cur); + cur.speaker = w.speaker; + cur.text = w.text; + cur.start = w.start; + cur.end = w.end; + cur.conf = w.conf; + } + } + result.push_back(cur); + + return result; +} + +} // namespace pk diff --git a/src/sas_merge.hpp b/src/sas_merge.hpp new file mode 100644 index 0000000..22eb165 --- /dev/null +++ b/src/sas_merge.hpp @@ -0,0 +1,52 @@ +#pragma once +#include "transcription.hpp" +#include "diarization.hpp" + +#include +#include + +namespace pk { + +// A word attributed to a speaker — the output of merging ASR +// transcription with diarization segments. +struct SpeakerWord { + int speaker; // from diarization (0-based, -1 = no speaker) + std::string text; // from ASR + float start; // from ASR word (seconds) + float end; // from ASR word (seconds) + float conf; // from ASR word +}; + +// A speaker-attributed utterance: consecutive words from the same speaker +// that form a phrase. Groups SpeakerWords where the speaker doesn't change +// and the gap between words is small. +struct SpeakerUtterance { + int speaker; + std::string text; // space-joined words + float start; // first word start + float end; // last word end + float conf; // min word confidence +}; + +// Merge ASR word timestamps with diarization speaker segments. +// +// For each word, the dominant active speaker is the one whose diarization +// segment overlaps the word's [start, end] interval by the largest amount. +// Words with no overlapping segment get speaker = -1. +// +// `word_frame_sec` and `diar_frame_sec` are the ASR and diarization encoder +// frame strides (seconds per encoder frame). They are not used for the merge +// itself (timestamps are already in seconds) but are exposed in the signature +// for future streaming use where frame-level alignment is needed. +std::vector merge_asr_diarization( + const std::vector& words, + const std::vector& segs); + +// Group speaker-attributed words into utterances. +// Consecutive words with the same speaker and gap <= max_gap_sec are joined. +// speaker == -1 words are grouped together as "unknown" utterances. +std::vector group_speaker_words( + const std::vector& swords, + float max_gap_sec = 0.5f); + +} // namespace pk From c38f6ae309dfc1ca6295d7ba605153d6257783b7 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 26 Sep 2026 11:09:05 +0000 Subject: [PATCH 05/17] feat(capi): add diarization and speaker-attributed ASR C-API (ABI v7) Bump ABI to 7. Add diarization C-API (Phase 1): - parakeet_capi_diarize_path / _pcm: run diarization on audio - parakeet_capi_diarize_json: JSON output with segments array - parakeet_capi_free_diar_results: free result array Add speaker-attributed ASR (SAS) C-API (Phase 3): - parakeet_sas_result struct (speaker, text, start, end, conf) - parakeet_capi_transcribe_and_diarize: run ASR + diarization, merge into speaker-attributed utterances (struct variant) - parakeet_capi_transcribe_and_diarize_json: same, JSON output with speakers/utterances/words arrays - parakeet_capi_free_sas_results: free result array SAS functions take separate ASR and diarization contexts so the caller can mix and match model types and sizes. --- include/parakeet_capi.h | 86 +++++++++++ src/parakeet_capi.cpp | 321 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 396 insertions(+), 11 deletions(-) diff --git a/include/parakeet_capi.h b/include/parakeet_capi.h index d4a5565..5c1ac20 100644 --- a/include/parakeet_capi.h +++ b/include/parakeet_capi.h @@ -47,6 +47,18 @@ typedef struct parakeet_ctx parakeet_ctx; // KenLM) that need the raw distribution rather than this library's own // greedy/beam decode. Freed with the new parakeet_capi_free_logits. The // original entry points are unchanged. +// v6.1: added offline speaker diarization entry points +// (parakeet_capi_diarize_path / parakeet_capi_diarize_pcm) for +// nvidia/Nemotron-3-Diarization and compatible Sortformer models. A +// parakeet_ctx now holds EITHER an ASR model (pk::Model) OR a diarization +// model (pk::DiarizationModel); the diarize functions dispatch on which +// is loaded. parakeet_capi_load auto-detects the arch. No existing ASR +// signatures changed. +// v7: added speaker-attributed ASR (SAS) entry points +// (parakeet_capi_transcribe_and_diarize / +// parakeet_capi_transcribe_and_diarize_json). Takes two contexts +// (an ASR ctx + a diarization ctx), runs both models on the same +// audio, and merges word timestamps with speaker segments. int parakeet_capi_abi_version(void); // Load a GGUF model. Returns an owning context, or NULL on failure. @@ -322,10 +334,84 @@ char* parakeet_capi_stream_finalize_json(parakeet_stream* s); // Free a streaming session. Safe on NULL. void parakeet_capi_stream_free(parakeet_stream* s); +// --------------------------------------------------------------------------- +// Offline speaker diarization (nvidia/Nemotron-3-Diarization and compatible +// Sortformer models). A parakeet_ctx loaded from a diarization GGUF holds a +// pk::DiarizationModel instead of an ASR pk::Model. The diarize functions +// below are the only valid entry points for such a context (the transcribe +// functions return NULL); conversely, diarize functions on an ASR context +// return NULL. parakeet_capi_load auto-detects the arch. +// --------------------------------------------------------------------------- + +// Diarize a WAV file. Returns a malloc'd UTF-8 JSON document (free with +// parakeet_capi_free_string) of the shape: +// {"speakers":8, +// "segments":[{"speaker":0,"start":0.10,"end":1.30}, ...]} +// where "speakers" is the model's max-speaker capacity, "speaker" is a 0-based +// speaker index, and "start"/"end" are seconds (2 decimals, matching NeMo's +// round(ts, 2)). Segments are sorted by start time then speaker. On error +// returns NULL and sets the context's last error. +char* parakeet_capi_diarize_path(parakeet_ctx* ctx, const char* wav_path); + +// Diarize in-memory mono float PCM (`samples`, length `n_samples`). If +// `sample_rate != 16000` the audio is linearly resampled to 16 kHz first. +// Returns the same JSON shape as parakeet_capi_diarize_path. Free with +// parakeet_capi_free_string; NULL on error. +char* parakeet_capi_diarize_pcm(parakeet_ctx* ctx, const float* samples, + int n_samples, int sample_rate); + // Free a string previously returned by parakeet_capi_transcribe_* / +// parakeet_capi_diarize_* / parakeet_capi_transcribe_and_diarize_* / // parakeet_capi_stream_*. Safe on NULL. void parakeet_capi_free_string(char* s); +// --------------------------------------------------------------------------- +// Speaker-attributed ASR (SAS): run both ASR and diarization on the same +// audio, then merge word timestamps with speaker segments ("who said what"). +// +// Takes two separately-loaded contexts: `asr_ctx` (an ASR model) and +// `diar_ctx` (a diarization model). Both must have been loaded successfully +// via parakeet_capi_load. The audio is fed to each model independently, so +// mel is computed twice (once per model). Phase 3.3 (mel sharing) will +// optimize this when the two models' mel configs match. +// --------------------------------------------------------------------------- + +// Speaker-attributed result: one utterance = one speaker + text + time span. +typedef struct parakeet_sas_result { + int speaker; // 0-based speaker index, -1 = no speaker found + char* text; // utterance text (space-joined words) + float start; // utterance start (seconds) + float end; // utterance end (seconds) + float conf; // min word confidence +} parakeet_sas_result; + +// Run ASR + diarization and merge. Returns a malloc'd array of +// parakeet_sas_result (free with parakeet_capi_free_sas_results). +// *n_results receives the count. Returns NULL on error. +parakeet_sas_result* parakeet_capi_transcribe_and_diarize( + parakeet_ctx* asr_ctx, + parakeet_ctx* diar_ctx, + const float* samples, int n_samples, int sample_rate, + int* n_results); + +// Free a result array from parakeet_capi_transcribe_and_diarize. Safe on NULL. +void parakeet_capi_free_sas_results(parakeet_sas_result* results); + +// JSON variant with full per-word + per-utterance detail. Returns a malloc'd +// UTF-8 JSON document (free with parakeet_capi_free_string) of the shape: +// {"speakers":8, +// "utterances":[ +// {"speaker":0,"text":"hello world","start":0.12,"end":0.85,"conf":0.95}, +// ...], +// "words":[ +// {"speaker":0,"text":"hello","start":0.12,"end":0.45,"conf":0.97}, +// ...]} +// Returns NULL on error. +char* parakeet_capi_transcribe_and_diarize_json( + parakeet_ctx* asr_ctx, + parakeet_ctx* diar_ctx, + const float* samples, int n_samples, int sample_rate); + // Human-readable description of the last error on `ctx`, or "" if none. // The returned pointer is owned by the context and valid until the next call on // it (or until parakeet_capi_free). Returns "" if `ctx` is NULL. diff --git a/src/parakeet_capi.cpp b/src/parakeet_capi.cpp index 1a6c9b5..ca1b1fd 100644 --- a/src/parakeet_capi.cpp +++ b/src/parakeet_capi.cpp @@ -1,8 +1,10 @@ #include "parakeet_capi.h" #include "parakeet.h" // pk::Decoder #include "model.hpp" // pk::Model +#include "diarization.hpp" // pk::DiarizationModel #include "streaming.hpp" // pk::StreamingSession #include "mel.hpp" // pk::MelFrontend +#include "sas_merge.hpp" // pk::merge_asr_diarization, pk::group_speaker_words #include "transcription.hpp" // pk::Transcription, pk::Word #include "transcription_json.hpp" @@ -35,11 +37,18 @@ // v6: transcribe_pcm_logits, exposing the CTC head's log-prob matrix (row-major // [T, vocab+1], already log-softmaxed) instead of decoded text, freed with // the new free_logits. Original entry points unchanged. -#define PARAKEET_CAPI_ABI_VERSION 6 +// v6.1: offline speaker diarization entry points (diarize_path / diarize_pcm). +// v7: speaker-attributed ASR (SAS) entry points — takes two contexts +// (ASR + diarization), runs both models, merges word timestamps with +// speaker segments. +#define PARAKEET_CAPI_ABI_VERSION 7 // The opaque context: a loaded model plus a buffer for the last error message. +// Exactly one of `model` / `diar` is non-null: ASR models use `model`, +// diarization models (Sortformer) use `diar`. struct parakeet_ctx { std::unique_ptr model; + std::unique_ptr diar; std::string last_error; }; @@ -130,12 +139,28 @@ extern "C" int parakeet_capi_abi_version(void) { extern "C" parakeet_ctx* parakeet_capi_load(const char* gguf_path) { if (!gguf_path) return nullptr; try { - std::unique_ptr model = pk::Model::load(gguf_path); - if (!model) return nullptr; // load failure (bad/missing GGUF) auto* ctx = new (std::nothrow) parakeet_ctx(); if (!ctx) return nullptr; - ctx->model = std::move(model); - return ctx; + + // Try ASR first. Model::load returns nullptr if the GGUF is not a + // valid ASR model (bad/missing file, or arch=="diarization" which + // Model::load rejects). Then try diarization. + std::unique_ptr model = pk::Model::load(gguf_path); + if (model) { + ctx->model = std::move(model); + return ctx; + } + + // Not an ASR model — try diarization. + std::unique_ptr diar = pk::DiarizationModel::load(gguf_path); + if (diar) { + ctx->diar = std::move(diar); + return ctx; + } + + // Neither — load failed entirely. + delete ctx; + return nullptr; } catch (...) { // Never let an exception cross the boundary. return nullptr; @@ -150,7 +175,12 @@ extern "C" char* parakeet_capi_transcribe_path_lang(parakeet_ctx* ctx, const char* wav_path, int decoder, const char* target_lang) { if (!ctx) return nullptr; - if (!ctx->model) { ctx->last_error = "context has no loaded model"; return nullptr; } + if (!ctx->model) { + ctx->last_error = ctx->diar + ? "context holds a diarization model; use parakeet_capi_diarize_*" + : "context has no loaded model"; + return nullptr; + } if (!wav_path) { ctx->last_error = "wav_path is NULL"; return nullptr; } // NULL / "" -> model default language (ignored by non-prompt models). const std::string lang = target_lang ? target_lang : ""; @@ -180,7 +210,12 @@ extern "C" char* parakeet_capi_transcribe_pcm_lang(parakeet_ctx* ctx, int sample_rate, int decoder, const char* target_lang) { if (!ctx) return nullptr; - if (!ctx->model) { ctx->last_error = "context has no loaded model"; return nullptr; } + if (!ctx->model) { + ctx->last_error = ctx->diar + ? "context holds a diarization model; use parakeet_capi_diarize_*" + : "context has no loaded model"; + return nullptr; + } if (!samples || n_samples < 0) { ctx->last_error = "invalid samples buffer"; return nullptr; } // NULL / "" -> model default language (ignored by non-prompt models). const std::string lang = target_lang ? target_lang : ""; @@ -257,7 +292,12 @@ extern "C" int parakeet_capi_transcribe_pcm_batch_lang(parakeet_ctx* ctx, const char* target_lang, char** out) { if (!ctx) return 1; - if (!ctx->model) { ctx->last_error = "context has no loaded model"; return 1; } + if (!ctx->model) { + ctx->last_error = ctx->diar + ? "context holds a diarization model; use parakeet_capi_diarize_*" + : "context has no loaded model"; + return 1; + } if (!samples || !n_samples || !out || n_clips < 0) { ctx->last_error = "invalid batch arguments"; return 1; @@ -314,7 +354,12 @@ extern "C" char* parakeet_capi_transcribe_path_json(parakeet_ctx* ctx, const char* wav_path, int decoder) { if (!ctx) return nullptr; - if (!ctx->model) { ctx->last_error = "context has no loaded model"; return nullptr; } + if (!ctx->model) { + ctx->last_error = ctx->diar + ? "context holds a diarization model; use parakeet_capi_diarize_*" + : "context has no loaded model"; + return nullptr; + } if (!wav_path) { ctx->last_error = "wav_path is NULL"; return nullptr; } try { pk::Transcription tr = @@ -341,7 +386,12 @@ extern "C" char* parakeet_capi_transcribe_pcm_batch_json_lang(parakeet_ctx* ctx, const float* samples_concat, const int* n_samples, int n_clips, int sample_rate, int decoder, const char* target_lang) { if (!ctx) return nullptr; - if (!ctx->model) { ctx->last_error = "context has no loaded model"; return nullptr; } + if (!ctx->model) { + ctx->last_error = ctx->diar + ? "context holds a diarization model; use parakeet_capi_diarize_*" + : "context has no loaded model"; + return nullptr; + } if (!samples_concat || !n_samples || n_clips < 0) { ctx->last_error = "invalid batch arguments"; return nullptr; } @@ -546,7 +596,12 @@ std::string feed_available(parakeet_stream* s, bool flush, int& eou_flag, extern "C" parakeet_stream* parakeet_capi_stream_begin_lang(parakeet_ctx* ctx, const char* target_lang) { if (!ctx) return nullptr; - if (!ctx->model) { ctx->last_error = "context has no loaded model"; return nullptr; } + if (!ctx->model) { + ctx->last_error = ctx->diar + ? "context holds a diarization model; use parakeet_capi_diarize_*" + : "context has no loaded model"; + return nullptr; + } if (!ctx->model->config().streaming.present) { ctx->last_error = "model is not a cache-aware streaming model"; return nullptr; @@ -802,7 +857,251 @@ extern "C" void parakeet_capi_free_string(char* s) { std::free(s); } +// --------------------------------------------------------------------------- +// Offline speaker diarization +// --------------------------------------------------------------------------- + +// Serialize a DiarizationResult to the JSON shape documented in the header. +static char* diar_result_to_json(const pk::DiarizationResult& r) { + // {"speakers":N,"segments":[{"speaker":S,"start":X.XX,"end":Y.YY}, ...]} + std::string json; + json.reserve(128 + r.segments.size() * 40); + json += "{\"speakers\":"; + json += std::to_string(r.n_speakers); + json += ",\"segments\":["; + for (size_t i = 0; i < r.segments.size(); ++i) { + if (i) json += ','; + char buf[80]; + std::snprintf(buf, sizeof(buf), + "{\"speaker\":%d,\"start\":%.2f,\"end\":%.2f}", + r.segments[i].speaker, r.segments[i].start, r.segments[i].end); + json += buf; + } + json += "]}"; + return dup_to_c(json); +} + +extern "C" char* parakeet_capi_diarize_path(parakeet_ctx* ctx, + const char* wav_path) { + if (!ctx) return nullptr; + if (!ctx->diar) { + ctx->last_error = "context has no loaded diarization model"; + return nullptr; + } + if (!wav_path) { ctx->last_error = "wav_path is NULL"; return nullptr; } + try { + pk::DiarizationResult r = ctx->diar->diarize_path(wav_path); + ctx->last_error.clear(); + return diar_result_to_json(r); + } catch (const std::exception& e) { + ctx->last_error = e.what(); + return nullptr; + } catch (...) { + ctx->last_error = "unknown error"; + return nullptr; + } +} + +extern "C" char* parakeet_capi_diarize_pcm(parakeet_ctx* ctx, + const float* samples, int n_samples, + int sample_rate) { + if (!ctx) return nullptr; + if (!ctx->diar) { + ctx->last_error = "context has no loaded diarization model"; + return nullptr; + } + if (!samples || n_samples < 0) { + ctx->last_error = "invalid samples buffer"; + return nullptr; + } + try { + std::vector pcm(samples, samples + n_samples); + pk::DiarizationResult r = ctx->diar->diarize_pcm(pcm, sample_rate); + ctx->last_error.clear(); + return diar_result_to_json(r); + } catch (const std::exception& e) { + ctx->last_error = e.what(); + return nullptr; + } catch (...) { + ctx->last_error = "unknown error"; + return nullptr; + } +} + extern "C" const char* parakeet_capi_last_error(parakeet_ctx* ctx) { if (!ctx) return ""; return ctx->last_error.c_str(); } + +// --------------------------------------------------------------------------- +// Speaker-attributed ASR (SAS) +// --------------------------------------------------------------------------- + +static char* sas_results_to_json(const std::vector& utts, + int n_speakers) { + // Build JSON: {"speakers":N, "utterances":[...], "words":[...]} + // For the non-words variant, just utterances. + std::string s; + s.reserve(4096); + s += "{\"speakers\":"; + s += std::to_string(n_speakers); + s += ",\"utterances\":["; + for (size_t i = 0; i < utts.size(); ++i) { + if (i) s += ','; + char buf[512]; + snprintf(buf, sizeof(buf), + "{\"speaker\":%d,\"text\":\"%s\",\"start\":%.2f,\"end\":%.2f,\"conf\":%.3f}", + utts[i].speaker, + utts[i].text.c_str(), + utts[i].start, + utts[i].end, + utts[i].conf); + s += buf; + } + s += "]}"; + return dup_to_c(s); +} + +static char* sas_results_to_json_full(const std::vector& utts, + const std::vector& swords, + int n_speakers) { + std::string s; + s.reserve(8192); + s += "{\"speakers\":"; + s += std::to_string(n_speakers); + s += ",\"utterances\":["; + for (size_t i = 0; i < utts.size(); ++i) { + if (i) s += ','; + char buf[512]; + snprintf(buf, sizeof(buf), + "{\"speaker\":%d,\"text\":\"%s\",\"start\":%.2f,\"end\":%.2f,\"conf\":%.3f}", + utts[i].speaker, + utts[i].text.c_str(), + utts[i].start, + utts[i].end, + utts[i].conf); + s += buf; + } + s += "],\"words\":["; + for (size_t i = 0; i < swords.size(); ++i) { + if (i) s += ','; + char buf[512]; + snprintf(buf, sizeof(buf), + "{\"speaker\":%d,\"text\":\"%s\",\"start\":%.3f,\"end\":%.3f,\"conf\":%.3f}", + swords[i].speaker, + swords[i].text.c_str(), + swords[i].start, + swords[i].end, + swords[i].conf); + s += buf; + } + s += "]}"; + return dup_to_c(s); +} + +// Internal: run ASR + diarization on the same audio, merge, return both +// utterances and per-word results. +static bool run_sas(parakeet_ctx* asr_ctx, + parakeet_ctx* diar_ctx, + const float* samples, int n_samples, int sample_rate, + std::vector& swords, + std::vector& utts, + int& n_speakers) { + if (!asr_ctx || !asr_ctx->model) { + if (asr_ctx) asr_ctx->last_error = "asr_ctx does not hold an ASR model"; + return false; + } + if (!diar_ctx || !diar_ctx->diar) { + if (diar_ctx) diar_ctx->last_error = "diar_ctx does not hold a diarization model"; + return false; + } + + // Run ASR (with timestamps so we get per-word [text, start, end, conf]) + pk::Transcription tr; + try { + std::vector pcm(samples, samples + n_samples); + tr = asr_ctx->model->transcribe_with_timestamps(pcm, sample_rate); + } catch (const std::exception& e) { + asr_ctx->last_error = std::string("ASR failed: ") + e.what(); + return false; + } + + // Run diarization + pk::DiarizationResult dr; + try { + std::vector pcm(samples, samples + n_samples); + dr = diar_ctx->diar->diarize_pcm(pcm, sample_rate); + } catch (const std::exception& e) { + diar_ctx->last_error = std::string("diarization failed: ") + e.what(); + return false; + } + + n_speakers = dr.n_speakers; + + // Merge: assign speaker to each word + swords = pk::merge_asr_diarization(tr.words, dr.segments); + + // Group into utterances + utts = pk::group_speaker_words(swords); + + return true; +} + +extern "C" parakeet_sas_result* parakeet_capi_transcribe_and_diarize( + parakeet_ctx* asr_ctx, + parakeet_ctx* diar_ctx, + const float* samples, int n_samples, int sample_rate, + int* n_results) { + if (n_results) *n_results = 0; + if (!asr_ctx || !diar_ctx) return nullptr; + + std::vector swords; + std::vector utts; + int n_speakers = 0; + + if (!run_sas(asr_ctx, diar_ctx, samples, n_samples, sample_rate, + swords, utts, n_speakers)) { + return nullptr; + } + + // Allocate result array + parakeet_sas_result* results = (parakeet_sas_result*) + std::malloc(sizeof(parakeet_sas_result) * utts.size()); + if (!results) return nullptr; + + for (size_t i = 0; i < utts.size(); ++i) { + results[i].speaker = utts[i].speaker; + results[i].text = dup_to_c(utts[i].text); + results[i].start = utts[i].start; + results[i].end = utts[i].end; + results[i].conf = utts[i].conf; + } + + if (n_results) *n_results = (int)utts.size(); + return results; +} + +extern "C" void parakeet_capi_free_sas_results(parakeet_sas_result* results) { + // We cannot free the .text strings because the caller doesn't pass the + // count to this function. The caller must free each .text with + // parakeet_capi_free_string and then call this function to free the array. + if (results) std::free(results); +} + +extern "C" char* parakeet_capi_transcribe_and_diarize_json( + parakeet_ctx* asr_ctx, + parakeet_ctx* diar_ctx, + const float* samples, int n_samples, int sample_rate) { + if (!asr_ctx || !diar_ctx) return nullptr; + + std::vector swords; + std::vector utts; + int n_speakers = 0; + + if (!run_sas(asr_ctx, diar_ctx, samples, n_samples, sample_rate, + swords, utts, n_speakers)) { + return nullptr; + } + + return sas_results_to_json_full(utts, swords, n_speakers); +} From e497e08e9281b60de90d9601eeb2cc8c89698250 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 26 Sep 2026 11:10:29 +0000 Subject: [PATCH 06/17] feat(diarization): add CLI, tests, build integration, and plan doc Build: - CMakeLists.txt: add diarization sources to parakeet library - examples/cli/CMakeLists.txt: add diarize CLI target - tests/CMakeLists.txt: register all diarization/SAS tests CLI: - examples/cli/diarize.cpp: minimal CLI (path -> segments JSON) Tests: - test_diarization.cpp: basic diarization smoke test - test_diarization_parity.cpp: full parity test vs PyTorch reference - test_diar_head_bisect.cpp: head stage-by-stage bisection - test_diar_layer0.cpp / _bisect.cpp: encoder layer 0 bisection - test_cont_check.cpp: verifies 2D cont works (PASS) - test_subpixel_check.cpp: verifies subpixel reshape (PASS) - test_3d_permute.cpp: documents 3D cont bug (expected FAIL) - test_sas_merge.cpp: 10 unit tests for SAS merge logic (PASS) - test_combined_offline.cpp: end-to-end SAS test (ASR + diarization) docs/diarization-plan.md: 3-phase plan with dependency graph. --- CMakeLists.txt | 6 +- docs/diarization-plan.md | 460 ++++++++++++++++++++++++++++++ examples/cli/CMakeLists.txt | 4 + examples/cli/diarize.cpp | 28 ++ tests/CMakeLists.txt | 12 + tests/test_3d_permute.cpp | 157 ++++++++++ tests/test_combined_offline.cpp | 327 +++++++++++++++++++++ tests/test_cont_check.cpp | 64 +++++ tests/test_diar_head_bisect.cpp | 189 ++++++++++++ tests/test_diar_layer0.cpp | 256 +++++++++++++++++ tests/test_diar_layer0_bisect.cpp | 434 ++++++++++++++++++++++++++++ tests/test_diarization.cpp | 86 ++++++ tests/test_diarization_parity.cpp | 133 +++++++++ tests/test_sas_merge.cpp | 227 +++++++++++++++ tests/test_subpixel_check.cpp | 88 ++++++ 15 files changed, 2470 insertions(+), 1 deletion(-) create mode 100644 docs/diarization-plan.md create mode 100644 examples/cli/diarize.cpp create mode 100644 tests/test_3d_permute.cpp create mode 100644 tests/test_combined_offline.cpp create mode 100644 tests/test_cont_check.cpp create mode 100644 tests/test_diar_head_bisect.cpp create mode 100644 tests/test_diar_layer0.cpp create mode 100644 tests/test_diar_layer0_bisect.cpp create mode 100644 tests/test_diarization.cpp create mode 100644 tests/test_diarization_parity.cpp create mode 100644 tests/test_sas_merge.cpp create mode 100644 tests/test_subpixel_check.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 778309d..6438d9a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,7 +92,11 @@ set(PARAKEET_SRC src/transducer_batch.cpp src/tokenizer.cpp src/search.cpp - src/transcription.cpp) + src/transcription.cpp + src/diarization.cpp + src/diarization_encoder.cpp + src/diarization_head.cpp + src/sas_merge.cpp) if(PARAKEET_SHARED) add_library(parakeet SHARED ${PARAKEET_SRC}) diff --git a/docs/diarization-plan.md b/docs/diarization-plan.md new file mode 100644 index 0000000..6104743 --- /dev/null +++ b/docs/diarization-plan.md @@ -0,0 +1,460 @@ +# Plan: Nemotron-3-Diarization support in parakeet.cpp + +## What the model is + +Nemotron-3-Diarization is a Sortformer speaker diarization model +(`SortformerEncLabelModel` in NeMo). It determines "who spoke when" in +audio with up to 8 speakers. Two stages: + +1. **NEST FastConformer encoder** — 16kHz audio → log-mel → `[d_model, T]`. + Architecturally the same FastConformer parakeet.cpp already runs. +2. **Transformer encoder sorting head** — takes encoder output → per-frame, + per-speaker sigmoid logits. No text, no tokenizer, no CTC/RNNT/TDT decoder. + +Streaming uses **AOSC (Arrival-Order Speaker Cache) + FIFO queue** — a +different streaming mechanism from NeMo's cache-aware streaming that +parakeet.cpp currently implements. Input buffer latency from 80ms +(ultra-low-latency) to 30.4s (offline-style); output frame resolution +configurable in multiples of 10ms. + +## What's reusable as-is (~60-70% of the engine) + +The FastConformer encoder, mel frontend, subsampling, positional encoding, +relpos attention, conformer layers, ggml graph infrastructure, audio I/O, +and FFT are all cleanly separable from the ASR-specific heads. The encoder +emits a `[d_model, Tout]` channels-first tensor — a diarization head plugs +in at the same boundary as `CTCDecoder` (`src/ctc_decoder.cpp`). + +| Component | File(s) | Reuse | +|---|---|---| +| Mel frontend (offline) | `src/mel.cpp` (`MelFrontend`, `MelKernel`) | As-is | +| Mel frontend (GPU) | `src/mel_gpu.cpp` (`GpuMel`) | As-is | +| Mel frontend (streaming) | `src/mel.cpp` (`StreamingMel`) | As-is (if `normalize=NA`) | +| Subsampling | `src/subsampling.cpp` | As-is | +| Positional encoding | `src/pos_enc.cpp` | As-is | +| RelPos attention | `src/relpos_attention.cpp` | As-is | +| Conformer layer | `src/conformer.cpp` | As-is | +| FastConformer encoder | `src/encoder.cpp` | As-is | +| GGML graph infra | `src/ggml_graph.cpp`, `src/graph_builder.hpp` | As-is | +| Audio I/O | `src/audio_io.cpp` | As-is | +| FFT | `src/fft.cpp` | As-is | + +## Where ASR-specific assumptions are baked in + +| Location | Assumption | Fix | +|---|---|---| +| `src/model_loader.cpp:203` | `return cfg_.d_model>0 && cfg_.vocab_size>0` | Relax to `cfg_.d_model>0` or branch on arch | +| `scripts/convert_parakeet_to_gguf.py:38` | `from nemo.collections.asr.models import ASRModel` | Also accept `SortformerEncLabelModel` | +| `scripts/convert_parakeet_to_gguf.py:297-302` | Vocab/tokenizer emitted unconditionally | Make conditional on arch | +| `src/model.cpp` (entire `Model` class) | All methods return text | New `DiarizationModel` class | +| `include/parakeet_capi.h` | All entry points return transcripts | New `diarize_*` surface | +| `src/streaming_encoder.cpp:40,56-58` | Hard-asserts NeMo cache-aware streaming | New AOSC+FIFO streaming path | +| `tests/test_model_loader.cpp:21-22` | Asserts `vocab_size > 0` | Conditional on arch | + +## Architectural decisions (decide upfront, before Phase 1) + +### Decision 1: Separate `DiarizationModel` class, not bolt-on to `Model` + +The existing `Model` class (`src/model.hpp:22-119`) is entirely ASR-shaped — +every public method returns `std::string` or `Transcription`. Bolting +diarization onto it would pollute the class. A separate `DiarizationModel` +class composes the same reusable pieces (`MelFrontend`, `Encoder`, new +`DiarizationHead`) — mirroring how `StreamingSession` is already a separate +class from `Model`. + +### Decision 2: Shared segment/word timestamp types + +Both ASR and diarization produce timestamped outputs. Design the types so +they compose: + +```cpp +// Existing (src/transcription.hpp): +struct Word { std::string text; float start; float end; float conf; }; + +// New (src/diarization.hpp): +struct SpeakerSegment { + int speaker; // 0-indexed speaker label + float start; // seconds + float end; // seconds + float conf; // aggregate per-frame confidence +}; +``` + +Both use `float start/end` in seconds. The merge for combined ASR+diarization +is then: for each word's `[start, end]`, find the dominant speaker in the +overlapping segments. This is a timestamp intersection, not a deep +architecture coupling. + +### Decision 3: Mel computation stays composable + +Don't bake mel computation into `DiarizationModel`. Keep `MelFrontend` as a +standalone composable step (as it already is) so both models can share it +when running combined ASR+diarization on the same audio. + +### Decision 4: C-API designed for composition from the start + +The diarization C-API uses a separate `parakeet_diar_ctx` opaque type, not +overloading `parakeet_ctx`. This lets a caller hold both contexts and call +both APIs, and later call a combined `parakeet_capi_transcribe_and_diarize` +that takes both. + +### Decision 5: New arch string `"diarization"` + +Add `arch = "diarization"` to the arch vocabulary. The model loader dispatches +on this to know it's not an ASR model (skip vocab/decoder/joint loading, load +diarization head config instead). + +--- + +## Phase 1: Offline diarization (standalone) + +**Goal**: Load Nemotron-3-Diarization GGUF, run offline diarization on a WAV +file, produce speaker segments. No streaming. + +### 1.1 Converter changes (`scripts/convert_parakeet_to_gguf.py`) + +- Import `SortformerEncLabelModel` alongside `ASRModel`. +- Add `"diarization"` branch to `detect_arch()`. +- Make vocab/tokenizer emission conditional — skip for diarization (no + tokenizer in a diarization checkpoint; `m.tokenizer` would `AttributeError`). +- Emit new GGUF KV: + - `parakeet.diarization.num_speakers` (max 8) + - `parakeet.diarization.threshold` (sigmoid threshold, default 0.5) + - `parakeet.diarization.head_layers` (transformer encoder layer count) + - `parakeet.diarization.head_d_model` + - `parakeet.diarization.head_n_heads` + - `parakeet.diarization.head_ff_dim` +- The generic tensor loop (line 329-357) writes NeMo state_dict keys verbatim + — encoder tensors (`encoder.layers.N.*`, `encoder.pre_encode.*`) convert + with zero changes. Add Sortformer-head linear patterns to the quantization + allowlist (`_QUANTIZABLE_PATTERNS`). +- Featurizer buffer lift (`preprocessor.featurizer.fb`, `.window`) is already + generic — works as-is. + +### 1.2 Model loader changes + +- `src/model_loader.cpp:203`: relax `vocab_size>0` check to + `cfg_.d_model>0` (or branch: `arch == "diarization"` → skip vocab check). +- Add `DiarizationCfg` sub-struct to `ParakeetConfig` in + `src/model_loader.hpp`: + ```cpp + struct DiarizationCfg { + uint32_t num_speakers = 0; + float threshold = 0.5f; + uint32_t head_layers = 0; + uint32_t head_d_model = 0; + uint32_t head_n_heads = 0; + uint32_t head_ff_dim = 0; + bool present = false; + }; + ``` +- Read `parakeet.diarization.*` KV in `ModelLoader::load`. +- The `parakeet.decoder.*` / `parakeet.joint.*` / `parakeet.tdt.*` fields + already default to 0 when absent — safe for diarization GGUFs that omit + them. + +### 1.3 Diarization head (`src/diarization_head.hpp` / `.cpp`) + +New file, mirrors `src/ctc_decoder.hpp`/`.cpp` as a template: + +- `class DiarizationHead`: + - `DiarizationHead(const ModelLoader& ml)` — reads transformer encoder + config + weights. + - `void forward(const std::vector& enc, int d_model, int T, + std::vector& probs, int& num_speakers)` — takes `[d_model, T]`, + runs transformer encoder layers (standard MHSA, not relpos) + sigmoid + output layer, returns `[T, num_speakers]` per-frame speaker probabilities. +- The transformer encoder uses standard multi-head self-attention (not + relpos). parakeet.cpp currently only has `RelPosAttention` — need a plain + `MultiHeadAttention` or verify if the Sortformer head uses a different + attention variant. Check NeMo source for the exact attention type. +- Weight names: NeMo keys like + `sortformer_modules.transformer_encoder.layers.N.*`, + `sortformer_modules.encoder2unfold.*`, + `sortformer_modules.linear_layer.*`. + +### 1.4 DiarizationModel class (`src/diarization.hpp` / `.cpp`) + +New class, composes `MelFrontend` + `Encoder` + `DiarizationHead`: + +```cpp +class DiarizationModel { +public: + static std::unique_ptr load(const std::string& gguf_path); + std::vector diarize_pcm( + const std::vector& pcm, int sample_rate) const; + std::vector diarize_path(const std::string& wav_path) const; +private: + ModelLoader loader_; +}; +``` + +Orchestration: `pcm → resample to 16k → MelFrontend → Encoder → DiarizationHead +→ threshold per-frame sigmoid → merge consecutive frames with same active +speaker → segments`. + +The merge logic: threshold the per-frame per-speaker probabilities at +`cfg.diarization.threshold`, group consecutive frames where the same speaker +is active into segments, convert frame indices to seconds using `frame_sec` +(`hop_length * subsampling_factor / sample_rate`). + +### 1.5 C-API surface (`include/parakeet_capi.h`, `src/parakeet_capi.cpp`) + +New entry points (bump ABI version v5 → v6): + +```c +typedef struct parakeet_diar_ctx parakeet_diar_ctx; + +typedef struct parakeet_segment { + int speaker; + float start; + float end; + float conf; +} parakeet_segment; + +parakeet_diar_ctx* parakeet_capi_diar_load(const char* gguf_path); +void parakeet_capi_diar_free(parakeet_diar_ctx* ctx); + +parakeet_segment* parakeet_capi_diarize_path( + parakeet_diar_ctx* ctx, const char* wav_path, int* n_segments); +parakeet_segment* parakeet_capi_diarize_pcm( + parakeet_diar_ctx* ctx, const float* samples, int n_samples, + int sample_rate, int* n_segments); +void parakeet_capi_free_segments(parakeet_segment* segs); + +// JSON variant: +// {"segments":[{"speaker":0,"start":0.48,"end":2.16,"conf":0.91},...], +// "frame_sec":0.080000} +char* parakeet_capi_diarize_path_json(parakeet_diar_ctx* ctx, const char* wav_path); +char* parakeet_capi_diarize_pcm_json(parakeet_diar_ctx* ctx, + const float* samples, int n_samples, int sample_rate); +``` + +### 1.6 Tests + +- Relax `tests/test_model_loader.cpp:21-22` to not assert `vocab_size > 0` + when `arch == "diarization"`. +- `tests/test_diarization_head.cpp` — unit test: encoder output → head → + per-frame speaker probs, compare vs NeMo baseline `.npz`. +- `tests/test_diarization.cpp` — end-to-end: PCM → segments, compare vs NeMo + `diar_model.diarize()` baseline. +- Existing encoder/mel/conformer/subsampling tests carry over unchanged + (they're arch-agnostic, test components in isolation). + +### 1.7 Parity validation + +- Set up NeMo baseline: load `SortformerEncLabelModel.from_pretrained(...)`, + run `diar_model.diarize(audio=[...])`, dump segments as `.json` baseline. +- Dump intermediate tensors (mel, encoder_out, head_probs) as `.npz` for + per-component parity testing. +- Match NeMo's segment merging logic (consecutive frames, same speaker, + threshold). + +--- + +## Phase 2: Streaming diarization (AOSC + FIFO) + +**Goal**: Stream audio in chunks, get incremental speaker segments with low +latency (80ms minimum, 0.32s recommended). + +### 2.1 Sortformer streaming encoder (`src/sortformer_streaming.hpp` / `.cpp`) + +The existing `StreamingEncoder` (`src/streaming_encoder.cpp`) is NOT reusable +— it hard-asserts NeMo cache-aware streaming at the ctor (lines 40, 56-58): +`c.streaming.present`, `c.causal_downsampling`, `c.conv_causal`, +`att_context_style == "chunked_limited"`. Sortformer uses a fundamentally +different streaming mechanism (AOSC + FIFO). + +**Reusable from existing streaming code:** +- Graph-input/cache-capture pattern (`graph_input_tensor`, `capture_graph_output`) +- `run_graph` + `GraphInputPool` machinery +- Subsampling `in_valid_frames` override path + +**New (not reusable):** +- AOSC cache: retains speaker summary representations (not raw K/V columns) +- FIFO queue: manages chunk overlap/drop +- Attention cache structure (different from NeMo's conv-left-context + K/V cache) +- The conformer layer's `build_stream_layer` cache threading is the wrong + mechanism for AOSC + +### 2.2 Streaming C-API + +```c +typedef struct parakeet_diar_stream parakeet_diar_stream; + +parakeet_diar_stream* parakeet_capi_diar_stream_begin(parakeet_diar_ctx* ctx); + +// Feed PCM, get newly-finalized segments +parakeet_segment* parakeet_capi_diar_stream_feed( + parakeet_diar_stream* s, const float* pcm, int n_samples, + int* n_new_segments); + +// Flush remaining audio, get tail segments +parakeet_segment* parakeet_capi_diar_stream_finalize( + parakeet_diar_stream* s, int* n_tail_segments); + +void parakeet_capi_diar_stream_free(parakeet_diar_stream* s); +``` + +### 2.3 Streaming config in GGUF + +New GGUF KV for Sortformer streaming: +- `parakeet.sortformer.chunk_len` (frames, e.g. 340 = 27.2s at 80ms/frame) +- `parakeet.sortformer.chunk_right_context` (frames, e.g. 40) +- `parakeet.sortformer.fifo_len` (frames, e.g. 40) +- `parakeet.sortformer.spkcache_len` (AOSC cache size in frames) +- `parakeet.sortformer.spkcache_update_period` (frames, e.g. 300) + +### 2.4 Tests + +- `tests/test_sortformer_streaming.cpp` — streaming parity vs NeMo streaming + config baseline. +- Test chunk boundary correctness (no speaker label jumps at chunk edges). +- Test AOSC persistence (speaker identity maintained across chunks). + +--- + +## Phase 3: Speaker-attributed ASR (combined parakeet + diarization) + +**Goal**: Run both ASR and diarization, merge into speaker-attributed +transcription ("who said what"). + +### 3.1 Merge layer (`src/sas_merge.hpp` / `.cpp`) + +Simple timestamp intersection: +```cpp +struct SpeakerWord { + int speaker; // from diarization + std::string text; // from ASR + float start; // from ASR word + float end; // from ASR word + float conf; // from ASR word +}; + +std::vector merge_asr_diarization( + const std::vector& words, // ASR (timestamped) + const std::vector& segs, // diarization (timestamped) + float frame_sec); +``` + +For each word's `[start, end]`, find the dominant active speaker in the +overlapping diarization segments. This is a linear scan, not a deep +architecture coupling. + +### 3.2 Combined C-API + +```c +// Load both models, get speaker-attributed transcription +typedef struct parakeet_sas_result { + int speaker; + char* text; + float start; + float end; + float conf; +} parakeet_sas_result; + +parakeet_sas_result* parakeet_capi_transcribe_and_diarize( + parakeet_ctx* asr_ctx, + parakeet_diar_ctx* diar_ctx, + const float* samples, int n_samples, int sample_rate, + int* n_results); +void parakeet_capi_free_sas_results(parakeet_sas_result* results); + +// JSON variant with full per-word + per-segment detail +char* parakeet_capi_transcribe_and_diarize_json( + parakeet_ctx* asr_ctx, + parakeet_diar_ctx* diar_ctx, + const float* samples, int n_samples, int sample_rate); +``` + +### 3.3 Mel sharing optimization + +Both models compute log-mel on the same 16kHz audio. If mel configs match +(`n_mels`, `hop_length`, `n_fft`, `preemph`, `mag_power` all identical), compute +mel once and feed both encoders. If configs differ, compute mel twice (cost is +negligible vs two encoder forward passes). + +Check at load time whether the two configs are compatible for mel sharing. + +### 3.4 Streaming combined ASR + diarization + +The two models have different streaming mechanisms and latencies: +- ASR: NeMo cache-aware streaming (chunked-limited attention, conv left-context) +- Diarization: AOSC + FIFO (speaker cache, different chunk structure) + +The ASR model might emit a word at time T, but the diarization model's speaker +decision for frame T might not be finalized yet. Need a **merge buffer** that: +1. Holds ASR word hypotheses with timestamps +2. Holds diarization frame labels +3. Emits combined `(speaker, text, start, end)` only when both models have + committed to a time range + +This is a bounded-delay merge problem — doable but requires careful design. + +### 3.5 LocalAI integration + +Wire the combined ASR+diarization as a LocalAI backend endpoint: +- `/v1/audio/transcriptions` with `diarize=true` → speaker-attributed text +- Streaming variant for real-time use + +### 3.6 Tests + +- `tests/test_sas_merge.cpp` — unit test the merge logic with known + word/segment inputs. +- `tests/test_combined_offline.cpp` — end-to-end: audio → both models → + speaker-attributed transcription, compare vs NeMo SAS baseline. +- `tests/test_combined_streaming.cpp` — streaming combined with merge buffer. + +### 3.7 Publish + +- Quantize + publish diarization GGUF to HuggingFace. +- Publish combined ASR+diarization documentation. + +--- + +## Dependency graph + +``` +Phase 1 (offline diarization) + ├── 1.1 Converter ──────┐ + ├── 1.2 Loader ─────────┤ + ├── 1.3 DiarizationHead ┼── 1.4 DiarizationModel ── 1.5 C-API ── 1.6 Tests ── 1.7 Parity + └───────────────────────┘ + +Phase 2 (streaming diarization) + ├── 2.1 SortformerStreamingEncoder ── 2.2 C-API ── 2.3 GGUF KV ── 2.4 Tests + └── depends on Phase 1 (head + model + converter) + +Phase 3 (combined ASR + diarization) + ├── 3.1 Merge layer ── 3.2 C-API ── 3.3 Mel sharing ── 3.6 Tests ── 3.7 Publish + ├── 3.4 Streaming combined (depends on Phase 2) + ├── 3.5 LocalAI integration + └── depends on Phase 1 (offline diarization works) + Phase 2 (for streaming combined) +``` + +## Risk areas / unknowns + +1. **Sortformer transformer head attention type**: Need to verify whether the + Sortformer sorting head uses standard MHSA, relpos, or a variant. parakeet.cpp + currently only has `RelPosAttention`. If standard MHSA is needed, it's a new + ggml graph (not hard, but needs implementing). Check NeMo source for the exact + attention type. + +2. **AOSC cache mechanism**: The AOSC is the novel streaming component. Its exact + implementation (what is cached, how it's updated, how it interacts with the + transformer attention) needs to be understood from the NeMo source before + implementing. This is the highest-risk piece of Phase 2. + +3. **Mel config compatibility**: Whether the diarization model's mel config + matches any existing parakeet model's config (for mel sharing in Phase 3). + If `normalize="per_feature"`, streaming mel can't be used incrementally — + `MelFrontend::compute` on the full clip works fine (offline path). + +4. **Frame alignment**: ASR and diarization may have different subsampling + factors, producing different `frame_sec` values. The merge must handle this + by working in seconds (both produce timestamps in seconds), not frame indices. + +5. **Segment merging conventions**: NeMo's segment merging logic (consecutive + frames, same speaker, threshold, minimum segment duration) needs to be + matched exactly for parity. Check NeMo `diarize()` output format. diff --git a/examples/cli/CMakeLists.txt b/examples/cli/CMakeLists.txt index 3bb901f..ef14574 100644 --- a/examples/cli/CMakeLists.txt +++ b/examples/cli/CMakeLists.txt @@ -1,3 +1,7 @@ add_executable(parakeet-cli main.cpp) target_link_libraries(parakeet-cli PRIVATE parakeet) target_include_directories(parakeet-cli PRIVATE ${CMAKE_SOURCE_DIR}/src) + +add_executable(diarize diarize.cpp) +target_link_libraries(diarize PRIVATE parakeet) +target_include_directories(diarize PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/src) diff --git a/examples/cli/diarize.cpp b/examples/cli/diarize.cpp new file mode 100644 index 0000000..389de1a --- /dev/null +++ b/examples/cli/diarize.cpp @@ -0,0 +1,28 @@ +// Standalone diarize tool — loads a diarization GGUF and diarizes a WAV. +// Usage: diarize +// Prints JSON segments to stdout. +#include "parakeet_capi.h" +#include +#include + +int main(int argc, char** argv) { + if (argc < 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + parakeet_ctx* ctx = parakeet_capi_load(argv[1]); + if (!ctx) { + fprintf(stderr, "failed to load %s\n", argv[1]); + return 1; + } + char* json = parakeet_capi_diarize_path(ctx, argv[2]); + if (!json) { + fprintf(stderr, "diarize failed: %s\n", parakeet_capi_last_error(ctx)); + parakeet_capi_free(ctx); + return 1; + } + printf("%s\n", json); + parakeet_capi_free_string(json); + parakeet_capi_free(ctx); + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d828c6e..0db851b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -70,6 +70,16 @@ pk_add_test(test_capi_stream_json) pk_add_test(test_capi_timestamps) pk_add_test(test_capi_batch_json) pk_add_test(test_capi_ctc_logits) +pk_add_test(test_diarization) +pk_add_test(test_diarization_parity) +pk_add_test(test_diar_layer0) +pk_add_test(test_diar_layer0_bisect) +pk_add_test(test_diar_head_bisect) +pk_add_test(test_cont_check) +pk_add_test(test_subpixel_check) +pk_add_test(test_3d_permute) +pk_add_test(test_sas_merge) +pk_add_test(test_combined_offline) if(TARGET parakeet-cli) add_test(NAME cli_version_long COMMAND $ --version) @@ -127,6 +137,7 @@ set_tests_properties(test_model_loader test_mel test_mel_gpu test_subsampling te test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json test_capi_timestamps test_capi_batch_json test_capi_ctc_logits + test_combined_offline PROPERTIES LABELS "model") # These tests read fixtures/baselines via paths relative to the project root. set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_batch test_subsampling_batch_causal test_relpos_attention test_relpos_attention_batch test_conformer test_conformer_batch @@ -143,6 +154,7 @@ set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_bat test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json test_capi_timestamps test_capi_batch_json test_capi_ctc_logits + test_combined_offline PROPERTIES WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) # Python converter check (skips with exit 77 when the venv/model are absent). diff --git a/tests/test_3d_permute.cpp b/tests/test_3d_permute.cpp new file mode 100644 index 0000000..4090fdd --- /dev/null +++ b/tests/test_3d_permute.cpp @@ -0,0 +1,157 @@ +// Test: permute + cont on a 3D tensor (the subpixel case) +#include +#include +#include +#include "backend.hpp" +#include "ggml_graph.hpp" +#include "ggml.h" + +int main() { + // 3D tensor: ne=[4, 2, 3] = [T, up, tf] + // Data: flat[t + u*4 + h*8] + // h=0: [10,11,12,13, 20,21,22,23] + // h=1: [30,31,32,33, 40,41,42,43] + // h=2: [50,51,52,53, 60,61,62,63] + // + // After permute(2,0,1,3): ne=[3, 4, 2] = [tf, T, up] + // element(h, t, u) = old element(t, u, h) = flat[t + u*4 + h*8] + // + // After cont: data should be contiguous + // new_flat[h + t*3 + u*12] = old_flat[t + u*4 + h*8] + // + // After reshape_2d(3, 8): ne=[3, 8] = [tf, T_out] + // element(h, t') = new_flat[h + t'*3] + // where t' = t + u*4 = t + u*T_enc + // + // Expected: + // h=0: t'=0: t=0,u=0 → flat[0+0*4+0*8] = 10 + // t'=1: t=1,u=0 → flat[1+0+0] = 11 + // t'=2: t=2,u=0 → flat[2] = 12 + // t'=3: t=3,u=0 → flat[3] = 13 + // t'=4: t=0,u=1 → flat[0+4+0] = 20 + // t'=5: t=1,u=1 → flat[1+4] = 21 + // t'=6: t=2,u=1 → flat[2+4] = 22 + // t'=7: t=3,u=1 → flat[3+4] = 23 + + const int T_enc = 4, up = 2, tf = 3; + float data[24]; + for (int h = 0; h < tf; h++) + for (int u = 0; u < up; u++) + for (int t = 0; t < T_enc; t++) + data[t + u*T_enc + h*up*T_enc] = (h*up + u + 1) * 10 + t; + + printf("Input data: "); + for (int i = 0; i < 24; i++) printf("%.0f ", data[i]); + printf("\n\n"); + + // Test 1: permute + cont + reshape_2d + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t ne[3] = {T_enc, up, tf}; + ggml_tensor* t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 3, ne, + data, 24 * sizeof(float)); + // t: ne=[T_enc, up, tf] = [4, 2, 3] + ggml_tensor* p = ggml_permute(ctx, t, 2, 0, 1, 3); + // p: ne=[tf, T_enc, up] = [3, 4, 2] + ggml_tensor* c = ggml_cont(ctx, p); + // c: ne=[3, 4, 2], contiguous + ggml_tensor* r = ggml_reshape_2d(ctx, c, tf, T_enc * up); + // r: ne=[tf, T_out] = [3, 8] + return r; + }, out); + + printf("Test 1 (permute+cont+reshape_2d):\n"); + printf("Expected:\n"); + for (int h = 0; h < tf; h++) { + printf(" h%d: ", h); + for (int tp = 0; tp < T_enc * up; tp++) { + int u = tp / T_enc; + int t = tp % T_enc; + printf("%.0f ", data[t + u*T_enc + h*up*T_enc]); + } + printf("\n"); + } + printf("Got (out[h + t'*tf]):\n"); + for (int h = 0; h < tf; h++) { + printf(" h%d: ", h); + for (int tp = 0; tp < T_enc * up; tp++) { + printf("%.0f ", out[h + tp * tf]); + } + printf("\n"); + } + + bool pass = true; + for (int h = 0; h < tf; h++) { + for (int tp = 0; tp < T_enc * up; tp++) { + int u = tp / T_enc; + int t = tp % T_enc; + float expected = data[t + u*T_enc + h*up*T_enc]; + if (out[h + tp * tf] != expected) { pass = false; break; } + } + } + printf("Result: %s\n\n", pass ? "PASS" : "FAIL"); + } + + // Test 2: Just cont (no permute) — should be identity copy + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t ne[3] = {T_enc, up, tf}; + ggml_tensor* t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 3, ne, + data, 24 * sizeof(float)); + return ggml_cont(ctx, t); + }, out); + + printf("Test 2 (cont identity):\n"); + printf("Expected: "); + for (int i = 0; i < 24; i++) printf("%.0f ", data[i]); + printf("\nGot: "); + for (int i = 0; i < 24; i++) printf("%.0f ", out[i]); + printf("\nResult: %s\n\n", data == out.data() ? "?" : + (memcmp(data, out.data(), 24*sizeof(float)) == 0 ? "PASS" : "FAIL")); + } + + // Test 3: permute + cont only (no reshape) + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t ne[3] = {T_enc, up, tf}; + ggml_tensor* t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 3, ne, + data, 24 * sizeof(float)); + ggml_tensor* p = ggml_permute(ctx, t, 2, 0, 1, 3); + ggml_tensor* c = ggml_cont(ctx, p); + return c; + }, out); + + // After permute(2,0,1,3) + cont: ne=[3, 4, 2] + // element(h, t, u) = old_element(t, u, h) = data[t + u*4 + h*8] + // Contiguous: out[h + t*3 + u*12] + printf("Test 3 (permute+cont, no reshape):\n"); + printf("Expected (h + t*3 + u*12):\n"); + for (int h = 0; h < tf; h++) + for (int u = 0; u < up; u++) + for (int t = 0; t < T_enc; t++) { + int idx = h + t*3 + u*12; + printf(" [%d] = %.0f (h=%d,t=%d,u=%d)\n", idx, + data[t + u*4 + h*8], h, t, u); + } + printf("Got:\n"); + for (int i = 0; i < 24; i++) { + // Find which (h,t,u) this should be + // i = h + t*3 + u*12 → h = i%3, t = (i/3)%4, u = i/12 + int h = i % 3, t = (i / 3) % 4, u = i / 12; + printf(" [%d] = %.0f (should be h=%d,t=%d,u=%d → %.0f)\n", + i, out[i], h, t, u, data[t + u*4 + h*8]); + } + + bool pass = true; + for (int i = 0; i < 24; i++) { + int h = i % 3, t = (i / 3) % 4, u = i / 12; + if (out[i] != data[t + u*4 + h*8]) { pass = false; } + } + printf("Result: %s\n", pass ? "PASS" : "FAIL"); + } + + return 0; +} diff --git a/tests/test_combined_offline.cpp b/tests/test_combined_offline.cpp new file mode 100644 index 0000000..a3dbc20 --- /dev/null +++ b/tests/test_combined_offline.cpp @@ -0,0 +1,327 @@ +// End-to-end test for Phase 3: speaker-attributed ASR (SAS). +// +// Loads an ASR model and a diarization model, runs both on the same audio file +// via parakeet_capi_transcribe_and_diarize_json, and validates: +// - the JSON has "speakers", "utterances", and "words" arrays +// - every word has a valid speaker (-1 or 0..n_speakers-1) +// - utterances have text, start, end, speaker fields +// - the JSON is parseable +// +// Env: +// PARAKEET_TEST_GGUF ASR model (skip 77 if unset) +// PARAKEET_TEST_DIAR_GGUF diarization model (skip 77 if unset) +// PARAKEET_TEST_COMBINED_WAV audio file (default: tests/fixtures/speech.wav) + +#include "parakeet_capi.h" +#include "audio_io.hpp" + +#include +#include +#include +#include +#include + +// Reuse the tiny JSON scanner pattern from test_capi_timestamps.cpp. +namespace { + +struct Scan { + const std::string& s; + size_t i = 0; + explicit Scan(const std::string& str) : s(str) {} + void ws() { while (i < s.size() && (s[i]==' '||s[i]=='\t'||s[i]=='\n'||s[i]=='\r')) ++i; } + bool eat(char c) { ws(); if (i < s.size() && s[i]==c) { ++i; return true; } return false; } + bool str(std::string& out) { + ws(); + if (i >= s.size() || s[i] != '"') return false; + ++i; out.clear(); + while (i < s.size() && s[i] != '"') { + if (s[i] == '\\' && i + 1 < s.size()) { + char c = s[i+1]; + switch (c) { + case 'n': out += '\n'; break; case 't': out += '\t'; break; + case 'r': out += '\r'; break; case 'b': out += '\b'; break; + case 'f': out += '\f'; break; case '"': out += '"'; break; + case '\\': out += '\\'; break; case '/': out += '/'; break; + default: out += c; break; + } + i += 2; + } else { out += s[i++]; } + } + if (i >= s.size()) return false; + ++i; return true; + } + bool num(double& out) { + ws(); + size_t st = i; + while (i < s.size() && std::strchr("+-0123456789.eE", s[i])) ++i; + if (i == st) return false; + out = std::strtod(s.substr(st, i - st).c_str(), nullptr); + return true; + } + bool seek_key(const char* key) { + std::string pat = std::string("\"") + key + "\""; + size_t p = s.find(pat, i); + if (p == std::string::npos) return false; + i = p + pat.size(); + return eat(':'); + } +}; + +// Check that a JSON array key exists (e.g. "utterances":[...]). +bool has_array(const std::string& s, const char* key) { + Scan sc(s); + return sc.seek_key(key) && sc.eat('['); +} + +// Count elements in an array (rough: count top-level '{' or ',' at depth 1). +int count_array_elements(const std::string& s, const char* key) { + Scan sc(s); + if (!sc.seek_key(key)) return -1; + if (!sc.eat('[')) return -1; + sc.ws(); + if (sc.i < s.size() && s[sc.i] == ']') return 0; + int count = 0; + int depth = 0; + while (sc.i < s.size()) { + char c = s[sc.i]; + if (c == '{') { if (depth == 0) ++count; ++depth; } + else if (c == '}') { --depth; } + else if (c == ']' && depth == 0) break; + ++sc.i; + } + return count; +} + +// Parse all "speaker" integer values from the words array. +bool parse_word_speakers(const std::string& s, std::vector& speakers) { + Scan sc(s); + if (!sc.seek_key("words")) return false; + if (!sc.eat('[')) return false; + sc.ws(); + if (sc.i < s.size() && s[sc.i] == ']') return true; // empty + + while (true) { + if (!sc.eat('{')) return false; + // Parse fields until '}' + while (true) { + std::string key; + if (!sc.str(key)) return false; + if (!sc.eat(':')) return false; + if (key == "speaker") { + double v; + if (!sc.num(v)) return false; + speakers.push_back((int)v); + } else { + // Skip value: string or number + std::string tmp; + double d; + if (!sc.str(tmp) && !sc.num(d)) return false; + } + if (sc.eat(',')) continue; + break; + } + if (!sc.eat('}')) return false; + if (sc.eat(',')) continue; + break; + } + return sc.eat(']'); +} + +} // namespace + +int main() { + // ABI version sanity. + int abi = parakeet_capi_abi_version(); + if (abi < 7) { + std::fprintf(stderr, "test_combined_offline: abi version %d < 7 (need SAS)\n", abi); + return 1; + } + + const char* asr_gguf = std::getenv("PARAKEET_TEST_GGUF"); + if (!asr_gguf) { + std::fprintf(stderr, "test_combined_offline: PARAKEET_TEST_GGUF not set; skip\n"); + return 77; + } + const char* diar_gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + if (!diar_gguf) { + std::fprintf(stderr, "test_combined_offline: PARAKEET_TEST_DIAR_GGUF not set; skip\n"); + return 77; + } + + const char* wav = std::getenv("PARAKEET_TEST_COMBINED_WAV"); + if (!wav) wav = "tests/fixtures/speech.wav"; + + // Load both models. + parakeet_ctx* asr_ctx = parakeet_capi_load(asr_gguf); + if (!asr_ctx) { + std::fprintf(stderr, "test_combined_offline: ASR load failed: %s\n", + asr_ctx ? parakeet_capi_last_error(asr_ctx) : "(null)"); + return 1; + } + parakeet_ctx* diar_ctx = parakeet_capi_load(diar_gguf); + if (!diar_ctx) { + std::fprintf(stderr, "test_combined_offline: diar load failed: %s\n", + diar_ctx ? parakeet_capi_last_error(diar_ctx) : "(null)"); + parakeet_capi_free(asr_ctx); + return 1; + } + + // Load audio from file — we need raw PCM, so we use the diarize_path JSON + // variant as a smoke test... no, we need PCM for transcribe_and_diarize. + // Load the WAV using the ASR model's path transcribe (which loads the wav) + // — actually, we need to load the WAV ourselves. + // The C-API has no "load WAV to PCM" function, so we use a simple approach: + // call parakeet_capi_transcribe_and_diarize_json with a file path... no. + // Actually, the SAS API takes PCM samples. We need to read the WAV file + // ourselves. Let's use the existing test audio loading approach. + + // Read WAV using the shared audio_io loader. + pk::Audio audio; + if (!pk::load_audio_16k_mono(wav, audio) || audio.samples.empty()) { + std::fprintf(stderr, "test_combined_offline: cannot read %s\n", wav); + parakeet_capi_free(asr_ctx); + parakeet_capi_free(diar_ctx); + return 77; + } + + std::vector& pcm = audio.samples; + int sr = 16000; + + std::fprintf(stderr, "test_combined_offline: loaded %s (%d samples, %d Hz)\n", + wav, (int)pcm.size(), sr); + + // --- Test 1: JSON variant --- + char* json = parakeet_capi_transcribe_and_diarize_json( + asr_ctx, diar_ctx, pcm.data(), (int)pcm.size(), sr); + if (!json) { + std::fprintf(stderr, "test_combined_offline: transcribe_and_diarize_json NULL: %s\n", + parakeet_capi_last_error(asr_ctx)); + parakeet_capi_free(asr_ctx); + parakeet_capi_free(diar_ctx); + return 1; + } + + const std::string doc(json); + parakeet_capi_free_string(json); + + std::fprintf(stderr, "test_combined_offline: json head = %.200s ...\n", doc.c_str()); + + bool ok = true; + + // Validate JSON structure: must have speakers, utterances, words. + if (!has_array(doc, "utterances")) { + std::fprintf(stderr, "test_combined_offline: missing \"utterances\" array\n"); + ok = false; + } + if (!has_array(doc, "words")) { + std::fprintf(stderr, "test_combined_offline: missing \"words\" array\n"); + ok = false; + } + + // Check "speakers" field exists and is positive. + { + Scan sc(doc); + if (!sc.seek_key("speakers")) { + std::fprintf(stderr, "test_combined_offline: missing \"speakers\" field\n"); + ok = false; + } else { + double spk; + if (!sc.num(spk) || spk <= 0) { + std::fprintf(stderr, "test_combined_offline: invalid speakers value\n"); + ok = false; + } else { + std::fprintf(stderr, "test_combined_offline: speakers = %.0f\n", spk); + } + } + } + + // Parse word speakers and validate range. + { + std::vector speakers; + if (!parse_word_speakers(doc, speakers)) { + std::fprintf(stderr, "test_combined_offline: failed to parse word speakers\n"); + ok = false; + } else { + std::fprintf(stderr, "test_combined_offline: %zu words parsed\n", + speakers.size()); + // Check that all speaker indices are valid (-1 or 0..7) + for (size_t i = 0; i < speakers.size(); ++i) { + if (speakers[i] < -1 || speakers[i] > 7) { + std::fprintf(stderr, + "test_combined_offline: word[%zu] speaker=%d out of range\n", + i, speakers[i]); + ok = false; + break; + } + } + } + } + + // Count utterances and words. + int n_utts = count_array_elements(doc, "utterances"); + int n_words = count_array_elements(doc, "words"); + std::fprintf(stderr, "test_combined_offline: %d utterances, %d words\n", + n_utts, n_words); + + if (n_utts < 0 || n_words < 0) { + std::fprintf(stderr, "test_combined_offline: failed to count arrays\n"); + ok = false; + } + if (n_words == 0) { + std::fprintf(stderr, "test_combined_offline: no words transcribed\n"); + ok = false; + } + + // --- Test 2: struct variant --- + int n_results = 0; + parakeet_sas_result* results = parakeet_capi_transcribe_and_diarize( + asr_ctx, diar_ctx, pcm.data(), (int)pcm.size(), sr, &n_results); + if (!results) { + std::fprintf(stderr, "test_combined_offline: transcribe_and_diarize NULL: %s\n", + parakeet_capi_last_error(asr_ctx)); + ok = false; + } else { + std::fprintf(stderr, "test_combined_offline: struct variant returned %d results\n", + n_results); + if (n_results != n_utts) { + std::fprintf(stderr, + "test_combined_offline: struct count %d != JSON count %d\n", + n_results, n_utts); + ok = false; + } + // Validate each result: speaker in range, text non-null, start < end. + for (int i = 0; i < n_results && i < 20; ++i) { + if (results[i].speaker < -1 || results[i].speaker > 7) { + std::fprintf(stderr, + "test_combined_offline: result[%d] speaker=%d out of range\n", + i, results[i].speaker); + ok = false; + } + if (!results[i].text) { + std::fprintf(stderr, + "test_combined_offline: result[%d] text is null\n", i); + ok = false; + } + if (results[i].end < results[i].start) { + std::fprintf(stderr, + "test_combined_offline: result[%d] end < start\n", i); + ok = false; + } + } + // Free text strings and the array. + for (int i = 0; i < n_results; ++i) { + if (results[i].text) parakeet_capi_free_string(results[i].text); + } + parakeet_capi_free_sas_results(results); + } + + parakeet_capi_free(asr_ctx); + parakeet_capi_free(diar_ctx); + + if (!ok) { + std::fprintf(stderr, "test_combined_offline: FAIL\n"); + return 1; + } + std::fprintf(stderr, "test_combined_offline: PASS\n"); + return 0; +} diff --git a/tests/test_cont_check.cpp b/tests/test_cont_check.cpp new file mode 100644 index 0000000..750176a --- /dev/null +++ b/tests/test_cont_check.cpp @@ -0,0 +1,64 @@ +// Minimal test: verify ggml_cont actually rearranges data in this backend. +#include +#include +#include "backend.hpp" +#include "ggml_graph.hpp" +#include "ggml.h" + +int main() { + // Create a simple 2x4 tensor, transpose it, cont it, and check if data + // is actually rearranged. + // Original: ne=[4, 2] = [cols, rows], data: flat[c + r*4] + // 0 1 2 3 + // 4 5 6 7 + // flat = [0, 1, 2, 3, 4, 5, 6, 7] + // + // After transpose: ne=[2, 4], strides say data should be + // 0 4 + // 1 5 + // 2 6 + // 3 7 + // But data is still flat = [0, 1, 2, 3, 4, 5, 6, 7] + // + // After cont: data should be flat = [0, 4, 1, 5, 2, 6, 3, 7] + // (reading row-by-row: row 0 = [0, 4], row 1 = [1, 5], etc.) + + float input_data[8] = {0, 1, 2, 3, 4, 5, 6, 7}; + + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t ne[2] = {4, 2}; // ne[0]=4 (cols), ne[1]=2 (rows) + ggml_tensor* t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, ne, + input_data, 8 * sizeof(float)); + // t: ne=[4, 2], data=[0,1,2,3,4,5,6,7] + + // Transpose: ne=[2, 4], data unchanged + ggml_tensor* tt = ggml_transpose(ctx, t); + // tt: ne=[2, 4], but data is still [0,1,2,3,4,5,6,7] + + // Cont: should rearrange to [0,4,1,5,2,6,3,7] + ggml_tensor* ct = ggml_cont(ctx, tt); + // ct: ne=[2, 4], data should be [0,4,1,5,2,6,3,7] + + return ct; + }, out); + + if (!ok) { + printf("FAIL: run_graph returned false\n"); + return 1; + } + + printf("Expected: 0 4 1 5 2 6 3 7\n"); + printf("Got: "); + for (int i = 0; i < 8; i++) printf("%.0f ", out[i]); + printf("\n"); + + float expected[8] = {0, 4, 1, 5, 2, 6, 3, 7}; + bool pass = true; + for (int i = 0; i < 8; i++) { + if (out[i] != expected[i]) { pass = false; break; } + } + + printf("Result: %s\n", pass ? "PASS - cont works" : "FAIL - cont is no-op"); + return pass ? 0 : 1; +} diff --git a/tests/test_diar_head_bisect.cpp b/tests/test_diar_head_bisect.cpp new file mode 100644 index 0000000..211fea0 --- /dev/null +++ b/tests/test_diar_head_bisect.cpp @@ -0,0 +1,189 @@ +// test_diar_head_bisect.cpp — dump diarization head intermediates stage by stage. +#include "diarization.hpp" +#include "mel.hpp" +#include "model_loader.hpp" +#include "backend.hpp" +#include "audio_io.hpp" +#include "diarization_encoder.hpp" +#include "diarization_head.hpp" +#include "ggml_graph.hpp" +#include "ggml.h" + +#include +#include +#include +#include + +static void write_npy_f32(const std::string& path, const float* data, + const std::vector& shape) { + std::string magic = "\x93NUMPY"; + uint8_t version[2] = {1, 0}; + std::string dict = "{'descr': ' 0) dict += ", "; + dict += std::to_string(shape[i]); + } + if (shape.size() == 1) dict += ","; + dict += "), }"; + int overhead = 10; + int target = overhead + dict.size() + 1; + int padded = ((target + 63) / 64) * 64; + int n_pad = padded - target; + uint16_t hlen = (uint16_t)(dict.size() + n_pad + 1); + std::ofstream f(path, std::ios::binary); + f.write(magic.data(), 6); + f.write((char*)version, 2); + f.write((char*)&hlen, 2); + f.write(dict.data(), (std::streamsize)dict.size()); + for (int i = 0; i < n_pad; ++i) f.write(" ", 1); + f.write("\n", 1); + size_t n = 1; + for (auto s : shape) n *= s; + f.write((const char*)data, (std::streamsize)(n * sizeof(float))); +} + +int main() { + const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + const char* wav_path = std::getenv("PARAKEET_TEST_DIAR_WAV"); + const char* out_dir = std::getenv("PARAKEET_TEST_DIAR_OUT"); + if (!gguf || !wav_path || !out_dir) return 77; + + auto m = pk::DiarizationModel::load(gguf); + if (!m) return 1; + pk::Audio audio; + if (!pk::load_audio_16k_mono(wav_path, audio)) return 1; + + const pk::ModelLoader& ml = m->loader(); + pk::MelFrontend mel(ml); + pk::DiarizationEncoder encoder(ml); + + std::vector feats; + int n_mels = 0, T = 0; + mel.compute(audio.samples, feats, n_mels, T); + + std::vector enc_out; + int d_model = 0, T_enc = 0; + encoder.forward(feats, n_mels, T, enc_out, d_model, T_enc); + + const auto& cfg = ml.config(); + int tf = (int)cfg.diarization.tf_d_model; + int n_spk = (int)cfg.diarization.n_speakers; + int up = (int)cfg.diarization.upsample_factor; + int T_out = T_enc * up; + + pk::ensure_weights_realized(ml); + + // Stage 1: encoder_proj output + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t xt_ne[2] = {T_enc, d_model}; + ggml_tensor* xt = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, xt_ne, + enc_out.data(), (size_t)d_model * T_enc * sizeof(float)); + ggml_tensor* ep_w = ml.tensor("sortformer_modules.encoder_proj.weight"); + ggml_tensor* W_proj = ggml_reshape_2d(ctx, ep_w, d_model, tf); + ggml_tensor* xt_t = ggml_cont(ctx, ggml_transpose(ctx, xt)); + ggml_tensor* proj = ggml_mul_mat(ctx, W_proj, xt_t); + ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); + if (ep_b) proj = ggml_add(ctx, proj, ep_b); + // proj: ne[0]=tf, ne[1]=T_enc + // Transpose to ne[0]=T_enc, ne[1]=tf to match PyTorch [T, tf] + proj = ggml_cont(ctx, ggml_transpose(ctx, proj)); + return proj; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/proj_pk.npy", out.data(), + {(int64_t)T_enc, (int64_t)tf}); + printf("proj: [%d, %d] first3: %.4f %.4f %.4f\n", T_enc, tf, out[0], out[1], out[2]); + } + + // Stage 2: conv output (pre-bias, pre-reshape) + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t xt_ne[2] = {T_enc, d_model}; + ggml_tensor* xt = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, xt_ne, + enc_out.data(), (size_t)d_model * T_enc * sizeof(float)); + ggml_tensor* ep_w = ml.tensor("sortformer_modules.encoder_proj.weight"); + ggml_tensor* W_proj = ggml_reshape_2d(ctx, ep_w, d_model, tf); + ggml_tensor* xt_t = ggml_cont(ctx, ggml_transpose(ctx, xt)); + ggml_tensor* proj = ggml_mul_mat(ctx, W_proj, xt_t); + ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); + if (ep_b) proj = ggml_add(ctx, proj, ep_b); + + // conv input: ne[0]=T_enc, ne[1]=tf, ne[2]=1 + ggml_tensor* conv_in = ggml_cont(ctx, ggml_transpose(ctx, proj)); + conv_in = ggml_reshape_3d(ctx, conv_in, T_enc, tf, 1); + + ggml_tensor* spk_w = ml.tensor("sortformer_modules.subpixel_upsample.weight"); + spk_w = ggml_cast(ctx, spk_w, GGML_TYPE_F16); + conv_in = ggml_cast(ctx, conv_in, GGML_TYPE_F16); + + ggml_tensor* conv_out = ggml_conv_1d(ctx, spk_w, conv_in, 1, 1, 1); + // conv_out: ne[0]=T_enc, ne[1]=tf*up, ne[2]=1 + // Reshape to 2D and transpose to [tf*up, T_enc] + conv_out = ggml_reshape_2d(ctx, conv_out, T_enc, tf * up); + conv_out = ggml_cont(ctx, ggml_transpose(ctx, conv_out)); + // ne[0]=tf*up, ne[1]=T_enc + ggml_tensor* spk_b = ml.tensor("sortformer_modules.subpixel_upsample.bias"); + if (spk_b) conv_out = ggml_add(ctx, conv_out, spk_b); + // Transpose to [T_enc, tf*up] to match PyTorch [T, OC] + conv_out = ggml_cont(ctx, ggml_transpose(ctx, conv_out)); + return conv_out; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/conv_pk.npy", out.data(), + {(int64_t)T_enc, (int64_t)(tf * up)}); + printf("conv: [%d, %d] first3: %.4f %.4f %.4f\n", T_enc, tf*up, out[0], out[1], out[2]); + } + + // Stage 3: upsampled (after subpixel reshape) + // Matches diarization_head.cpp: add bias directly (no transpose), then subpixel reshape + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t xt_ne[2] = {T_enc, d_model}; + ggml_tensor* xt = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, xt_ne, + enc_out.data(), (size_t)d_model * T_enc * sizeof(float)); + ggml_tensor* ep_w = ml.tensor("sortformer_modules.encoder_proj.weight"); + ggml_tensor* W_proj = ggml_reshape_2d(ctx, ep_w, d_model, tf); + ggml_tensor* xt_t = ggml_cont(ctx, ggml_transpose(ctx, xt)); + ggml_tensor* proj = ggml_mul_mat(ctx, W_proj, xt_t); + ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); + if (ep_b) proj = ggml_add(ctx, proj, ep_b); + + ggml_tensor* conv_in = ggml_cont(ctx, ggml_transpose(ctx, proj)); + conv_in = ggml_reshape_3d(ctx, conv_in, T_enc, tf, 1); + + ggml_tensor* spk_w = ml.tensor("sortformer_modules.subpixel_upsample.weight"); + spk_w = ggml_cast(ctx, spk_w, GGML_TYPE_F16); + conv_in = ggml_cast(ctx, conv_in, GGML_TYPE_F16); + + ggml_tensor* conv_out = ggml_conv_1d(ctx, spk_w, conv_in, 1, 1, 1); + // conv_out: ne=[T_enc, tf*up, 1], data: flat[t + c*T_enc] + conv_out = ggml_reshape_2d(ctx, conv_out, T_enc, tf * up); + + // Add bias directly (no transpose) + ggml_tensor* spk_b = ml.tensor("sortformer_modules.subpixel_upsample.bias"); + if (spk_b) { + ggml_tensor* spk_b_2d = ggml_reshape_2d(ctx, spk_b, 1, tf * up); + conv_out = ggml_add(ctx, conv_out, spk_b_2d); + } + + // Subpixel reshape (matches head code) + ggml_tensor* upsampled = ggml_reshape_3d(ctx, conv_out, T_enc, up, tf); + upsampled = ggml_reshape_2d(ctx, upsampled, T_enc * up, tf); + upsampled = ggml_cont(ctx, ggml_transpose(ctx, upsampled)); + // ne[0]=tf, ne[1]=T_out + // Transpose to [T_out, tf] to match PyTorch + upsampled = ggml_cont(ctx, ggml_transpose(ctx, upsampled)); + return upsampled; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/up_pk.npy", out.data(), + {(int64_t)T_out, (int64_t)tf}); + printf("up: [%d, %d] first3: %.4f %.4f %.4f\n", T_out, tf, out[0], out[1], out[2]); + } + + return 0; +} diff --git a/tests/test_diar_layer0.cpp b/tests/test_diar_layer0.cpp new file mode 100644 index 0000000..dfc7fb2 --- /dev/null +++ b/tests/test_diar_layer0.cpp @@ -0,0 +1,256 @@ +// test_diar_layer0.cpp — dump intermediate outputs after embed_norm and layer 0 +// for comparison with the PyTorch reference. +#include "diarization.hpp" +#include "mel.hpp" +#include "model_loader.hpp" +#include "backend.hpp" +#include "audio_io.hpp" +#include "diarization_encoder.hpp" +#include "diarization_head.hpp" +#include "ggml_graph.hpp" +#include "graph_builder.hpp" +#include "ggml.h" + +#include +#include +#include +#include +#include + +static void write_npy_f32(const std::string& path, const float* data, + const std::vector& shape) { + std::string magic = "\x93NUMPY"; + uint8_t version[2] = {1, 0}; + std::string dict = "{'descr': ' 0) dict += ", "; + dict += std::to_string(shape[i]); + } + if (shape.size() == 1) dict += ","; + dict += "), }"; + int overhead = 10; + int target = overhead + dict.size() + 1; + int padded = ((target + 63) / 64) * 64; + int n_pad = padded - target; + uint16_t hlen = (uint16_t)(dict.size() + n_pad + 1); + std::ofstream f(path, std::ios::binary); + f.write(magic.data(), 6); + f.write((char*)version, 2); + f.write((char*)&hlen, 2); + f.write(dict.data(), (std::streamsize)dict.size()); + for (int i = 0; i < n_pad; ++i) f.write(" ", 1); + f.write("\n", 1); + size_t n = 1; + for (auto s : shape) n *= s; + f.write((const char*)data, (std::streamsize)(n * sizeof(float))); +} + +int main() { + const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + const char* wav_path = std::getenv("PARAKEET_TEST_DIAR_WAV"); + const char* out_dir = std::getenv("PARAKEET_TEST_DIAR_OUT"); + if (!gguf || !wav_path || !out_dir) return 77; + + auto m = pk::DiarizationModel::load(gguf); + if (!m) return 1; + + pk::Audio audio; + if (!pk::load_audio_16k_mono(wav_path, audio)) return 1; + + const pk::ModelLoader& ml = m->loader(); + pk::MelFrontend mel(ml); + + std::vector feats; + int n_mels = 0, T = 0; + mel.compute(audio.samples, feats, n_mels, T); + + // Now build the graph manually up to embed_norm + layer 0 + const auto& cfg = ml.config(); + int d_model = (int)cfg.d_model; + int n_heads = (int)cfg.n_heads; + int head_dim = d_model / n_heads; + int n_layers = (int)cfg.n_layers; + int ff_dim = (int)cfg.ff_dim; + int factor = (int)cfg.subsampling_factor; + int n_mels_cfg = (int)cfg.n_mels; + float ln_eps = 1e-5f; + int n_rot = head_dim; // rotary_fraction=1.0 + + int pad = (factor - (T % factor)) % factor; + int T_padded = T + pad; + int Tp = T_padded / factor; + + std::vector positions(Tp); + for (int i = 0; i < Tp; ++i) positions[i] = i; + + pk::ensure_weights_realized(ml); + pk::GraphInputPool pool; + + // Output: after embed_norm [Tp, d_model] (channels-last, like PyTorch) + std::vector embed_norm_out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + // FeatureStacking + int64_t mel_ne[2] = {T_padded, n_mels_cfg}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); + for (int mm = 0; mm < n_mels_cfg; ++mm) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); // [d_model, Tp] + + // embed_norm + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + x = ggml_norm(ctx, x, ln_eps); + x = ggml_mul(ctx, x, g); + x = ggml_add(ctx, x, b); + // x is [d_model, Tp] (channels-first in ggml) + // PyTorch has [Tp, d_model] (channels-last) + // Transpose for comparison + x = ggml_cont(ctx, ggml_transpose(ctx, x)); + return x; + }, embed_norm_out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/embed_norm_pk.npy", + embed_norm_out.data(), {(int64_t)Tp, (int64_t)d_model}); + std::printf("embed_norm: [%d, %d]\n", Tp, d_model); + std::printf(" first 5: "); + for (int i = 0; i < 5; ++i) std::printf("%.4f ", embed_norm_out[i]); + std::printf("\n"); + + // Layer 0 full + std::vector layer0_out; + ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + // FeatureStacking + embed_norm + int64_t mel_ne[2] = {T_padded, n_mels_cfg}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); + for (int mm = 0; mm < n_mels_cfg; ++mm) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); + + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + x = ggml_norm(ctx, x, ln_eps); + x = ggml_mul(ctx, x, g); + x = ggml_add(ctx, x, b); + } + // x: [d_model, Tp] + + // Position tensor + int64_t pos_ne[1] = {Tp}; + ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, + positions.data(), (size_t)Tp * sizeof(int32_t)); + + // Layer 0 + std::string base = "encoder.layers.0."; + + // norm1 + { + ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_mul(ctx, h, ng); + h = ggml_add(ctx, h, nb); + + // QKV + ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); + ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); // [1536, Tp] + qkv = ggml_cont(ctx, qkv); + qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); // [512, 3, Tp] + + size_t qkv_ts = (size_t)3 * d_model * sizeof(float); + ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); + ggml_tensor* k = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)d_model * sizeof(float)); + ggml_tensor* v = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)2 * d_model * sizeof(float)); + + q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); + k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), head_dim, n_heads, Tp); + v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), head_dim, n_heads, Tp); + + // RoPE + q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, + 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, + 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + // Manual attention: permute to [hd, Tp, H] + q = ggml_permute(ctx, q, 0, 2, 1, 3); + k = ggml_permute(ctx, k, 0, 2, 1, 3); + v = ggml_permute(ctx, v, 0, 2, 1, 3); + + // scores = mul_mat(k, q) → [Tp, Tp, H] + ggml_tensor* scores = ggml_mul_mat(ctx, k, q); + float attn_scale = 1.0f / std::sqrt((float)head_dim); + scores = ggml_scale(ctx, scores, attn_scale); + scores = ggml_soft_max(ctx, scores); + + // out = mul_mat(v_t, scores) + ggml_tensor* v_t = ggml_permute(ctx, v, 1, 0, 2, 3); // [Tp, hd, H] + v_t = ggml_cont(ctx, v_t); + ggml_tensor* attn_out = ggml_mul_mat(ctx, v_t, scores); // [Tp, hd, H] + attn_out = ggml_permute(ctx, attn_out, 1, 0, 2, 3); // [hd, Tp, H] + attn_out = ggml_cont(ctx, attn_out); + ggml_tensor* attn = ggml_reshape_2d(ctx, attn_out, (int64_t)d_model, (int64_t)Tp); + + // out_proj + ggml_tensor* op_w = pk::clone_weight(ctx, ml, (base + "attn.out_proj.weight").c_str()); + attn = ggml_mul_mat(ctx, op_w, attn); + ggml_tensor* op_b = pk::clone_weight_opt(ctx, ml, (base + "attn.out_proj.bias").c_str()); + if (op_b) attn = ggml_add(ctx, attn, op_b); + + // Residual + x = ggml_add(ctx, x, attn); + } + + // FFN + { + ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm2.weight").c_str()); + ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm2.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_mul(ctx, h, ng); + h = ggml_add(ctx, h, nb); + + ggml_tensor* f0_w = pk::clone_weight(ctx, ml, (base + "ffn.net.0.weight").c_str()); + h = ggml_mul_mat(ctx, f0_w, h); + ggml_tensor* f0_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.0.bias").c_str()); + if (f0_b) h = ggml_add(ctx, h, f0_b); + h = ggml_gelu(ctx, h); + + ggml_tensor* f3_w = pk::clone_weight(ctx, ml, (base + "ffn.net.3.weight").c_str()); + h = ggml_mul_mat(ctx, f3_w, h); + ggml_tensor* f3_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.3.bias").c_str()); + if (f3_b) h = ggml_add(ctx, h, f3_b); + + x = ggml_add(ctx, x, h); + } + + // x is [d_model, Tp] (ne[0]=d_model, ne[1]=Tp) + // The output flat buffer is time-major: [d0_t0, d1_t0, ..., d511_t0, d0_t1, ...] + // = flat[t * d_model + d] + // PyTorch has [Tp, d_model] = flat[t * d_model + d] — same! + // So NO transpose needed. Just return x directly. + return x; + }, layer0_out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/layer0_pk.npy", + layer0_out.data(), {(int64_t)Tp, (int64_t)d_model}); + std::printf("layer0: [%d, %d]\n", Tp, d_model); + std::printf(" first 5: "); + for (int i = 0; i < 5; ++i) std::printf("%.4f ", layer0_out[i]); + std::printf("\n"); + + return 0; +} diff --git a/tests/test_diar_layer0_bisect.cpp b/tests/test_diar_layer0_bisect.cpp new file mode 100644 index 0000000..56e0a4a --- /dev/null +++ b/tests/test_diar_layer0_bisect.cpp @@ -0,0 +1,434 @@ +// test_diar_layer0_bisect.cpp — dump layer-0 intermediates stage by stage +// for comparison with PyTorch reference (dump_layer0_ref.py). +#include "diarization.hpp" +#include "mel.hpp" +#include "model_loader.hpp" +#include "backend.hpp" +#include "audio_io.hpp" +#include "diarization_encoder.hpp" +#include "diarization_head.hpp" +#include "ggml_graph.hpp" +#include "graph_builder.hpp" +#include "ggml.h" + +#include +#include +#include +#include +#include + +static void write_npy_f32(const std::string& path, const float* data, + const std::vector& shape) { + std::string magic = "\x93NUMPY"; + uint8_t version[2] = {1, 0}; + std::string dict = "{'descr': ' 0) dict += ", "; + dict += std::to_string(shape[i]); + } + if (shape.size() == 1) dict += ","; + dict += "), }"; + int overhead = 10; + int target = overhead + dict.size() + 1; + int padded = ((target + 63) / 64) * 64; + int n_pad = padded - target; + uint16_t hlen = (uint16_t)(dict.size() + n_pad + 1); + std::ofstream f(path, std::ios::binary); + f.write(magic.data(), 6); + f.write((char*)version, 2); + f.write((char*)&hlen, 2); + f.write(dict.data(), (std::streamsize)dict.size()); + for (int i = 0; i < n_pad; ++i) f.write(" ", 1); + f.write("\n", 1); + size_t n = 1; + for (auto s : shape) n *= s; + f.write((const char*)data, (std::streamsize)(n * sizeof(float))); +} + +int main() { + const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + const char* wav_path = std::getenv("PARAKEET_TEST_DIAR_WAV"); + const char* out_dir = std::getenv("PARAKEET_TEST_DIAR_OUT"); + if (!gguf || !wav_path || !out_dir) return 77; + + auto m = pk::DiarizationModel::load(gguf); + if (!m) return 1; + + pk::Audio audio; + if (!pk::load_audio_16k_mono(wav_path, audio)) return 1; + + const pk::ModelLoader& ml = m->loader(); + pk::MelFrontend mel(ml); + + std::vector feats; + int n_mels = 0, T = 0; + mel.compute(audio.samples, feats, n_mels, T); + + const auto& cfg = ml.config(); + int d_model = (int)cfg.d_model; + int n_heads = (int)cfg.n_heads; + int head_dim = d_model / n_heads; + int factor = (int)cfg.subsampling_factor; + int n_mels_cfg = (int)cfg.n_mels; + float ln_eps = 1e-5f; + int n_rot = head_dim; + + int pad = (factor - (T % factor)) % factor; + int T_padded = T + pad; + int Tp = T_padded / factor; + + std::vector positions(Tp); + for (int i = 0; i < Tp; ++i) positions[i] = i; + + pk::ensure_weights_realized(ml); + pk::GraphInputPool pool; + + // ---- Stage 1: norm1 output ---- + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t mel_ne[2] = {T_padded, n_mels_cfg}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); + for (int mm = 0; mm < n_mels_cfg; ++mm) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); + + // embed_norm + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + x = ggml_norm(ctx, x, ln_eps); + x = ggml_mul(ctx, x, g); + x = ggml_add(ctx, x, b); + } + + // norm1 + std::string base = "encoder.layers.0."; + ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_mul(ctx, h, ng); + h = ggml_add(ctx, h, nb); + // h is [d_model, Tp] in ggml. PyTorch norm1 is [Tp, d_model]. + // ggml flat: [t * d_model + d] = same as PyTorch [t, d] row-major. + return h; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/l0_norm1_pk.npy", out.data(), + {(int64_t)Tp, (int64_t)d_model}); + printf("norm1: [Tp=%d, d=%d] first5: ", Tp, d_model); + for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); + printf("\n"); + } + + // ---- Stage 2: QKV ---- + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t mel_ne[2] = {T_padded, n_mels_cfg}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); + for (int mm = 0; mm < n_mels_cfg; ++mm) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + x = ggml_norm(ctx, x, ln_eps); + x = ggml_mul(ctx, x, g); + x = ggml_add(ctx, x, b); + } + std::string base = "encoder.layers.0."; + ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_mul(ctx, h, ng); + h = ggml_add(ctx, h, nb); + + // QKV + ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); + ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); // [3*d, Tp] + qkv = ggml_cont(ctx, qkv); + // PyTorch qkv: [B, T, 3*D] → flat[t * 3*D + j] + // ggml: [3*d, Tp] → flat[t * 3*d + j] — same! + return qkv; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/l0_qkv_pk.npy", out.data(), + {(int64_t)Tp, (int64_t)(3 * d_model)}); + printf("qkv: [Tp=%d, 3d=%d] first5: ", Tp, 3*d_model); + for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); + printf("\n"); + } + + // ---- Stage 3: Q pre-RoPE ---- + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t mel_ne[2] = {T_padded, n_mels_cfg}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); + for (int mm = 0; mm < n_mels_cfg; ++mm) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + x = ggml_norm(ctx, x, ln_eps); + x = ggml_mul(ctx, x, g); + x = ggml_add(ctx, x, b); + } + std::string base = "encoder.layers.0."; + ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_mul(ctx, h, ng); + h = ggml_add(ctx, h, nb); + + ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); + ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); + qkv = ggml_cont(ctx, qkv); + qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); + size_t qkv_ts = (size_t)3 * d_model * sizeof(float); + ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); + q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); + // q: [hd, H, Tp] in ggml + // PyTorch q: [H, T, hd] — flat[t * H * hd + h * hd + d] = [t * d_model + h * hd + d] + // ggml [hd, H, Tp]: flat[t * H * hd + h * hd + d] — same! + return q; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/l0_q_pk.npy", out.data(), + {(int64_t)Tp, (int64_t)d_model}); + printf("q_pre_rope: [Tp=%d, d=%d] first5: ", Tp, d_model); + for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); + printf("\n"); + } + + // ---- Stage 4: Q post-RoPE ---- + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t mel_ne[2] = {T_padded, n_mels_cfg}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); + for (int mm = 0; mm < n_mels_cfg; ++mm) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + x = ggml_norm(ctx, x, ln_eps); + x = ggml_mul(ctx, x, g); + x = ggml_add(ctx, x, b); + } + std::string base = "encoder.layers.0."; + ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_mul(ctx, h, ng); + h = ggml_add(ctx, h, nb); + + int64_t pos_ne[1] = {Tp}; + ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, + positions.data(), (size_t)Tp * sizeof(int32_t)); + + ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); + ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); + qkv = ggml_cont(ctx, qkv); + qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); + size_t qkv_ts = (size_t)3 * d_model * sizeof(float); + ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); + q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); + q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, + 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + return q; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/l0_q_rot_pk.npy", out.data(), + {(int64_t)Tp, (int64_t)d_model}); + printf("q_post_rope: [Tp=%d, d=%d] first5: ", Tp, d_model); + for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); + printf("\n"); + } + + // ---- Stage 5: attention output (pre out_proj) ---- + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t mel_ne[2] = {T_padded, n_mels_cfg}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); + for (int mm = 0; mm < n_mels_cfg; ++mm) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + x = ggml_norm(ctx, x, ln_eps); + x = ggml_mul(ctx, x, g); + x = ggml_add(ctx, x, b); + } + std::string base = "encoder.layers.0."; + ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_mul(ctx, h, ng); + h = ggml_add(ctx, h, nb); + + int64_t pos_ne[1] = {Tp}; + ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, + positions.data(), (size_t)Tp * sizeof(int32_t)); + + ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); + ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); + qkv = ggml_cont(ctx, qkv); + qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); + size_t qkv_ts = (size_t)3 * d_model * sizeof(float); + ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); + ggml_tensor* k = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)d_model * sizeof(float)); + ggml_tensor* v = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)2 * d_model * sizeof(float)); + q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); + k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), head_dim, n_heads, Tp); + v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), head_dim, n_heads, Tp); + q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, + 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, + 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + // Manual attention (same as test_diar_layer0.cpp) + q = ggml_permute(ctx, q, 0, 2, 1, 3); // [hd, Tp, H] + k = ggml_permute(ctx, k, 0, 2, 1, 3); + v = ggml_permute(ctx, v, 0, 2, 1, 3); + ggml_tensor* scores = ggml_mul_mat(ctx, k, q); // [Tp, Tp, H] + float attn_scale = 1.0f / std::sqrt((float)head_dim); + scores = ggml_scale(ctx, scores, attn_scale); + scores = ggml_soft_max(ctx, scores); + ggml_tensor* v_t = ggml_permute(ctx, v, 1, 0, 2, 3); // [Tp, hd, H] + v_t = ggml_cont(ctx, v_t); + ggml_tensor* attn_out = ggml_mul_mat(ctx, v_t, scores); // [Tp, hd, H] -> [hd, Tp, H]? + // Actually mul_mat(v_t [Tp,hd,H], scores [Tp,Tp,H]) gives [hd, Tp, H] + attn_out = ggml_permute(ctx, attn_out, 1, 0, 2, 3); // hmm + + // Let's just use flash_attn_ext instead + (void)attn_out; // unused + float scale = 1.0f / std::sqrt((float)head_dim); + ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, scale, 0.0f, 0.0f); + attn = ggml_permute(ctx, attn, 0, 2, 1, 3); // [hd, H, Tp, 1] + attn = ggml_cont(ctx, attn); + attn = ggml_reshape_2d(ctx, attn, (int64_t)d_model, (int64_t)Tp); + // attn: [d_model, Tp], flat[t * d + d] = same as PyTorch [Tp, d_model] + return attn; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/l0_attn_pk.npy", out.data(), + {(int64_t)Tp, (int64_t)d_model}); + printf("attn: [Tp=%d, d=%d] first5: ", Tp, d_model); + for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); + printf("\n"); + } + + // ---- Stage 6: attn_out (after out_proj) ---- + { + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t mel_ne[2] = {T_padded, n_mels_cfg}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); + for (int mm = 0; mm < n_mels_cfg; ++mm) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + x = ggml_norm(ctx, x, ln_eps); + x = ggml_mul(ctx, x, g); + x = ggml_add(ctx, x, b); + } + std::string base = "encoder.layers.0."; + ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_mul(ctx, h, ng); + h = ggml_add(ctx, h, nb); + + int64_t pos_ne[1] = {Tp}; + ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, + positions.data(), (size_t)Tp * sizeof(int32_t)); + + ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); + ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); + qkv = ggml_cont(ctx, qkv); + qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); + size_t qkv_ts = (size_t)3 * d_model * sizeof(float); + ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); + ggml_tensor* k = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)d_model * sizeof(float)); + ggml_tensor* v = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)2 * d_model * sizeof(float)); + q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); + k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), head_dim, n_heads, Tp); + v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), head_dim, n_heads, Tp); + q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, + 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, + 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + q = ggml_permute(ctx, q, 0, 2, 1, 3); + k = ggml_permute(ctx, k, 0, 2, 1, 3); + v = ggml_permute(ctx, v, 0, 2, 1, 3); + float scale = 1.0f / std::sqrt((float)head_dim); + ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, scale, 0.0f, 0.0f); + attn = ggml_permute(ctx, attn, 0, 2, 1, 3); + attn = ggml_cont(ctx, attn); + attn = ggml_reshape_2d(ctx, attn, (int64_t)d_model, (int64_t)Tp); + ggml_tensor* op_w = pk::clone_weight(ctx, ml, (base + "attn.out_proj.weight").c_str()); + attn = ggml_mul_mat(ctx, op_w, attn); + ggml_tensor* op_b = pk::clone_weight_opt(ctx, ml, (base + "attn.out_proj.bias").c_str()); + if (op_b) attn = ggml_add(ctx, attn, op_b); + return attn; + }, out); + assert(ok); + write_npy_f32(std::string(out_dir) + "/l0_attn_out_pk.npy", out.data(), + {(int64_t)Tp, (int64_t)d_model}); + printf("attn_out: [Tp=%d, d=%d] first5: ", Tp, d_model); + for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); + printf("\n"); + } + + return 0; +} diff --git a/tests/test_diarization.cpp b/tests/test_diarization.cpp new file mode 100644 index 0000000..7a3c51a --- /dev/null +++ b/tests/test_diarization.cpp @@ -0,0 +1,86 @@ +#include "diarization.hpp" +#include "model_loader.hpp" +#include +#include +#include + +int main() { + const char* path = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + if (!path) { + std::fprintf(stderr, + "PARAKEET_TEST_DIAR_GGUF not set; skipping diarization test\n"); + return 77; + } + + // Load via DiarizationModel::load (exercises loader, mel, encoder, head). + std::unique_ptr m = pk::DiarizationModel::load(path); + if (!m) { + std::fprintf(stderr, "DiarizationModel::load failed for %s\n", path); + return 1; + } + + // Config sanity: arch must be "diarization" and diarization.present true. + pk::ModelLoader ml; + if (!ml.load(path)) { + std::fprintf(stderr, "ModelLoader::load failed\n"); + return 1; + } + const pk::ParakeetConfig& c = ml.config(); + if (c.arch != "diarization") { + std::fprintf(stderr, "arch != diarization (got %s)\n", c.arch.c_str()); + return 1; + } + if (!c.diarization.present) { + std::fprintf(stderr, "diarization.present is false\n"); + return 1; + } + if (c.diarization.n_speakers == 0) { + std::fprintf(stderr, "n_speakers == 0\n"); + return 1; + } + std::printf("diarization config OK: arch=%s n_spk=%u tf_d_model=%u " + "upsample=%u frame_sec=%.4f onset=%.2f offset=%.2f\n", + c.arch.c_str(), c.diarization.n_speakers, + c.diarization.tf_d_model, c.diarization.upsample_factor, + c.diarization.frame_resolution_sec, + c.diarization.onset_threshold, + c.diarization.offset_threshold); + + // Verify key sortformer tensors are present. + const char* required[] = { + "sortformer_modules.encoder_proj.weight", + "sortformer_modules.subpixel_upsample.weight", + "sortformer_modules.first_hidden_to_hidden.weight", + "sortformer_modules.single_hidden_to_spks.weight", + nullptr, + }; + for (size_t i = 0; required[i]; ++i) { + if (ml.tensor(required[i]) == nullptr) { + std::fprintf(stderr, "missing tensor: %s\n", required[i]); + return 1; + } + } + std::printf("all required sortformer tensors present\n"); + + // If a test audio file is provided, run end-to-end diarization. + const char* wav = std::getenv("PARAKEET_TEST_DIAR_WAV"); + if (wav) { + try { + pk::DiarizationResult r = m->diarize_path(wav); + std::printf("diarized %s -> %zu segments, %d speakers\n", + wav, r.segments.size(), r.n_speakers); + for (size_t i = 0; i < r.segments.size() && i < 20; ++i) { + std::printf(" spk %d: %.2f - %.2f\n", + r.segments[i].speaker, + r.segments[i].start, r.segments[i].end); + } + } catch (const std::exception& e) { + std::fprintf(stderr, "diarize_path threw: %s\n", e.what()); + return 1; + } + } else { + std::printf("PARAKEET_TEST_DIAR_WAV not set; skipping inference test\n"); + } + + return 0; +} diff --git a/tests/test_diarization_parity.cpp b/tests/test_diarization_parity.cpp new file mode 100644 index 0000000..828810c --- /dev/null +++ b/tests/test_diarization_parity.cpp @@ -0,0 +1,133 @@ +// test_diarization_parity.cpp — dump intermediate outputs for parity comparison. +// +// Env: PARAKEET_TEST_DIAR_GGUF (required), PARAKEET_TEST_DIAR_WAV (required), +// PARAKEET_TEST_DIAR_OUT (required, output directory). +#include "diarization.hpp" +#include "mel.hpp" +#include "model_loader.hpp" +#include "backend.hpp" +#include "audio_io.hpp" +#include "diarization_encoder.hpp" +#include "diarization_head.hpp" + +#include +#include +#include +#include +#include + +static void write_npy_f32(const std::string& path, const float* data, + const std::vector& shape) { + // NumPy v1 format: magic(6) + version(2) + header_len(2) + header + // Header = dict_string + padding_spaces + \n, total padded to multiple of 64. + std::string magic = "\x93NUMPY"; + uint8_t version[2] = {1, 0}; + std::string dict = "{'descr': ' 0) dict += ", "; + dict += std::to_string(shape[i]); + } + if (shape.size() == 1) dict += ","; + dict += "), }"; + // header = dict + padding + \n, total must be multiple of 64 + // total_file = 6 (magic) + 2 (version) + 2 (hlen) + header_len + // We want header_len such that 10 + header_len is multiple of 64. + // header_len = dict.size() + n_pad + 1(\n) + int overhead = 10; // magic + version + hlen + int target = overhead + dict.size() + 1; // +1 for \n + int padded = ((target + 63) / 64) * 64; + int n_pad = padded - target; + uint16_t hlen = (uint16_t)(dict.size() + n_pad + 1); + + std::ofstream f(path, std::ios::binary); + f.write(magic.data(), 6); + f.write((char*)version, 2); + f.write((char*)&hlen, 2); + f.write(dict.data(), (std::streamsize)dict.size()); + for (int i = 0; i < n_pad; ++i) f.write(" ", 1); + f.write("\n", 1); + size_t n = 1; + for (auto s : shape) n *= s; + f.write((const char*)data, (std::streamsize)(n * sizeof(float))); +} + +int main() { + const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + const char* wav_path = std::getenv("PARAKEET_TEST_DIAR_WAV"); + const char* out_dir = std::getenv("PARAKEET_TEST_DIAR_OUT"); + if (!gguf || !wav_path || !out_dir) { + std::fprintf(stderr, "PARAKEET_TEST_DIAR_GGUF, PARAKEET_TEST_DIAR_WAV, " + "PARAKEET_TEST_DIAR_OUT all required\n"); + return 77; + } + + // Load model + std::unique_ptr m = pk::DiarizationModel::load(gguf); + if (!m) { + std::fprintf(stderr, "DiarizationModel::load failed\n"); + return 1; + } + + // Load audio + pk::Audio audio; + if (!pk::load_audio_16k_mono(wav_path, audio)) { + std::fprintf(stderr, "failed to load audio: %s\n", wav_path); + return 1; + } + std::printf("audio: %zu samples, %.2fs\n", audio.samples.size(), + (float)audio.samples.size() / 16000.0f); + + // Build components from the loader (same as DiarizationModel::run) + const pk::ModelLoader& ml = m->loader(); + pk::MelFrontend mel(ml); + pk::DiarizationEncoder encoder(ml); + pk::DiarizationHead head(ml); + + // 1. Mel + std::vector feats; + int n_mels = 0, T = 0; + mel.compute(audio.samples, feats, n_mels, T); + std::printf("mel: [%d, %d]\n", n_mels, T); + write_npy_f32(std::string(out_dir) + "/mel_pk.npy", feats.data(), + {(int64_t)n_mels, (int64_t)T}); + + // 2. Encoder + std::vector enc_out; + int d_model = 0, T_enc = 0; + encoder.forward(feats, n_mels, T, enc_out, d_model, T_enc); + std::printf("enc_out: [%d, %d]\n", d_model, T_enc); + write_npy_f32(std::string(out_dir) + "/enc_out_pk.npy", enc_out.data(), + {(int64_t)d_model, (int64_t)T_enc}); + + // 3. Head + std::vector probs; + int n_spk = 0, T_out = 0; + head.forward(enc_out, d_model, T_enc, probs, n_spk, T_out); + std::printf("probs: [%d, %d]\n", n_spk, T_out); + write_npy_f32(std::string(out_dir) + "/probs_pk.npy", probs.data(), + {(int64_t)n_spk, (int64_t)T_out}); + + // 4. Segments (reuse the model's diarize_path which does the full pipeline) + pk::DiarizationResult r = m->diarize_path(wav_path); + std::printf("segments: %zu\n", r.segments.size()); + + std::ofstream sf(std::string(out_dir) + "/segments_pk.json"); + sf << "[\n"; + for (size_t i = 0; i < r.segments.size(); ++i) { + sf << " {\"speaker\": " << r.segments[i].speaker + << ", \"start\": " << r.segments[i].start + << ", \"end\": " << r.segments[i].end << "}"; + if (i + 1 < r.segments.size()) sf << ","; + sf << "\n"; + } + sf << "]\n"; + + for (size_t i = 0; i < r.segments.size() && i < 20; ++i) { + std::printf(" spk %d: %.2f - %.2f\n", + r.segments[i].speaker, + r.segments[i].start, r.segments[i].end); + } + + std::printf("\nDone. Outputs in %s/\n", out_dir); + return 0; +} diff --git a/tests/test_sas_merge.cpp b/tests/test_sas_merge.cpp new file mode 100644 index 0000000..fa250b3 --- /dev/null +++ b/tests/test_sas_merge.cpp @@ -0,0 +1,227 @@ +// Unit test for the SAS merge layer (merge_asr_diarization + group_speaker_words). +// +// No model or audio needed — constructs synthetic Word and SpeakerSegment vectors +// and verifies the merge + grouping logic directly. + +#include "sas_merge.hpp" +#include "transcription.hpp" +#include "diarization.hpp" + +#include +#include + +using namespace pk; + +static int failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::fprintf(stderr, "FAIL: %s (line %d)\n", #cond, __LINE__); \ + ++failures; \ + } \ + } while (0) + +// ── Test 1: basic overlap assignment ────────────────────────────────────── +// Two speakers, non-overlapping segments. Words fall clearly within each +// speaker's segment. +static void test_basic_assignment() { + std::vector words = { + {"hello", 0.10f, 0.30f, 0.95f}, + {"world", 0.35f, 0.55f, 0.90f}, + {"foo", 1.10f, 1.30f, 0.80f}, + {"bar", 1.35f, 1.55f, 0.85f}, + }; + std::vector segs = { + {0, 0.00f, 1.00f}, // speaker 0: 0–1s + {1, 1.00f, 2.00f}, // speaker 1: 1–2s + }; + + auto swords = merge_asr_diarization(words, segs); + CHECK(swords.size() == 4); + CHECK(swords[0].speaker == 0); + CHECK(swords[1].speaker == 0); + CHECK(swords[2].speaker == 1); + CHECK(swords[3].speaker == 1); + + // Verify text and timestamps are preserved + CHECK(swords[0].text == "hello"); + CHECK(swords[0].start == 0.10f); + CHECK(swords[0].end == 0.30f); + CHECK(swords[0].conf == 0.95f); +} + +// ── Test 2: dominant speaker (overlap) ──────────────────────────────────── +// Two overlapping segments: the one with the larger overlap wins. +static void test_dominant_speaker() { + std::vector words = { + {"x", 0.50f, 0.80f, 0.9f}, // overlaps both, but more with spk 0 + }; + std::vector segs = { + {0, 0.00f, 0.70f}, // overlap = 0.20s + {1, 0.60f, 1.00f}, // overlap = 0.20s — equal! first one (sorted) wins + }; + + auto swords = merge_asr_diarization(words, segs); + CHECK(swords.size() == 1); + // Equal overlap → the first segment (sorted by start, then speaker) wins + // because we use strict > (not >=). + CHECK(swords[0].speaker == 0); + + // Now make speaker 1 overlap more + segs[1].start = 0.40f; // overlap with [0.5, 0.8] = 0.40s > 0.20s + swords = merge_asr_diarization(words, segs); + CHECK(swords[0].speaker == 1); +} + +// ── Test 3: no overlapping segment ──────────────────────────────────────── +// Word falls outside all segments → speaker = -1. +static void test_no_speaker() { + std::vector words = { + {"silence", 5.00f, 5.50f, 0.5f}, + }; + std::vector segs = { + {0, 0.00f, 1.00f}, + {1, 2.00f, 3.00f}, + }; + + auto swords = merge_asr_diarization(words, segs); + CHECK(swords.size() == 1); + CHECK(swords[0].speaker == -1); +} + +// ── Test 4: utterance grouping — same speaker, small gap ────────────────── +static void test_grouping_same_speaker() { + std::vector swords = { + {0, "hello", 0.10f, 0.30f, 0.95f}, + {0, "world", 0.40f, 0.60f, 0.90f}, // gap = 0.10s ≤ 0.5s + }; + + auto utts = group_speaker_words(swords); + CHECK(utts.size() == 1); + CHECK(utts[0].speaker == 0); + CHECK(utts[0].text == "hello world"); + CHECK(utts[0].start == 0.10f); + CHECK(utts[0].end == 0.60f); + CHECK(utts[0].conf == 0.90f); // min(0.95, 0.90) +} + +// ── Test 5: utterance grouping — speaker change ─────────────────────────── +static void test_grouping_speaker_change() { + std::vector swords = { + {0, "hello", 0.10f, 0.30f, 0.95f}, + {1, "world", 0.40f, 0.60f, 0.90f}, + }; + + auto utts = group_speaker_words(swords); + CHECK(utts.size() == 2); + CHECK(utts[0].speaker == 0); + CHECK(utts[0].text == "hello"); + CHECK(utts[1].speaker == 1); + CHECK(utts[1].text == "world"); +} + +// ── Test 6: utterance grouping — large gap splits ───────────────────────── +static void test_grouping_large_gap() { + std::vector swords = { + {0, "hello", 0.10f, 0.30f, 0.95f}, + {0, "world", 1.00f, 1.20f, 0.90f}, // gap = 0.70s > 0.5s + }; + + auto utts = group_speaker_words(swords); + CHECK(utts.size() == 2); + CHECK(utts[0].text == "hello"); + CHECK(utts[1].text == "world"); +} + +// ── Test 7: utterance grouping — unknown speaker (-1) ───────────────────── +static void test_grouping_unknown_speaker() { + std::vector swords = { + {0, "a", 0.10f, 0.20f, 0.9f}, + {-1, "b", 0.30f, 0.40f, 0.8f}, + {-1, "c", 0.45f, 0.55f, 0.7f}, + {1, "d", 0.60f, 0.70f, 0.85f}, + }; + + auto utts = group_speaker_words(swords); + CHECK(utts.size() == 3); + CHECK(utts[0].speaker == 0); + CHECK(utts[0].text == "a"); + CHECK(utts[1].speaker == -1); + CHECK(utts[1].text == "b c"); + CHECK(utts[2].speaker == 1); + CHECK(utts[2].text == "d"); +} + +// ── Test 8: empty inputs ────────────────────────────────────────────────── +static void test_empty() { + std::vector words; + std::vector segs; + + auto swords = merge_asr_diarization(words, segs); + CHECK(swords.empty()); + + auto utts = group_speaker_words(swords); + CHECK(utts.empty()); +} + +// ── Test 9: word exactly at segment boundary ─────────────────────────────── +static void test_boundary() { + std::vector words = { + {"x", 1.00f, 1.10f, 0.9f}, // starts exactly at seg0 end / seg1 start + }; + std::vector segs = { + {0, 0.00f, 1.00f}, // seg.end <= w.start → skipped (<=) + {1, 1.00f, 2.00f}, // overlap = 0.10s + }; + + auto swords = merge_asr_diarization(words, segs); + CHECK(swords.size() == 1); + CHECK(swords[0].speaker == 1); +} + +// ── Test 10: multiple speakers on same segment ──────────────────────────── +// Same segment list, multiple words — all should get the same speaker. +static void test_multiple_words_same_speaker() { + std::vector words = { + {"a", 0.10f, 0.20f, 0.9f}, + {"b", 0.25f, 0.35f, 0.8f}, + {"c", 0.40f, 0.50f, 0.7f}, + }; + std::vector segs = { + {2, 0.00f, 1.00f}, + }; + + auto swords = merge_asr_diarization(words, segs); + CHECK(swords.size() == 3); + for (int i = 0; i < 3; ++i) { + CHECK(swords[i].speaker == 2); + } + + // Group should merge all into one utterance + auto utts = group_speaker_words(swords); + CHECK(utts.size() == 1); + CHECK(utts[0].text == "a b c"); + CHECK(utts[0].speaker == 2); + CHECK(utts[0].conf == 0.7f); // min +} + +int main() { + test_basic_assignment(); + test_dominant_speaker(); + test_no_speaker(); + test_grouping_same_speaker(); + test_grouping_speaker_change(); + test_grouping_large_gap(); + test_grouping_unknown_speaker(); + test_empty(); + test_boundary(); + test_multiple_words_same_speaker(); + + if (failures == 0) { + std::printf("All SAS merge tests passed.\n"); + return 0; + } + std::printf("%d assertion(s) failed.\n", failures); + return 1; +} diff --git a/tests/test_subpixel_check.cpp b/tests/test_subpixel_check.cpp new file mode 100644 index 0000000..5c4bc40 --- /dev/null +++ b/tests/test_subpixel_check.cpp @@ -0,0 +1,88 @@ +// Test: subpixel reshape WITHOUT 3D permute +// Uses reshape_3d → reshape_2d → 2D transpose+cont (avoids 3D cont bug) +#include +#include +#include +#include "backend.hpp" +#include "ggml_graph.hpp" +#include "ggml.h" + +int main() { + // Small test: T_enc=4, up=2, tf=3 + // conv_out has ne=[T_enc, tf*up] = [4, 6], data: flat[t + c*4] + // c = h*up + u + // + // Reference subpixel: + // x.view(B, C//up, up, T) → x.view(B, C//up, up*T) + // up_pk[h, t'] = conv[h*up+u, t] where t' = u*T + t + // + // In ggml (column-major): + // 1. reshape_3d(T_enc, up, tf): ne=[T_enc, up, tf], element(t,u,h) = flat[t + u*T + h*up*T] + // = flat[t + (h*up+u)*T] = flat[t + c*T] ✓ + // 2. reshape_2d(T_out, tf): ne=[T_out, tf], element(t',h) = flat[t' + h*T_out] + // t' = t + u*T → flat[t + u*T + h*up*T] = flat[t + c*T] ✓ + // 3. transpose: ne=[tf, T_out], element(h,t') = flat[t' + h*T_out] (view, strides change) + // 4. cont: copies to flat[h + t'*tf] (2D cont, which WORKS) + + const int T_enc = 4, up = 2, tf = 3; + const int T_out = T_enc * up; + + float conv_data[24]; + for (int c = 0; c < tf * up; c++) + for (int t = 0; t < T_enc; t++) + conv_data[t + c * T_enc] = (c + 1) * 10 + t; + + float bias_data[6] = {0}; + + std::vector out; + bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t ne[2] = {T_enc, tf * up}; + ggml_tensor* conv_out = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, ne, + conv_data, 24 * sizeof(float)); + + int64_t bne[2] = {1, tf * up}; + ggml_tensor* bias = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, bne, + bias_data, 6 * sizeof(float)); + conv_out = ggml_add(ctx, conv_out, bias); + + // Subpixel: reshape_3d → reshape_2d → transpose → cont + ggml_tensor* upsampled = ggml_reshape_3d(ctx, conv_out, T_enc, up, tf); + upsampled = ggml_reshape_2d(ctx, upsampled, T_enc * up, tf); // ne=[T_out, tf] + upsampled = ggml_cont(ctx, ggml_transpose(ctx, upsampled)); // ne=[tf, T_out] + return upsampled; + }, out); + + if (!ok) { printf("FAIL: run_graph returned false\n"); return 1; } + + // out has ne=[tf, T_out], data: flat[h + t'*tf] + printf("Expected:\n"); + for (int h = 0; h < tf; h++) { + printf(" h%d: ", h); + for (int tp = 0; tp < T_out; tp++) { + int u = tp / T_enc; + int t = tp % T_enc; + int c = h * up + u; + printf("%.0f ", conv_data[t + c * T_enc]); + } + printf("\n"); + } + printf("Got:\n"); + for (int h = 0; h < tf; h++) { + printf(" h%d: ", h); + for (int tp = 0; tp < T_out; tp++) { + printf("%.0f ", out[h + tp * tf]); + } + printf("\n"); + } + + bool pass = true; + for (int h = 0; h < tf; h++) + for (int tp = 0; tp < T_out; tp++) { + int u = tp / T_enc; + int t = tp % T_enc; + int c = h * up + u; + if (out[h + tp * tf] != conv_data[t + c * T_enc]) { pass = false; } + } + printf("Result: %s\n", pass ? "PASS" : "FAIL"); + return pass ? 0 : 1; +} From 394d270fabb1d6125f05c772aa3ca078a574b19d Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 26 Sep 2026 19:54:39 +0000 Subject: [PATCH 07/17] feat(diarization): add streaming diarization and streaming SAS (ABI v8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the DiarizationEncoder into pre_encode() and transformer_forward() so the streaming path can run the transformer over [spkcache | chunk] without re-doing the mel front-end. Add forward_range() to DiarizationHead to compute probs for a sub-range of encoder frames (the chunk portion only). StreamingDiarization implements the AOSC mechanism from NeMo's Sortformer: chunked processing with a speaker cache, FIFO overflow, and per-speaker top-K compression of the spkcache. When fifo_len=0 (Nemotron-3 default) every chunk immediately overflows into the spkcache, which is then compressed back to spkcache_len frames. C-API ABI v8 adds: - parakeet_capi_diarize_stream_begin / _feed / _free (streaming diarization) - parakeet_capi_sas_stream_begin / _feed / _free (streaming SAS) Streaming SAS (Phase 3.4) buffers incoming PCM, feeds complete chunks to both ASR and diarization, and merges the results with the same SAS merge logic as the offline path. ModelLoader reads streaming config from GGUF KVs (parakeet.diar.chunk_len, spkcache_len, fifo_len, etc.). When absent, Nemotron-3-Diarization defaults are used so the streaming C-API works without converter changes. test_streaming_diarization verifies the streaming path produces segments in chronological order within the audio duration, and detects speakers. On the 2-minute test audio: 115 streaming segments vs 583 offline (the gap is expected — streaming uses chunk-local context, not full audio). --- CMakeLists.txt | 3 +- include/parakeet_capi.h | 78 ++++ src/diarization.hpp | 3 + src/diarization_encoder.cpp | 185 ++++++++++ src/diarization_encoder.hpp | 22 ++ src/diarization_head.cpp | 17 + src/diarization_head.hpp | 7 + src/diarization_streaming.cpp | 515 +++++++++++++++++++++++++++ src/diarization_streaming.hpp | 162 +++++++++ src/model_loader.cpp | 33 ++ src/model_loader.hpp | 14 + src/parakeet_capi.cpp | 203 ++++++++++- tests/CMakeLists.txt | 5 +- tests/test_streaming_diarization.cpp | 130 +++++++ 14 files changed, 1373 insertions(+), 4 deletions(-) create mode 100644 src/diarization_streaming.cpp create mode 100644 src/diarization_streaming.hpp create mode 100644 tests/test_streaming_diarization.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6438d9a..82e6374 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,7 +96,8 @@ set(PARAKEET_SRC src/diarization.cpp src/diarization_encoder.cpp src/diarization_head.cpp - src/sas_merge.cpp) + src/sas_merge.cpp + src/diarization_streaming.cpp) if(PARAKEET_SHARED) add_library(parakeet SHARED ${PARAKEET_SRC}) diff --git a/include/parakeet_capi.h b/include/parakeet_capi.h index 5c1ac20..e422d0f 100644 --- a/include/parakeet_capi.h +++ b/include/parakeet_capi.h @@ -59,6 +59,10 @@ typedef struct parakeet_ctx parakeet_ctx; // parakeet_capi_transcribe_and_diarize_json). Takes two contexts // (an ASR ctx + a diarization ctx), runs both models on the same // audio, and merges word timestamps with speaker segments. +// v8: added streaming diarization entry points +// (parakeet_capi_diarize_stream_begin / _feed / _free) and streaming +// speaker-attributed ASR (parakeet_capi_sas_stream_begin / _feed / _free). +// Streaming diarization uses the AOSC mechanism (spkcache + FIFO). int parakeet_capi_abi_version(void); // Load a GGUF model. Returns an owning context, or NULL on failure. @@ -417,6 +421,80 @@ char* parakeet_capi_transcribe_and_diarize_json( // it (or until parakeet_capi_free). Returns "" if `ctx` is NULL. const char* parakeet_capi_last_error(parakeet_ctx* ctx); +// --------------------------------------------------------------------------- +// v8: Streaming diarization (AOSC + FIFO) +// +// Streaming diarization processes audio in chunks. The caller: +// 1. parakeet_capi_diarize_stream_begin(ctx) → parakeet_diar_stream* +// 2. parakeet_capi_diarize_stream_feed(stream, mel, n_mels, n_frames, is_last) +// → parakeet_diar_segment* (segments for this chunk) +// 3. Repeat step 2 for each chunk +// 4. parakeet_capi_diarize_stream_free(stream) +// +// The mel features must be pre-computed by the caller (128-dim, 16kHz). +// Each chunk should be exactly `chunk_len` mel frames (available from +// parakeet_capi_diar_stream_chunk_len()). The final chunk may be shorter. + +typedef struct parakeet_diar_segment { + int speaker; // 0-indexed speaker ID + float start; // wall-clock seconds from stream start + float end; +} parakeet_diar_segment; + +typedef struct parakeet_diar_stream parakeet_diar_stream; + +// Begin a streaming diarization session. Returns NULL on error. +parakeet_diar_stream* parakeet_capi_diarize_stream_begin(parakeet_ctx* diar_ctx); + +// Feed one chunk of mel features. Returns segments for this chunk +// (caller must free the returned array with parakeet_capi_free_diar_segments). +// `mel` is row-major [n_mels, n_frames]: mel[m*n_frames + t]. +// `is_last` marks the final chunk (no spkcache update after it). +// `out_count` receives the number of returned segments. +// Returns NULL if no segments were produced (out_count = 0). +parakeet_diar_segment* parakeet_capi_diarize_stream_feed( + parakeet_diar_stream* stream, + const float* mel, int n_mels, int n_frames, + int is_last, int* out_count); + +// Get the expected chunk length in mel frames. +int parakeet_capi_diar_stream_chunk_len(parakeet_diar_stream* stream); + +// Get the expected number of mel features. +int parakeet_capi_diar_stream_n_mels(parakeet_diar_stream* stream); + +// Free segments returned by parakeet_capi_diarize_stream_feed. +void parakeet_capi_free_diar_segments(parakeet_diar_segment* segs); + +// Free a streaming diarization session. +void parakeet_capi_diarize_stream_free(parakeet_diar_stream* stream); + +// --------------------------------------------------------------------------- +// v8: Streaming speaker-attributed ASR (Phase 3.4) +// +// Combines streaming diarization with chunked ASR. The caller feeds audio +// chunks; internally, both ASR and diarization process the audio and the +// results are merged using the same SAS merge logic as the offline path. + +typedef struct parakeet_sas_stream parakeet_sas_stream; + +// Begin a streaming SAS session. Returns NULL on error. +parakeet_sas_stream* parakeet_capi_sas_stream_begin( + parakeet_ctx* asr_ctx, parakeet_ctx* diar_ctx); + +// Feed one chunk of PCM samples (mono float, 16 kHz). +// Returns speaker-attributed utterances for this chunk. +// Caller must free each result's .text with parakeet_capi_free_string, +// then the array with parakeet_capi_free_sas_results. +// Returns NULL if nothing was produced (out_count = 0). +parakeet_sas_result* parakeet_capi_sas_stream_feed( + parakeet_sas_stream* stream, + const float* pcm, int n_samples, + int is_last, int* out_count); + +// Free a streaming SAS session. +void parakeet_capi_sas_stream_free(parakeet_sas_stream* stream); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/diarization.hpp b/src/diarization.hpp index 44bc6a3..a60387c 100644 --- a/src/diarization.hpp +++ b/src/diarization.hpp @@ -47,6 +47,9 @@ class DiarizationModel { const ParakeetConfig& config() const { return loader_.config(); } const ModelLoader& loader() const { return loader_; } + // Access the mel frontend (for streaming diarization to compute mel features). + const MelFrontend& mel() const { return *mel_; } + private: DiarizationModel() = default; diff --git a/src/diarization_encoder.cpp b/src/diarization_encoder.cpp index 6a03ef4..c91be3c 100644 --- a/src/diarization_encoder.cpp +++ b/src/diarization_encoder.cpp @@ -219,4 +219,189 @@ void DiarizationEncoder::forward(const std::vector& mel, int n_mels, int T_enc = Tp; } +// ============================================================================ +// Streaming split: pre_encode + transformer_forward +// ============================================================================ + +void DiarizationEncoder::pre_encode(const std::vector& mel, int n_mels, int T, + std::vector& emb, int& d_model, int& T_enc) const { + assert(n_mels == n_mels_); + assert((int)mel.size() == n_mels * T); + + const int factor = subsampling_factor_; + const int pad = (factor - (T % factor)) % factor; + const int T_padded = T + pad; + const int Tp = T_padded / factor; + + const ModelLoader& ml = ml_; + const int d = d_model_; + const float ln_eps = ln_eps_; + + pk::ensure_weights_realized(ml); + GraphInputPool pool; + + bool ok = pk::run_graph(0, 0, + [&](ggml_context* ctx) -> ggml_tensor* { + // --- 1. FeatureStacking --- + int64_t mel_ne[2] = {T_padded, n_mels}; + std::vector& mel_padded = pool.alloc_f32((size_t)n_mels * T_padded); + for (int m = 0; m < n_mels; ++m) + for (int t = 0; t < T; ++t) + mel_padded[(size_t)m * T_padded + t] = mel[(size_t)m * T + t]; + + ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, + mel_padded.data(), (size_t)n_mels * T_padded * sizeof(float)); + mel_t = ggml_cont(ctx, mel_t); + mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); + ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels * factor, Tp); + + // Linear(1024 → 512, no bias) + ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); + ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); + + // --- 2. embed_norm --- + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); + ggml_tensor* y = ggml_norm(ctx, x, ln_eps); + x = ggml_add(ctx, ggml_mul(ctx, y, g), b); + + // Transpose to channels-first: [d_model, T_enc] + // pre_encode returns channels-first: emb[c*T_enc + t] + x = ggml_cont(ctx, ggml_transpose(ctx, x)); + return x; + }, emb); + + assert(ok && "diarization pre_encode graph failed"); + (void)ok; + + d_model = d_model_; + T_enc = Tp; +} + +void DiarizationEncoder::transformer_forward(const std::vector& emb, int d_model, int T_enc, + std::vector& enc_out) const { + assert(d_model == d_model_); + assert((int)emb.size() == d_model * T_enc); + + const ModelLoader& ml = ml_; + const int d = d_model_; + const int H = n_heads_; + const int hd = head_dim_; + const int nls = n_layers_; + const float ln_eps = ln_eps_; + const float rope_base = rope_base_; + const int n_rot = (int)(hd * rotary_fraction_); + + std::vector positions(T_enc); + for (int i = 0; i < T_enc; ++i) positions[i] = i; + + pk::ensure_weights_realized(ml); + GraphInputPool pool; + + bool ok = pk::run_graph(0, 0, + [&](ggml_context* ctx) -> ggml_tensor* { + // Input: emb is channels-first [d_model, T_enc] → emb[c*T_enc + t] + // ggml column-major: ne[0]=T_enc (fastest), ne[1]=d_model + // → flat[t + c*T_enc] = emb[c*T_enc + t] ✓ + int64_t emb_ne[2] = {T_enc, d}; + ggml_tensor* x = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, emb_ne, + const_cast(emb.data()), (size_t)d * T_enc * sizeof(float)); + // Transpose to time-major: ne[0]=d_model, ne[1]=T_enc + // (same layout as forward() uses after pre-encode) + x = ggml_cont(ctx, ggml_transpose(ctx, x)); + // x: ne[0]=d, ne[1]=T_enc — time-major + + // Position tensor for RoPE + int64_t pos_ne[1] = {(int64_t)T_enc}; + ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, + positions.data(), (size_t)T_enc * sizeof(int32_t)); + + // N × TransformerBlock (pre-norm) + for (int i = 0; i < nls; ++i) { + std::string base = "encoder.layers." + std::to_string(i) + "."; + + // norm1 + { + ggml_tensor* g = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); + ggml_tensor* b = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_add(ctx, ggml_mul(ctx, h, g), b); + + // Fused QKV (no bias) + ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); + ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); + qkv = ggml_cont(ctx, qkv); + qkv = ggml_reshape_3d(ctx, qkv, d, 3, T_enc); + + size_t ts = (size_t)3 * d * sizeof(float); + ggml_tensor* q = ggml_view_2d(ctx, qkv, d, T_enc, ts, 0); + ggml_tensor* k = ggml_view_2d(ctx, qkv, d, T_enc, ts, (size_t)d * sizeof(float)); + ggml_tensor* v = ggml_view_2d(ctx, qkv, d, T_enc, ts, (size_t)2 * d * sizeof(float)); + + q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), hd, H, T_enc); + k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), hd, H, T_enc); + v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), hd, H, T_enc); + + q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, + GGML_ROPE_TYPE_NEOX, 0, rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, + GGML_ROPE_TYPE_NEOX, 0, rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + q = ggml_permute(ctx, q, 0, 2, 1, 3); + k = ggml_permute(ctx, k, 0, 2, 1, 3); + v = ggml_permute(ctx, v, 0, 2, 1, 3); + + float scale = 1.0f / std::sqrt((float)hd); + ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, + scale, 0.0f, 0.0f); + attn = ggml_cont(ctx, attn); + attn = ggml_reshape_2d(ctx, attn, (int64_t)d, (int64_t)T_enc); + + ggml_tensor* op_w = pk::clone_weight(ctx, ml, (base + "attn.out_proj.weight").c_str()); + attn = ggml_mul_mat(ctx, op_w, attn); + ggml_tensor* op_b = pk::clone_weight_opt(ctx, ml, (base + "attn.out_proj.bias").c_str()); + if (op_b) attn = ggml_add(ctx, attn, op_b); + + x = ggml_add(ctx, x, attn); + } + + // norm2 + FFN + { + ggml_tensor* g = pk::clone_weight(ctx, ml, (base + "norm2.weight").c_str()); + ggml_tensor* b = pk::clone_weight(ctx, ml, (base + "norm2.bias").c_str()); + ggml_tensor* h = ggml_norm(ctx, x, ln_eps); + h = ggml_add(ctx, ggml_mul(ctx, h, g), b); + + ggml_tensor* f0_w = pk::clone_weight(ctx, ml, (base + "ffn.net.0.weight").c_str()); + h = ggml_mul_mat(ctx, f0_w, h); + ggml_tensor* f0_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.0.bias").c_str()); + if (f0_b) h = ggml_add(ctx, h, f0_b); + h = ggml_gelu(ctx, h); + + ggml_tensor* f3_w = pk::clone_weight(ctx, ml, (base + "ffn.net.3.weight").c_str()); + h = ggml_mul_mat(ctx, f3_w, h); + ggml_tensor* f3_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.3.bias").c_str()); + if (f3_b) h = ggml_add(ctx, h, f3_b); + + x = ggml_add(ctx, x, h); + } + } + + // final_norm + { + ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.final_norm.weight"); + ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.final_norm.bias"); + ggml_tensor* y = ggml_norm(ctx, x, ln_eps); + x = ggml_add(ctx, ggml_mul(ctx, y, g), b); + } + + // Transpose to channels-first + x = ggml_cont(ctx, ggml_transpose(ctx, x)); + return x; + }, enc_out); + + assert(ok && "diarization transformer_forward graph failed"); + (void)ok; +} + } // namespace pk diff --git a/src/diarization_encoder.hpp b/src/diarization_encoder.hpp index 1a927e4..6fc1e49 100644 --- a/src/diarization_encoder.hpp +++ b/src/diarization_encoder.hpp @@ -30,6 +30,28 @@ class DiarizationEncoder { void forward(const std::vector& mel, int n_mels, int T, std::vector& enc_out, int& d_model, int& T_enc) const; + // --- Streaming split: pre_encode + transformer_forward --- + + // Pre-encoder: FeatureStacking + Linear(1024→512) + embed_norm. + // mel: row-major [n_mels, T] — mel[m*T + t] + // emb: row-major [d_model, T_enc] — emb[c*T_enc + t] (channels-first) + // T_enc = T_padded / subsampling_factor + void pre_encode(const std::vector& mel, int n_mels, int T, + std::vector& emb, int& d_model, int& T_enc) const; + + // Transformer blocks + final_norm (the second half of the encoder). + // emb: row-major [d_model, T_enc] — emb[c*T_enc + t] (channels-first) + // enc_out: row-major [d_model, T_enc] — enc_out[c*T_enc + t] + void transformer_forward(const std::vector& emb, int d_model, int T_enc, + std::vector& enc_out) const; + + int subsampling() const { return subsampling_factor_; } + int n_mels() const { return n_mels_; } + int d_model() const { return d_model_; } + int n_layers() const { return n_layers_; } + int n_heads() const { return n_heads_; } + int head_dim() const { return head_dim_; } + private: const ModelLoader& ml_; int d_model_; // encoder d_model (512) diff --git a/src/diarization_head.cpp b/src/diarization_head.cpp index d5c435d..32a2f0a 100644 --- a/src/diarization_head.cpp +++ b/src/diarization_head.cpp @@ -165,4 +165,21 @@ void DiarizationHead::forward(const std::vector& enc_out, int d_model, in (void)ok; } +void DiarizationHead::forward_range(const std::vector& enc_out, int d_model, int T_enc_total, + int start_enc, int count_enc, + std::vector& probs, int& n_spk, int& T_out) const { + assert(d_model == d_model_); + assert(start_enc >= 0 && count_enc > 0 && start_enc + count_enc <= T_enc_total); + + // Extract the sub-range [start_enc, start_enc+count_enc) from enc_out. + // enc_out is channels-first: enc_out[c*T_enc_total + t]. + std::vector sub((size_t)d_model * count_enc); + for (int c = 0; c < d_model; ++c) + for (int t = 0; t < count_enc; ++t) + sub[(size_t)c * count_enc + t] = + enc_out[(size_t)c * T_enc_total + (start_enc + t)]; + + forward(sub, d_model, count_enc, probs, n_spk, T_out); +} + } // namespace pk diff --git a/src/diarization_head.hpp b/src/diarization_head.hpp index 29068a3..cf82d7d 100644 --- a/src/diarization_head.hpp +++ b/src/diarization_head.hpp @@ -30,6 +30,13 @@ class DiarizationHead { void forward(const std::vector& enc_out, int d_model, int T_enc, std::vector& probs, int& n_spk, int& T_out) const; + // Compute the per-frame probabilities for a sub-range of the encoder output. + // Same as forward() but operates on enc_out[start_enc .. start_enc+count_enc-1]. + // Used by the streaming path to get probs for just the chunk portion. + void forward_range(const std::vector& enc_out, int d_model, int T_enc_total, + int start_enc, int count_enc, + std::vector& probs, int& n_spk, int& T_out) const; + private: const ModelLoader& ml_; int d_model_; // encoder d_model (512) diff --git a/src/diarization_streaming.cpp b/src/diarization_streaming.cpp new file mode 100644 index 0000000..e392c4c --- /dev/null +++ b/src/diarization_streaming.cpp @@ -0,0 +1,515 @@ +#include "diarization_streaming.hpp" +#include "backend.hpp" +#include "ggml_graph.hpp" +#include "ggml.h" + +#include +#include +#include +#include +#include + +namespace pk { + +StreamingDiarization::StreamingDiarization(const ModelLoader& ml) + : ml_(ml) + , encoder_(ml) + , head_(ml) +{ + const auto& cfg = ml.config(); + const auto& d = cfg.diarization; + + d_model_ = (int)cfg.d_model; + tf_d_model_ = (int)d.tf_d_model; + n_spk_ = (int)d.n_speakers; + n_layers_ = (int)cfg.n_layers; + n_heads_ = (int)cfg.n_heads; + head_dim_ = d_model_ / n_heads_; + ff_dim_ = (int)cfg.ff_dim; + subsampling_factor_ = encoder_.subsampling(); + upsample_factor_ = (int)d.upsample_factor; + n_mels_ = encoder_.n_mels(); + chunk_len_ = d.chunk_len; + spkcache_len_ = d.spkcache_len; + fifo_len_ = d.fifo_len; + frame_sec_ = d.frame_resolution_sec; + onset_threshold_ = d.onset_threshold; + offset_threshold_ = d.offset_threshold; + + // AOSC tuning + spkcache_sil_frames_per_spk_ = d.spkcache_sil_frames_per_spk; + sil_threshold_ = d.sil_threshold; + pred_score_threshold_ = d.pred_score_threshold; + scores_boost_latest_ = d.scores_boost_latest; + strong_boost_rate_ = d.strong_boost_rate; + weak_boost_rate_ = d.weak_boost_rate; + min_pos_scores_rate_ = d.min_pos_scores_rate; + max_index_ = 99999; + + // Load the learned silence embedding if present + if (d.use_learnable_sil_emb) { + const ggml_tensor* t = ml.tensor("sortformer_modules.learnable_sil_emb"); + if (t) { + has_silence_emb_ = true; + silence_emb_.resize(d_model_); + ensure_weights_realized(ml); + const float* data = (const float*)t->data; + std::memcpy(silence_emb_.data(), data, d_model_ * sizeof(float)); + } + } + + mean_sil_emb_.assign(d_model_, 0.0f); + n_sil_frames_ = 0; + total_mel_frames_ = 0; + spkcache_enc_len_ = 0; + spkcache_preds_valid_ = false; + fifo_enc_len_ = 0; +} + +StreamingDiarization::~StreamingDiarization() = default; + +void StreamingDiarization::reset() { + spkcache_embs_.clear(); + spkcache_preds_.clear(); + spkcache_enc_len_ = 0; + spkcache_preds_valid_ = false; + fifo_embs_.clear(); + fifo_preds_.clear(); + fifo_enc_len_ = 0; + mean_sil_emb_.assign(d_model_, 0.0f); + n_sil_frames_ = 0; + total_mel_frames_ = 0; +} + +std::vector StreamingDiarization::feed_mel_chunk( + const std::vector& mel_chunk, int n_mels, int n_frames, + bool is_last) { + + assert(n_mels == n_mels_); + assert((int)mel_chunk.size() == n_mels * n_frames); + + const int factor = subsampling_factor_; // 8 + // Pad the chunk to a multiple of subsampling_factor + const int pad = (factor - (n_frames % factor)) % factor; + const int T_padded = n_frames + pad; + const int chunk_enc_len = T_padded / factor; + + // --- 1. Pre-encode the chunk --- + std::vector chunk_emb; + int d_model = 0, T_enc_chunk = 0; + encoder_.pre_encode(mel_chunk, n_mels, n_frames, chunk_emb, d_model, T_enc_chunk); + assert(d_model == d_model_); + assert(T_enc_chunk == chunk_enc_len); + + // --- 2. Concatenate [spkcache | chunk] for the encoder input --- + // Build the combined mel input: [spkcache_mel | chunk_mel] + // But wait — we work at the EMBEDDING level for spkcache, not mel level. + // The spkcache stores pre-encoded embeddings. So we concatenate at the + // embedding level: [spkcache_embs | chunk_emb], then run transformer_forward. + + int spk_enc = spkcache_enc_len_; + int total_enc = spk_enc + chunk_enc_len; + + std::vector combined_emb((size_t)d_model_ * total_enc); + if (spk_enc > 0) { + std::memcpy(combined_emb.data(), spkcache_embs_.data(), + (size_t)d_model_ * spk_enc * sizeof(float)); + } + std::memcpy(combined_emb.data() + (size_t)d_model_ * spk_enc, + chunk_emb.data(), + (size_t)d_model_ * chunk_enc_len * sizeof(float)); + + // --- 3. Run the transformer over [spkcache | chunk] --- + std::vector enc_out; + encoder_.transformer_forward(combined_emb, d_model_, total_enc, enc_out); + + // --- 4. Run the diarization head on the CHUNK portion only --- + // enc_out is [d_model, total_enc] channels-first. + // The chunk portion starts at frame spk_enc. + std::vector probs; + int n_spk = 0, T_out = 0; + head_.forward_range(enc_out, d_model_, total_enc, spk_enc, chunk_enc_len, + probs, n_spk, T_out); + assert(n_spk == n_spk_); + + // --- 5. Also get probs for the spkcache portion (for scoring) --- + std::vector full_probs; + int full_n_spk = 0, full_T_out = 0; + if (spk_enc > 0) { + head_.forward_range(enc_out, d_model_, total_enc, 0, spk_enc, + full_probs, full_n_spk, full_T_out); + assert(full_n_spk == n_spk_); + } + + // --- 6. Post-process the chunk probs into segments --- + float time_offset = total_mel_frames_ * frame_sec_; + auto segments = postprocess_chunk(probs, n_spk_, T_out, time_offset); + + // --- 7. Update stream state (FIFO → spkcache → compress) --- + if (!is_last) { + // Build the full prediction output [n_spk, total_out] for the spkcache + // portion + chunk portion, used by stream_state_update. + int spk_out = 0; + if (spk_enc > 0 && full_T_out > 0) { + spk_out = full_T_out; + } + + // The chunk portion of the prediction for scoring during compression + stream_state_update(chunk_emb, chunk_enc_len, + probs, T_out, + full_probs, full_T_out); + } + + total_mel_frames_ += n_frames; + return segments; +} + +void StreamingDiarization::boost_topk_scores( + float* scores, int n_frames, int n_spk, + int k_per_spk, float scale_factor, float offset) const { + if (k_per_spk <= 0 || k_per_spk > n_frames) return; + float boost = -scale_factor * std::log(offset); + + for (int s = 0; s < n_spk; ++s) { + std::vector> sv(n_frames); + for (int t = 0; t < n_frames; ++t) { + sv[t] = {scores[(size_t)t * n_spk + s], t}; + } + std::nth_element(sv.begin(), sv.begin() + k_per_spk, sv.end(), + [](const std::pair& a, const std::pair& b) { + return a.first > b.first; + }); + for (int i = 0; i < k_per_spk; ++i) { + scores[(size_t)sv[i].second * n_spk + s] += boost; + } + } +} + +void StreamingDiarization::update_silence_profile( + const float* pop_embs, const float* pop_preds, + int pop_len) { + for (int t = 0; t < pop_len; ++t) { + float pred_sum = 0; + for (int s = 0; s < n_spk_; ++s) { + pred_sum += pop_preds[(size_t)t * n_spk_ + s]; + } + if (pred_sum < sil_threshold_) { + ++n_sil_frames_; + float w_old = (float)(n_sil_frames_ - 1) / (float)n_sil_frames_; + float w_new = 1.0f / (float)n_sil_frames_; + for (int d = 0; d < d_model_; ++d) { + mean_sil_emb_[d] = w_old * mean_sil_emb_[d] + + w_new * pop_embs[(size_t)t * d_model_ + d]; + } + } + } +} + +void StreamingDiarization::compress_spkcache() { + const int n_frames = spkcache_enc_len_; + // Target spkcache size in ENCODER frames + const int target_enc_len = spkcache_len_ / subsampling_factor_; + const int sil_per_spk = spkcache_sil_frames_per_spk_; + const int per_spk = target_enc_len / n_spk_ - sil_per_spk; + const int strong_k = (int)std::floor(per_spk * strong_boost_rate_); + const int weak_k = (int)std::floor(per_spk * weak_boost_rate_); + const int min_pos_k = (int)std::floor(per_spk * min_pos_scores_rate_); + + // 1. Compute log-based importance scores [n_frames, n_spk] + std::vector scores((size_t)n_frames * n_spk_); + for (int t = 0; t < n_frames; ++t) { + const float* p = &spkcache_preds_[(size_t)t * n_spk_]; + float log_1_sum = 0; + for (int s = 0; s < n_spk_; ++s) + log_1_sum += std::log(std::max(1.0f - p[s], pred_score_threshold_)); + for (int s = 0; s < n_spk_; ++s) { + float lp = std::log(std::max(p[s], pred_score_threshold_)); + float l1p = std::log(std::max(1.0f - p[s], pred_score_threshold_)); + scores[(size_t)t * n_spk_ + s] = lp - l1p + log_1_sum - std::log(0.5f); + } + } + + // 2. Disable non-speech scores (preds <= 0.5 → -inf) + for (int t = 0; t < n_frames; ++t) + for (int s = 0; s < n_spk_; ++s) + if (spkcache_preds_[(size_t)t * n_spk_ + s] <= 0.5f) + scores[(size_t)t * n_spk_ + s] = -INFINITY; + + // Disable non-positive scores if speaker has enough positive ones + for (int s = 0; s < n_spk_; ++s) { + int pos_cnt = 0; + for (int t = 0; t < n_frames; ++t) + if (scores[(size_t)t * n_spk_ + s] > 0) ++pos_cnt; + if (pos_cnt >= min_pos_k) { + for (int t = 0; t < n_frames; ++t) { + if (scores[(size_t)t * n_spk_ + s] <= 0 && + spkcache_preds_[(size_t)t * n_spk_ + s] > 0.5f) + scores[(size_t)t * n_spk_ + s] = -INFINITY; + } + } + } + + // 3. Boost latest frames (beyond target_enc_len) + if (scores_boost_latest_ > 0) { + for (int t = target_enc_len; t < n_frames; ++t) + for (int s = 0; s < n_spk_; ++s) { + float& sc = scores[(size_t)t * n_spk_ + s]; + if (sc != -INFINITY) sc += scores_boost_latest_; + } + } + + // 4. Strong boost: top-K per speaker (scale=2) + boost_topk_scores(scores.data(), n_frames, n_spk_, strong_k, 2.0f, 0.5f); + + // 5. Weak boost: top-K per speaker (scale=1) + int wk = std::min(weak_k, n_frames); + boost_topk_scores(scores.data(), n_frames, n_spk_, wk, 1.0f, 0.5f); + + // 6. Add silence placeholder frames at end (+inf for each speaker) + int n_sil_pad = sil_per_spk; + int n_total = n_frames + n_sil_pad; + scores.resize((size_t)n_total * n_spk_); + for (int t = n_frames; t < n_total; ++t) + for (int s = 0; s < n_spk_; ++s) + scores[(size_t)t * n_spk_ + s] = INFINITY; + + // 7. Flatten as (n_spk, n_total) and find top target_enc_len entries + int flat_len = n_spk_ * n_total; + std::vector> flat(flat_len); + for (int s = 0; s < n_spk_; ++s) + for (int t = 0; t < n_total; ++t) + flat[(size_t)s * n_total + t] = {scores[(size_t)t * n_spk_ + s], + s * n_total + t}; + + std::nth_element(flat.begin(), flat.begin() + target_enc_len, flat.end(), + [](const std::pair& a, const std::pair& b) { + return a.first > b.first; + }); + + // Replace -inf entries with max_index, keep valid entries + std::vector topk_indices(target_enc_len); + for (int i = 0; i < target_enc_len; ++i) { + if (flat[i].first == -INFINITY) { + topk_indices[i] = max_index_; + } else { + topk_indices[i] = flat[i].second; + } + } + + // Sort to preserve original frame order + std::sort(topk_indices.begin(), topk_indices.end()); + + // Convert to frame indices and determine disabled mask + int n_frames_no_sil = n_total - n_sil_pad; + std::vector is_disabled(target_enc_len, false); + for (int i = 0; i < target_enc_len; ++i) { + if (topk_indices[i] == max_index_) { + is_disabled[i] = true; + } + topk_indices[i] = topk_indices[i] % n_total; + if (topk_indices[i] >= n_frames_no_sil) { + is_disabled[i] = true; + } + if (is_disabled[i]) { + topk_indices[i] = 0; // placeholder for gather + } + } + + // 8. Gather embeddings and predictions + std::vector new_embs((size_t)target_enc_len * d_model_); + std::vector new_preds((size_t)target_enc_len * n_spk_); + + // Use learned silence emb if available, otherwise running mean + const float* sil_emb = has_silence_emb_ ? silence_emb_.data() : mean_sil_emb_.data(); + + for (int i = 0; i < target_enc_len; ++i) { + int tidx = topk_indices[i]; + if (is_disabled[i]) { + std::memcpy(&new_embs[(size_t)i * d_model_], sil_emb, d_model_ * sizeof(float)); + std::memset(&new_preds[(size_t)i * n_spk_], 0, n_spk_ * sizeof(float)); + } else { + std::memcpy(&new_embs[(size_t)i * d_model_], + &spkcache_embs_[(size_t)tidx * d_model_], + d_model_ * sizeof(float)); + std::memcpy(&new_preds[(size_t)i * n_spk_], + &spkcache_preds_[(size_t)tidx * n_spk_], + n_spk_ * sizeof(float)); + } + } + + spkcache_embs_ = std::move(new_embs); + spkcache_preds_ = std::move(new_preds); + spkcache_enc_len_ = target_enc_len; +} + +void StreamingDiarization::stream_state_update( + const std::vector& chunk_preenc, int chunk_enc_len, + const std::vector& chunk_preds, int chunk_out_len, + const std::vector& full_pred_out, int full_out_len) { + + int old_sc_len = spkcache_enc_len_; + int old_fifo_len = fifo_enc_len_; + + // With fifo_len=0, the FIFO immediately overflows on every chunk. + // The chunk embeddings are popped from the FIFO and appended to the spkcache. + + // Extract chunk predictions (chunk_preds is already [n_spk, chunk_out_len]) + // We need predictions in [chunk_enc_len, n_spk] format. + // chunk_out_len should equal chunk_enc_len * upsample_factor, but the + // predictions used for scoring are at encoder resolution, so we subsample. + // + // Actually, the spkcache stores ENCODER-resolution predictions. + // The diarization head outputs at upsampled resolution (T_out = chunk_enc_len * upsample). + // For spkcache scoring, NeMo uses the pre-sigmoid predictions at encoder resolution. + // We approximate by taking the mean of each upsample_factor block. + std::vector chunk_preds_enc((size_t)chunk_enc_len * n_spk_, 0.0f); + if (chunk_out_len == chunk_enc_len) { + // Already at encoder resolution + std::memcpy(chunk_preds_enc.data(), chunk_preds.data(), + chunk_preds.size() * sizeof(float)); + } else if (chunk_out_len > 0 && chunk_out_len % chunk_enc_len == 0) { + int up = chunk_out_len / chunk_enc_len; + for (int t = 0; t < chunk_enc_len; ++t) { + for (int s = 0; s < n_spk_; ++s) { + float sum = 0; + for (int u = 0; u < up; ++u) { + int idx = t * up + u; + sum += chunk_preds[(size_t)s * chunk_out_len + idx]; + } + chunk_preds_enc[(size_t)t * n_spk_ + s] = sum / up; + } + } + } + + // Append chunk to FIFO + int new_fifo_total = old_fifo_len + chunk_enc_len; + std::vector updated_fifo((size_t)(old_fifo_len + chunk_enc_len) * d_model_); + std::vector updated_fifo_preds((size_t)(old_fifo_len + chunk_enc_len) * n_spk_); + + if (old_fifo_len > 0) { + std::memcpy(updated_fifo.data(), fifo_embs_.data(), + (size_t)old_fifo_len * d_model_ * sizeof(float)); + std::memcpy(updated_fifo_preds.data(), fifo_preds_.data(), + (size_t)old_fifo_len * n_spk_ * sizeof(float)); + } + std::memcpy(updated_fifo.data() + (size_t)old_fifo_len * d_model_, + chunk_preenc.data(), + (size_t)chunk_enc_len * d_model_ * sizeof(float)); + std::memcpy(updated_fifo_preds.data() + (size_t)old_fifo_len * n_spk_, + chunk_preds_enc.data(), + (size_t)chunk_enc_len * n_spk_ * sizeof(float)); + + // FIFO target in encoder frames + int fifo_enc_target = fifo_len_ / subsampling_factor_; + + if (new_fifo_total > fifo_enc_target) { + int pop_out_len = spkcache_len_ / subsampling_factor_; // spkcache_update_period + pop_out_len = std::max(pop_out_len, chunk_enc_len - fifo_enc_target + old_fifo_len); + pop_out_len = std::min(pop_out_len, new_fifo_total); + + const float* pop_embs = updated_fifo.data(); + const float* pop_preds = updated_fifo_preds.data(); + + update_silence_profile(pop_embs, pop_preds, pop_out_len); + + int remaining_fifo = new_fifo_total - pop_out_len; + fifo_embs_.resize((size_t)remaining_fifo * d_model_); + fifo_preds_.resize((size_t)remaining_fifo * n_spk_); + if (remaining_fifo > 0) { + std::memcpy(fifo_embs_.data(), + updated_fifo.data() + (size_t)pop_out_len * d_model_, + (size_t)remaining_fifo * d_model_ * sizeof(float)); + std::memcpy(fifo_preds_.data(), + updated_fifo_preds.data() + (size_t)pop_out_len * n_spk_, + (size_t)remaining_fifo * n_spk_ * sizeof(float)); + } + fifo_enc_len_ = remaining_fifo; + + // Append popped frames to spkcache + int new_sc_len = old_sc_len + pop_out_len; + spkcache_embs_.resize((size_t)new_sc_len * d_model_); + std::memcpy(spkcache_embs_.data() + (size_t)old_sc_len * d_model_, + pop_embs, (size_t)pop_out_len * d_model_ * sizeof(float)); + + if (spkcache_preds_valid_) { + spkcache_preds_.resize((size_t)new_sc_len * n_spk_); + std::memcpy(spkcache_preds_.data() + (size_t)old_sc_len * n_spk_, + pop_preds, (size_t)pop_out_len * n_spk_ * sizeof(float)); + } + spkcache_enc_len_ = new_sc_len; + + // Check if compression needed + int target_enc = spkcache_len_ / subsampling_factor_; + if (new_sc_len > target_enc) { + if (!spkcache_preds_valid_) { + // First time: init spkcache_preds from full prediction output + spkcache_preds_.resize((size_t)new_sc_len * n_spk_); + // Copy predictions for old spkcache frames from full_pred_out + if (old_sc_len > 0 && full_out_len >= old_sc_len) { + // full_pred_out is [n_spk, full_out_len] — need to transpose to + // [old_sc_len, n_spk] + for (int t = 0; t < old_sc_len; ++t) + for (int s = 0; s < n_spk_; ++s) + spkcache_preds_[(size_t)t * n_spk_ + s] = + full_pred_out[(size_t)s * full_out_len + t]; + } + // Copy pop_out predictions (already in [pop_out_len, n_spk]) + std::memcpy(spkcache_preds_.data() + (size_t)old_sc_len * n_spk_, + pop_preds, (size_t)pop_out_len * n_spk_ * sizeof(float)); + spkcache_preds_valid_ = true; + } + compress_spkcache(); + } + } else { + fifo_embs_ = std::move(updated_fifo); + fifo_preds_ = std::move(updated_fifo_preds); + fifo_enc_len_ = new_fifo_total; + } +} + +std::vector StreamingDiarization::postprocess_chunk( + const std::vector& probs, int n_spk, int T_out, + float time_offset) const { + + std::vector segments; + + for (int s = 0; s < n_spk; ++s) { + const float* p = probs.data() + (size_t)s * T_out; + + bool active = false; + int start_frame = 0; + + for (int t = 0; t < T_out; ++t) { + const bool on = (p[t] >= onset_threshold_); + if (on && !active) { + start_frame = t; + active = true; + } else if (!on && active) { + float start_sec = time_offset + start_frame * frame_sec_; + float end_sec = time_offset + t * frame_sec_; + segments.push_back({s, start_sec, end_sec}); + active = false; + } + } + if (active) { + float start_sec = time_offset + start_frame * frame_sec_; + float end_sec = time_offset + T_out * frame_sec_; + segments.push_back({s, start_sec, end_sec}); + } + } + + std::sort(segments.begin(), segments.end(), + [](const StreamingSpeakerSegment& a, const StreamingSpeakerSegment& b) { + if (a.start != b.start) return a.start < b.start; + return a.speaker < b.speaker; + }); + + for (auto& seg : segments) { + seg.start = std::round(seg.start * 100.0f) / 100.0f; + seg.end = std::round(seg.end * 100.0f) / 100.0f; + } + + return segments; +} + +} // namespace pk diff --git a/src/diarization_streaming.hpp b/src/diarization_streaming.hpp new file mode 100644 index 0000000..9cd8973 --- /dev/null +++ b/src/diarization_streaming.hpp @@ -0,0 +1,162 @@ +#pragma once +#include "model_loader.hpp" +#include "diarization_encoder.hpp" +#include "diarization_head.hpp" +#include "mel.hpp" +#include +#include +#include +#include + +namespace pk { + +// A speaker segment emitted during streaming. Timestamps are wall-clock seconds +// from the start of the stream. +struct StreamingSpeakerSegment { + int speaker; + float start; + float end; +}; + +// Streaming diarization state for the Nemotron-3-Diarization Sortformer. +// +// The AOSC ("Attention-Only Streaming with Compression") mechanism: +// +// 1. Audio is processed in chunks of `chunk_len` mel frames (264). +// 2. Each chunk is concatenated with the speaker cache: [spkcache | chunk]. +// The full bidirectional encoder runs over this concatenated sequence. +// 3. The encoder output for the chunk portion (last chunk_enc_len frames) +// is passed through the diarization head to get per-frame speaker probs. +// 4. The probs for the spkcache portion are stored for compression scoring. +// 5. After processing, the chunk embeddings are appended to a FIFO buffer. +// When the FIFO overflows (exceeds fifo_len), the oldest frames are popped +// and appended to the spkcache. +// 6. When the spkcache exceeds spkcache_len, AOSC compression is triggered: +// frames are scored per-speaker (log-odds), top-K are selected globally, +// and the spkcache is rebuilt to spkcache_len frames. +// +// For Nemotron-3-Diarization: +// fifo_len=0, spkcache_len=264, chunk_len=264 +// subsampling_factor=8, upsample_factor=8 +// use_learnable_sil_emb=true +// +// With fifo_len=0, every chunk immediately overflows the FIFO, so every chunk's +// embeddings are appended to the spkcache and compression runs after every chunk. +// +// The streaming path reuses the SAME encoder and head as the offline path — +// only the chunking and spkcache management differ. +class StreamingDiarization { +public: + explicit StreamingDiarization(const ModelLoader& ml); + ~StreamingDiarization(); + + // Reset the stream state (clear spkcache, frame counter). + void reset(); + + // Feed one chunk of mel features [n_mels, n_frames] (row-major: + // mel[m*n_frames + t]). The caller must provide exactly `chunk_len` frames + // (or fewer for the final chunk). Returns the speaker segments for this + // chunk (with wall-clock timestamps). + // + // is_last marks the final chunk — the spkcache is not updated after it. + std::vector feed_mel_chunk( + const std::vector& mel_chunk, int n_mels, int n_frames, + bool is_last = false); + + // Chunk parameters (from GGUF config). + int chunk_len() const { return chunk_len_; } + int spkcache_len() const { return spkcache_len_; } + int fifo_len() const { return fifo_len_; } + int n_mels() const { return n_mels_; } + int n_speakers() const { return n_spk_; } + + // The frame-to-second conversion: each output frame is hop_length/sample_rate + // seconds = 160/16000 = 0.01s. + float frame_sec() const { return frame_sec_; } + +private: + const ModelLoader& ml_; + DiarizationEncoder encoder_; + DiarizationHead head_; + + int d_model_; // encoder d_model (512) + int tf_d_model_; // sortformer tf_d_model (192) + int n_spk_; // number of speakers (8) + int n_layers_; // transformer blocks (31) + int n_heads_; // attention heads (8) + int head_dim_; // d_model / n_heads (64) + int ff_dim_; // feed-forward dim (2048) + int subsampling_factor_; // 8 + int upsample_factor_; // 8 + int n_mels_; // 128 + int chunk_len_; // 264 mel frames + int spkcache_len_; // 264 mel frames + int fifo_len_; // 0 for Nemotron-3 + float frame_sec_; // 0.01s per output frame + float onset_threshold_; + float offset_threshold_; + + // --- AOSC streaming config --- + int spkcache_sil_frames_per_spk_; // 3 + float sil_threshold_; // 0.2 + float pred_score_threshold_; // 0.25 + float scores_boost_latest_; // 0.05 + float strong_boost_rate_; // 0.75 + float weak_boost_rate_; // 1.5 + float min_pos_scores_rate_; // 0.5 + int max_index_; // 99999 (placeholder for disabled slots) + + // --- Spkcache state --- + // Encoder embeddings [d_model, spkcache_enc_len] (channels-first: emb[c*len + t]) + std::vector spkcache_embs_; + std::vector spkcache_preds_; // [n_spk, spkcache_enc_len] + int spkcache_enc_len_ = 0; + bool spkcache_preds_valid_ = false; + + // --- FIFO state --- + // With fifo_len=0, this overflows every chunk. + std::vector fifo_embs_; // [d_model, fifo_enc_len] + std::vector fifo_preds_; // [n_spk, fifo_enc_len] + int fifo_enc_len_ = 0; + + // --- Silence profile --- + std::vector mean_sil_emb_; // [d_model] + int n_sil_frames_ = 0; + + // --- The learned silence embedding (model parameter) --- + bool has_silence_emb_ = false; + std::vector silence_emb_; // [d_model] + + // Running count of total mel frames consumed (for wall-clock timestamps). + int total_mel_frames_ = 0; + + // --- Internal helpers --- + + // AOSC: boost top-K scores per speaker + void boost_topk_scores(float* scores, int n_frames, int n_spk, + int k_per_spk, float scale_factor, float offset) const; + + // AOSC: compress spkcache from current length to spkcache_len_ frames. + void compress_spkcache(); + + // Update running silence profile from popped embeddings. + void update_silence_profile(const float* pop_embs, const float* pop_preds, + int pop_len); + + // Update stream state after processing one chunk (FIFO → spkcache → compress). + // chunk_preenc: pre-encoded embeddings for the chunk [d_model, chunk_enc_len] + // chunk_preds: per-speaker probs for the chunk [n_spk, chunk_out_len] + // full_pred_out: full prediction output for [spkcache | chunk] [n_spk, total_out_len] + // (used to extract spkcache predictions for scoring) + void stream_state_update( + const std::vector& chunk_preenc, int chunk_enc_len, + const std::vector& chunk_preds, int chunk_out_len, + const std::vector& full_pred_out, int full_out_len); + + // Post-process per-frame speaker probabilities into segments for this chunk. + std::vector postprocess_chunk( + const std::vector& probs, int n_spk, int T_out, + float time_offset) const; +}; + +} // namespace pk diff --git a/src/model_loader.cpp b/src/model_loader.cpp index f7d37f8..ab63ae6 100644 --- a/src/model_loader.cpp +++ b/src/model_loader.cpp @@ -207,6 +207,39 @@ bool ModelLoader::load(const std::string& path){ d.frame_resolution_sec = kv_f32(gguf_, "parakeet.diar.frame_resolution_sec", 0.01f); d.onset_threshold = kv_f32(gguf_, "parakeet.diar.onset_threshold", 0.5f); d.offset_threshold = kv_f32(gguf_, "parakeet.diar.offset_threshold", 0.5f); + // AOSC streaming config (Phase 2) + // If streaming KVs are absent, use Nemotron-3-Diarization defaults + // so the streaming C-API works out of the box. + if (gguf_find_key(gguf_, "parakeet.diar.chunk_len") >= 0) { + d.streaming_capable = true; + d.chunk_len = (int32_t)kv_u32(gguf_, "parakeet.diar.chunk_len"); + d.spkcache_len = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_len"); + d.fifo_len = (int32_t)kv_u32(gguf_, "parakeet.diar.fifo_len", 0); + d.spkcache_update_period = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_update_period", (uint32_t)d.chunk_len); + d.spkcache_sil_frames_per_spk = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_sil_frames_per_spk", 3); + d.sil_threshold = kv_f32(gguf_, "parakeet.diar.sil_threshold", 0.2f); + d.pred_score_threshold = kv_f32(gguf_, "parakeet.diar.pred_score_threshold", 0.25f); + d.scores_boost_latest = kv_f32(gguf_, "parakeet.diar.scores_boost_latest", 0.05f); + d.strong_boost_rate = kv_f32(gguf_, "parakeet.diar.strong_boost_rate", 0.75f); + d.weak_boost_rate = kv_f32(gguf_, "parakeet.diar.weak_boost_rate", 1.5f); + d.min_pos_scores_rate = kv_f32(gguf_, "parakeet.diar.min_pos_scores_rate", 0.5f); + d.use_learnable_sil_emb = kv_bool(gguf_, "parakeet.diar.use_learnable_sil_emb", false); + } else { + // Nemotron-3-Diarization defaults + d.streaming_capable = true; + d.chunk_len = 264; + d.spkcache_len = 264; + d.fifo_len = 0; + d.spkcache_update_period = 264; + d.spkcache_sil_frames_per_spk = 3; + d.sil_threshold = 0.2f; + d.pred_score_threshold = 0.25f; + d.scores_boost_latest = 0.05f; + d.strong_boost_rate = 0.75f; + d.weak_boost_rate = 1.5f; + d.min_pos_scores_rate = 0.5f; + d.use_learnable_sil_emb = (gguf_find_key(gguf_, "sortformer_modules.learnable_sil_emb") >= 0); + } } // durations array (stored as INT32 by the converter) { int64_t id = gguf_find_key(gguf_, "parakeet.tdt.durations"); diff --git a/src/model_loader.hpp b/src/model_loader.hpp index 0d35636..417dde2 100644 --- a/src/model_loader.hpp +++ b/src/model_loader.hpp @@ -82,6 +82,20 @@ struct ParakeetConfig { float frame_resolution_sec=0.01f; // output frame duration float onset_threshold=0.5f; // hysteresis onset float offset_threshold=0.5f; // hysteresis offset + // --- AOSC streaming config (Phase 2) --- + bool streaming_capable=false; + int32_t chunk_len=0; // mel frames per chunk (264) + int32_t spkcache_len=0; // target spkcache size in mel frames (264) + int32_t fifo_len=0; // FIFO buffer length (0 for Nemotron-3) + int32_t spkcache_update_period=0; // frames before spkcache update + int32_t spkcache_sil_frames_per_spk=3; // silence placeholders per speaker + float sil_threshold=0.2f; // silence detection threshold + float pred_score_threshold=0.25f; // log-score clamp floor + float scores_boost_latest=0.05f; // boost for latest frames + float strong_boost_rate=0.75f; // strong top-K fraction + float weak_boost_rate=1.5f; // weak top-K fraction + float min_pos_scores_rate=0.5f; // min positive scores fraction + bool use_learnable_sil_emb=false; // silence embedding is a model param } diarization; // Diarization encoder config (Nemotron-3-Diarization uses a TransformerEncoder // with RoPE, not a FastConformer). These are read from parakeet.encoder.* KVs diff --git a/src/parakeet_capi.cpp b/src/parakeet_capi.cpp index ca1b1fd..147dd5e 100644 --- a/src/parakeet_capi.cpp +++ b/src/parakeet_capi.cpp @@ -2,6 +2,7 @@ #include "parakeet.h" // pk::Decoder #include "model.hpp" // pk::Model #include "diarization.hpp" // pk::DiarizationModel +#include "diarization_streaming.hpp" // pk::StreamingDiarization #include "streaming.hpp" // pk::StreamingSession #include "mel.hpp" // pk::MelFrontend #include "sas_merge.hpp" // pk::merge_asr_diarization, pk::group_speaker_words @@ -41,7 +42,7 @@ // v7: speaker-attributed ASR (SAS) entry points — takes two contexts // (ASR + diarization), runs both models, merges word timestamps with // speaker segments. -#define PARAKEET_CAPI_ABI_VERSION 7 +#define PARAKEET_CAPI_ABI_VERSION 8 // The opaque context: a loaded model plus a buffer for the last error message. // Exactly one of `model` / `diar` is non-null: ASR models use `model`, @@ -1105,3 +1106,203 @@ extern "C" char* parakeet_capi_transcribe_and_diarize_json( return sas_results_to_json_full(utts, swords, n_speakers); } + +// =========================================================================== +// v8: Streaming diarization C-API +// =========================================================================== + +struct parakeet_diar_stream { + std::unique_ptr diar; + parakeet_ctx* diar_ctx = nullptr; + std::string error; +}; + +extern "C" parakeet_diar_stream* parakeet_capi_diarize_stream_begin( + parakeet_ctx* diar_ctx) { + if (!diar_ctx || !diar_ctx->diar) { + return nullptr; + } + auto* stream = new (std::nothrow) parakeet_diar_stream(); + if (!stream) return nullptr; + stream->diar_ctx = diar_ctx; + stream->diar = std::make_unique(diar_ctx->diar->loader()); + stream->diar->reset(); + return stream; +} + +extern "C" parakeet_diar_segment* parakeet_capi_diarize_stream_feed( + parakeet_diar_stream* stream, + const float* mel, int n_mels, int n_frames, + int is_last, int* out_count) { + if (!stream || !mel || n_mels <= 0 || n_frames <= 0) { + if (out_count) *out_count = 0; + return nullptr; + } + try { + std::vector mel_vec(mel, mel + (size_t)n_mels * n_frames); + auto segs = stream->diar->feed_mel_chunk(mel_vec, n_mels, n_frames, is_last != 0); + if (segs.empty()) { + if (out_count) *out_count = 0; + return nullptr; + } + auto* out = (parakeet_diar_segment*)std::malloc(segs.size() * sizeof(parakeet_diar_segment)); + if (!out) { + if (out_count) *out_count = 0; + return nullptr; + } + for (size_t i = 0; i < segs.size(); ++i) { + out[i].speaker = segs[i].speaker; + out[i].start = segs[i].start; + out[i].end = segs[i].end; + } + if (out_count) *out_count = (int)segs.size(); + return out; + } catch (const std::exception& e) { + stream->error = e.what(); + if (out_count) *out_count = 0; + return nullptr; + } +} + +extern "C" int parakeet_capi_diar_stream_chunk_len(parakeet_diar_stream* stream) { + if (!stream || !stream->diar) return 0; + return stream->diar->chunk_len(); +} + +extern "C" int parakeet_capi_diar_stream_n_mels(parakeet_diar_stream* stream) { + if (!stream || !stream->diar) return 0; + return stream->diar->n_mels(); +} + +extern "C" void parakeet_capi_free_diar_segments(parakeet_diar_segment* segs) { + std::free(segs); +} + +extern "C" void parakeet_capi_diarize_stream_free(parakeet_diar_stream* stream) { + delete stream; +} + +// =========================================================================== +// v8: Streaming speaker-attributed ASR (Phase 3.4) +// =========================================================================== + +struct parakeet_sas_stream { + parakeet_ctx* asr_ctx = nullptr; + parakeet_diar_stream* diar_stream = nullptr; + std::vector pcm_buffer; + int asr_chunk_samples = 0; + int total_samples = 0; + std::string error; +}; + +extern "C" parakeet_sas_stream* parakeet_capi_sas_stream_begin( + parakeet_ctx* asr_ctx, parakeet_ctx* diar_ctx) { + if (!asr_ctx || !asr_ctx->model || !diar_ctx || !diar_ctx->diar) { + return nullptr; + } + auto* stream = new (std::nothrow) parakeet_sas_stream(); + if (!stream) return nullptr; + stream->asr_ctx = asr_ctx; + stream->diar_stream = parakeet_capi_diarize_stream_begin(diar_ctx); + if (!stream->diar_stream) { + delete stream; + return nullptr; + } + int chunk_mel = stream->diar_stream->diar->chunk_len(); + stream->asr_chunk_samples = chunk_mel * 160; // hop_length = 160 + stream->total_samples = 0; + return stream; +} + +extern "C" parakeet_sas_result* parakeet_capi_sas_stream_feed( + parakeet_sas_stream* stream, + const float* pcm, int n_samples, + int is_last, int* out_count) { + if (!stream || !pcm || n_samples <= 0) { + if (out_count) *out_count = 0; + return nullptr; + } + try { + stream->pcm_buffer.insert(stream->pcm_buffer.end(), pcm, pcm + n_samples); + + std::vector all_words; + std::vector all_segs; + + while ((int)stream->pcm_buffer.size() >= stream->asr_chunk_samples || + (is_last && !stream->pcm_buffer.empty())) { + int chunk_samples = std::min(stream->asr_chunk_samples, + (int)stream->pcm_buffer.size()); + bool chunk_is_last = is_last && + (chunk_samples == (int)stream->pcm_buffer.size()); + + std::vector chunk_pcm(stream->pcm_buffer.begin(), + stream->pcm_buffer.begin() + chunk_samples); + + // 1. Run ASR on the chunk + pk::Transcription trans = + stream->asr_ctx->model->transcribe_with_timestamps(chunk_pcm, 16000); + float chunk_offset = (float)stream->total_samples / 16000.0f; + for (auto& w : trans.words) { + w.start += chunk_offset; + w.end += chunk_offset; + all_words.push_back(w); + } + + // 2. Run diarization on the chunk + std::vector mel; + int n_mels = 0, n_frames = 0; + stream->diar_stream->diar_ctx->diar->mel().compute( + chunk_pcm, mel, n_mels, n_frames); + + auto segs = stream->diar_stream->diar->feed_mel_chunk( + mel, n_mels, n_frames, chunk_is_last); + for (auto& s : segs) + all_segs.push_back(s); + + stream->pcm_buffer.erase(stream->pcm_buffer.begin(), + stream->pcm_buffer.begin() + chunk_samples); + stream->total_samples += chunk_samples; + + if (chunk_is_last) break; + } + + // 3. Merge ASR words with diarization segments + std::vector diar_segs; + for (auto& s : all_segs) + diar_segs.push_back({s.speaker, s.start, s.end}); + + auto swords = pk::merge_asr_diarization(all_words, diar_segs); + auto utts = pk::group_speaker_words(swords, 0.5f); + + if (utts.empty()) { + if (out_count) *out_count = 0; + return nullptr; + } + + auto* results = (parakeet_sas_result*)std::calloc(utts.size(), + sizeof(parakeet_sas_result)); + if (!results) { + if (out_count) *out_count = 0; + return nullptr; + } + for (size_t i = 0; i < utts.size(); ++i) { + results[i].speaker = utts[i].speaker; + results[i].text = dup_to_c(utts[i].text); + results[i].start = utts[i].start; + results[i].end = utts[i].end; + results[i].conf = utts[i].conf; + } + if (out_count) *out_count = (int)utts.size(); + return results; + } catch (const std::exception& e) { + stream->error = e.what(); + if (out_count) *out_count = 0; + return nullptr; + } +} + +extern "C" void parakeet_capi_sas_stream_free(parakeet_sas_stream* stream) { + if (!stream) return; + parakeet_capi_diarize_stream_free(stream->diar_stream); + delete stream; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0db851b..dc1f50f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -80,6 +80,7 @@ pk_add_test(test_subpixel_check) pk_add_test(test_3d_permute) pk_add_test(test_sas_merge) pk_add_test(test_combined_offline) +pk_add_test(test_streaming_diarization) if(TARGET parakeet-cli) add_test(NAME cli_version_long COMMAND $ --version) @@ -137,7 +138,7 @@ set_tests_properties(test_model_loader test_mel test_mel_gpu test_subsampling te test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json test_capi_timestamps test_capi_batch_json test_capi_ctc_logits - test_combined_offline + test_combined_offline test_streaming_diarization PROPERTIES LABELS "model") # These tests read fixtures/baselines via paths relative to the project root. set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_batch test_subsampling_batch_causal test_relpos_attention test_relpos_attention_batch test_conformer test_conformer_batch @@ -154,7 +155,7 @@ set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_bat test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json test_capi_timestamps test_capi_batch_json test_capi_ctc_logits - test_combined_offline + test_combined_offline test_streaming_diarization PROPERTIES WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) # Python converter check (skips with exit 77 when the venv/model are absent). diff --git a/tests/test_streaming_diarization.cpp b/tests/test_streaming_diarization.cpp new file mode 100644 index 0000000..3281a14 --- /dev/null +++ b/tests/test_streaming_diarization.cpp @@ -0,0 +1,130 @@ +// test_streaming_diarization.cpp +// +// Tests the streaming diarization path (Phase 2) against the offline path. +// Loads the diarization GGUF, processes a WAV file in chunks, and compares +// the total number of segments and speaker coverage with the offline path. +// +// Environment: +// PARAKEET_TEST_DIAR_GGUF — path to the diarization GGUF +// PARAKEET_TEST_AUDIO — path to a test WAV file + +#include "diarization.hpp" +#include "diarization_streaming.hpp" +#include "mel.hpp" +#include "audio_io.hpp" + +#include +#include +#include +#include +#include + +int main() { + const char* gguf_path = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + const char* audio_path = std::getenv("PARAKEET_TEST_AUDIO"); + if (!gguf_path || !audio_path) { + std::printf("test_streaming_diarization: PARAKEET_TEST_DIAR_GGUF or PARAKEET_TEST_AUDIO not set; skip\n"); + return 77; + } + + // Load the diarization model + auto model = pk::DiarizationModel::load(gguf_path); + if (!model) { + std::printf("test_streaming_diarization: failed to load model: %s\n", gguf_path); + return 1; + } + + // Load audio + pk::Audio audio; + if (!pk::load_audio_16k_mono(audio_path, audio)) { + std::printf("test_streaming_diarization: failed to load audio: %s\n", audio_path); + return 1; + } + std::printf("test_streaming_diarization: loaded %s (%zu samples, %d Hz)\n", + audio_path, audio.samples.size(), audio.sample_rate); + + // 1. Run offline diarization for reference + auto offline_result = model->diarize_pcm(audio.samples, audio.sample_rate); + std::printf("test_streaming_diarization: offline segments = %zu\n", + offline_result.segments.size()); + + // 2. Run streaming diarization + pk::StreamingDiarization stream(model->loader()); + stream.reset(); + + // Compute mel features for the full audio + std::vector mel; + int n_mels = 0, T = 0; + model->mel().compute(audio.samples, mel, n_mels, T); + std::printf("test_streaming_diarization: mel = %d x %d\n", n_mels, T); + + int chunk_len = stream.chunk_len(); + int n_mels_expected = stream.n_mels(); + assert(n_mels == n_mels_expected); + + std::vector stream_segs; + int offset = 0; + while (offset < T) { + int n_frames = std::min(chunk_len, T - offset); + bool is_last = (offset + n_frames >= T); + + // Extract chunk: mel[m*n_frames + t] + std::vector chunk((size_t)n_mels * n_frames); + for (int m = 0; m < n_mels; ++m) + for (int t = 0; t < n_frames; ++t) + chunk[(size_t)m * n_frames + t] = mel[(size_t)m * T + (offset + t)]; + + auto segs = stream.feed_mel_chunk(chunk, n_mels, n_frames, is_last); + for (auto& s : segs) + stream_segs.push_back(s); + + offset += n_frames; + } + + std::printf("test_streaming_diarization: streaming segments = %zu\n", + stream_segs.size()); + + // 3. Verify: streaming should produce a reasonable number of segments. + // The exact count won't match offline (streaming uses spkcache context), + // but it should be in the same ballpark. + if (stream_segs.empty()) { + std::printf("test_streaming_diarization: FAIL — no streaming segments produced\n"); + return 1; + } + + // Count unique speakers in both + std::set offline_spk, stream_spk; + for (auto& s : offline_result.segments) offline_spk.insert(s.speaker); + for (auto& s : stream_segs) stream_spk.insert(s.speaker); + + std::printf("test_streaming_diarization: offline speakers = %zu, streaming speakers = %zu\n", + offline_spk.size(), stream_spk.size()); + + // Streaming should detect at least 1 speaker + if (stream_spk.empty()) { + std::printf("test_streaming_diarization: FAIL — no speakers detected in streaming\n"); + return 1; + } + + // Segments should be in chronological order + for (size_t i = 1; i < stream_segs.size(); ++i) { + if (stream_segs[i].start < stream_segs[i-1].start) { + std::printf("test_streaming_diarization: FAIL — segments not in order at %zu\n", i); + return 1; + } + } + + // Segment timestamps should be within the audio duration + float audio_dur = (float)audio.samples.size() / 16000.0f; + for (auto& s : stream_segs) { + if (s.start < 0.0f || s.end > audio_dur + 1.0f) { + std::printf("test_streaming_diarization: FAIL — segment out of range: %.2f-%.2f (dur=%.2f)\n", + s.start, s.end, audio_dur); + return 1; + } + } + + std::printf("test_streaming_diarization: PASS (%zu streaming segments, %zu offline)\n", + stream_segs.size(), offline_result.segments.size()); + return 0; +} From 5495716856b1779edafdfe33be5c363a0a506515 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 00:12:53 +0000 Subject: [PATCH 08/17] feat(converter): write AOSC streaming config KVs to diarization GGUF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Write parakeet.diar.chunk_len, spkcache_len, fifo_len, and the AOSC compression tuning parameters as GGUF KV pairs. Values are read from the NeMo config 'streaming' section, with Nemotron-3-Diarization defaults (chunk_len=264, spkcache_len=264, fifo_len=0) when absent. The model loader already falls back to these defaults when the KVs are missing, so this is not a breaking change — but writing them explicitly ensures non-default models are handled correctly. --- scripts/convert_parakeet_to_gguf.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/convert_parakeet_to_gguf.py b/scripts/convert_parakeet_to_gguf.py index 11c6f12..7301d25 100644 --- a/scripts/convert_parakeet_to_gguf.py +++ b/scripts/convert_parakeet_to_gguf.py @@ -567,6 +567,34 @@ def _int_list(v): w.add_float32("parakeet.diar.onset_threshold", onset) w.add_float32("parakeet.diar.offset_threshold", offset) + # AOSC streaming config (Phase 2) + # Nemotron-3-Diarization defaults from NeMo config + streaming_cfg = _get(cfg, "streaming", {}) or {} + chunk_len = int(_get(streaming_cfg, "chunk_len", 264)) + spkcache_len = int(_get(streaming_cfg, "spkcache_len", chunk_len)) + fifo_len = int(_get(streaming_cfg, "fifo_len", 0)) + spkcache_update = int(_get(streaming_cfg, "spkcache_update_period", chunk_len)) + sil_per_spk = int(_get(streaming_cfg, "spkcache_sil_frames_per_spk", 3)) + sil_thresh = float(_get(streaming_cfg, "sil_threshold", 0.2)) + pred_score_thresh = float(_get(streaming_cfg, "pred_score_threshold", 0.25)) + scores_boost = float(_get(streaming_cfg, "scores_boost_latest", 0.05)) + strong_boost = float(_get(streaming_cfg, "strong_boost_rate", 0.75)) + weak_boost = float(_get(streaming_cfg, "weak_boost_rate", 1.5)) + min_pos = float(_get(streaming_cfg, "min_pos_scores_rate", 0.5)) + learnable_sil = bool(_get(streaming_cfg, "use_learnable_sil_emb", True)) + w.add_uint32("parakeet.diar.chunk_len", chunk_len) + w.add_uint32("parakeet.diar.spkcache_len", spkcache_len) + w.add_uint32("parakeet.diar.fifo_len", fifo_len) + w.add_uint32("parakeet.diar.spkcache_update_period", spkcache_update) + w.add_uint32("parakeet.diar.spkcache_sil_frames_per_spk", sil_per_spk) + w.add_float32("parakeet.diar.sil_threshold", sil_thresh) + w.add_float32("parakeet.diar.pred_score_threshold", pred_score_thresh) + w.add_float32("parakeet.diar.scores_boost_latest", scores_boost) + w.add_float32("parakeet.diar.strong_boost_rate", strong_boost) + w.add_float32("parakeet.diar.weak_boost_rate", weak_boost) + w.add_float32("parakeet.diar.min_pos_scores_rate", min_pos) + w.add_bool("parakeet.diar.use_learnable_sil_emb", learnable_sil) + # transducer config if arch in ("rnnt", "tdt", "hybrid_rnnt_ctc", "hybrid_tdt_ctc"): prednet = _get(cfg.decoder, "prednet", {}) or {} From 6869cba0d2d18e33dd444bd7883ac6bc319d409d Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:23:52 +0000 Subject: [PATCH 09/17] fix(diarization): match NeMo offline speaker probabilities The offline diarization output disagreed with NeMo on most frames and almost never detected a second speaker. Three causes: - The subpixel upsample used the wrong interleave. NeMo reshapes the conv output as [T, up, hidden], so output frame t*up+u reads conv channels u*tf..u*tf+tf at frame t. The old code read channel h*up+u and wrote frame u*T+t, which scrambled time across the whole clip. The conv now runs as im2col + mul_mat in F32, and its time-major output reshapes straight into the upsampled sequence. - NeMo peak-normalizes the waveform in offline mode (x / (max(x) + 1e-3)). Without it every log-mel bin was shifted. - The mel was not trimmed to floor(S / hop), and the upsampled output kept the FeatureStacking pad frames. The pipeline up to the probabilities is now DiarizationModel:: speaker_probs, which returns the same tensor as NeMo's offline forward(). On a 2-speaker clip the F32 max prob diff vs NeMo drops from 0.96 to 0.004 and the segments match exactly. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- src/diarization.cpp | 73 +++++++++++++++++++++++++------ src/diarization.hpp | 6 +++ src/diarization_head.cpp | 70 ++++++++++------------------- tests/test_diarization_parity.cpp | 4 +- 4 files changed, 89 insertions(+), 64 deletions(-) diff --git a/src/diarization.cpp b/src/diarization.cpp index 544d845..64cbb9e 100644 --- a/src/diarization.cpp +++ b/src/diarization.cpp @@ -59,33 +59,78 @@ DiarizationResult DiarizationModel::diarize_pcm( return run(pcm16k); } -DiarizationResult DiarizationModel::run(const std::vector& samples) { +void DiarizationModel::speaker_probs(const std::vector& samples, + std::vector& probs, + int& n_spk, int& T) const { const ParakeetConfig& cfg = loader_.config(); - - // 1. Log-mel front end → feats [n_mels, T] + n_spk = (int)cfg.diarization.n_speakers; + T = 0; + probs.clear(); + + // 1. Log-mel front end -> feats [n_mels, T] + // NeMo SortformerEncLabelModel.process_signal peak-normalizes the waveform + // in offline (non-streaming) mode: x * 1 / (max(x) + eps), eps = 1e-3. + // Note max(x), not max(|x|). + std::vector norm(samples); + if (!norm.empty()) { + const float peak = *std::max_element(norm.begin(), norm.end()); + const float scale = 1.0f / (peak + 1e-3f); + for (float& v : norm) v *= scale; + } std::vector feats; - int n_mels = 0, T = 0; - mel_->compute(samples, feats, n_mels, T); + int n_mels = 0, T_mel = 0; + mel_->compute(norm, feats, n_mels, T_mel); + + // NeMo trims the features to the valid length floor(S / hop) before the + // encoder; the centered STFT yields one extra frame past it. + const int hop = (int)cfg.hop_length; + const int T_valid = hop > 0 ? std::min(T_mel, (int)(samples.size() / hop)) : T_mel; + if (T_valid < T_mel) { + std::vector trimmed((size_t)n_mels * T_valid); + for (int m = 0; m < n_mels; ++m) + std::copy_n(feats.begin() + (size_t)m * T_mel, T_valid, + trimmed.begin() + (size_t)m * T_valid); + feats.swap(trimmed); + T_mel = T_valid; + } + if (T_mel == 0) return; - // 2. Diarization encoder → enc_out [d_model, T_enc] (channels-first) + // 2. Diarization encoder -> enc_out [d_model, T_enc] (channels-first) std::vector enc_out; int d_model = 0, T_enc = 0; - encoder_->forward(feats, n_mels, T, enc_out, d_model, T_enc); + encoder_->forward(feats, n_mels, T_mel, enc_out, d_model, T_enc); + + // 3. Diarization head -> probs [n_spk, T_out] (post-sigmoid) + int T_out = 0; + head_->forward(enc_out, d_model, T_enc, probs, n_spk, T_out); + + // High-resolution output has one frame per mel frame; drop the frames the + // FeatureStacking pad added past T_mel (NeMo slices preds to the mel length). + if (T_out > T_mel) { + std::vector cut((size_t)n_spk * T_mel); + for (int s = 0; s < n_spk; ++s) + std::copy_n(probs.begin() + (size_t)s * T_out, T_mel, + cut.begin() + (size_t)s * T_mel); + probs.swap(cut); + T_out = T_mel; + } + T = T_out; +} + +DiarizationResult DiarizationModel::run(const std::vector& samples) { + const ParakeetConfig& cfg = loader_.config(); - // 3. Diarization head → probs [n_spk, T_out] (post-sigmoid) std::vector probs; int n_spk = 0, T_out = 0; - head_->forward(enc_out, d_model, T_enc, probs, n_spk, T_out); + speaker_probs(samples, probs, n_spk, T_out); - // 4. Post-process → speaker segments - std::vector segs = postprocess( + // 4. Post-process -> speaker segments + DiarizationResult result; + result.segments = postprocess( probs, n_spk, T_out, cfg.diarization.frame_resolution_sec, cfg.diarization.onset_threshold, cfg.diarization.offset_threshold); - - DiarizationResult result; - result.segments = std::move(segs); result.n_speakers = n_spk; return result; } diff --git a/src/diarization.hpp b/src/diarization.hpp index a60387c..5ce1af6 100644 --- a/src/diarization.hpp +++ b/src/diarization.hpp @@ -44,6 +44,12 @@ class DiarizationModel { DiarizationResult diarize_pcm(const std::vector& samples, int sample_rate); + // Per-frame speaker activity probabilities for already-16 kHz PCM, the + // same tensor NeMo's offline forward() returns: row-major [n_spk, T] + // (probs[s*T + t], post-sigmoid), one frame per 10 ms mel frame. + void speaker_probs(const std::vector& pcm16k, std::vector& probs, + int& n_spk, int& T) const; + const ParakeetConfig& config() const { return loader_.config(); } const ModelLoader& loader() const { return loader_; } diff --git a/src/diarization_head.cpp b/src/diarization_head.cpp index 32a2f0a..0ea2ffc 100644 --- a/src/diarization_head.cpp +++ b/src/diarization_head.cpp @@ -65,61 +65,35 @@ void DiarizationHead::forward(const std::vector& enc_out, int d_model, in ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); if (ep_b) proj = ggml_add(ctx, proj, ep_b); - // ---- subpixel_upsample: Conv1d(tf → tf*up, k=3, pad=1) + bias ---- - // Conv1d expects data as ne[0]=T, ne[1]=IC, ne[2]=N - // proj is ne[0]=tf, ne[1]=T_enc → need transpose + // ---- subpixel_upsample: Conv1d(tf -> tf*up, k=3, pad=1) + bias ---- + // im2col + mul_mat in F32 (ggml_conv_1d would force an F16 im2col). + // im2col wants data as ne=[T, IC, N]; proj is ne=[tf, T_enc]. ggml_tensor* conv_in = ggml_cont(ctx, ggml_transpose(ctx, proj)); - // conv_in: ne[0]=T_enc, ne[1]=tf conv_in = ggml_reshape_3d(ctx, conv_in, T_enc, tf, 1); - // conv_in: ne[0]=T_enc, ne[1]=tf, ne[2]=1 - // Conv1d weight: GGUF stores ne=[k, IC, OC]=[3, 192, 1536] (converter - // already wrote it in ggml layout). Use directly. + // GGUF stores the PyTorch [OC, IC, k] weight as ggml ne=[k, IC, OC]. ggml_tensor* spk_w = ml.tensor("sortformer_modules.subpixel_upsample.weight"); if (!spk_w) throw std::runtime_error("parakeet: missing sortformer_modules.subpixel_upsample.weight"); + ggml_tensor* cols = ggml_im2col(ctx, spk_w, conv_in, /*s0*/1, /*s1*/0, + /*p0*/1, /*p1*/0, /*d0*/1, /*d1*/0, + /*is_2D*/false, GGML_TYPE_F32); + // cols: ne=[k*IC, T_enc, 1] + cols = ggml_reshape_2d(ctx, cols, cols->ne[0], T_enc); + ggml_tensor* w2d = spk_w->type == GGML_TYPE_F32 + ? spk_w : ggml_cast(ctx, spk_w, GGML_TYPE_F32); + w2d = ggml_reshape_2d(ctx, w2d, spk_w->ne[0] * spk_w->ne[1], spk_w->ne[2]); + ggml_tensor* conv_out = ggml_mul_mat(ctx, w2d, cols); + // conv_out: ne=[OC=tf*up, T_enc], flat[c + t*OC] - // ggml's CPU im2col expects the conv kernel to be F16 (assertion - // in ggml_compute_forward_im2col_f16). Cast it explicitly. - spk_w = ggml_cast(ctx, spk_w, GGML_TYPE_F16); - // Also cast the input to F16 for the im2col path - conv_in = ggml_cast(ctx, conv_in, GGML_TYPE_F16); - - // ggml_conv_1d(ctx, kernel, data, stride=1, pad=1, dilation=1) - ggml_tensor* conv_out = ggml_conv_1d(ctx, spk_w, conv_in, 1, 1, 1); - // conv_out: ne[0]=T_enc, ne[1]=tf*up, ne[2]=1 - // Data layout: flat[t + c*T_enc] (ne[0]=T_enc fastest) - - // Reshape to 2D (keep ne[0]=T_enc, ne[1]=tf*up) - conv_out = ggml_reshape_2d(ctx, conv_out, T_enc, tf * up); - // ne[0]=T_enc, ne[1]=tf*up, data: flat[t + c*T_enc] - - // Add subpixel bias [tf*up] directly. Reshape to [1, tf*up] - // so ggml_add broadcasts over ne[0]=T_enc. ggml_tensor* spk_b = ml.tensor("sortformer_modules.subpixel_upsample.bias"); - if (spk_b) { - ggml_tensor* spk_b_2d = ggml_reshape_2d(ctx, spk_b, 1, tf * up); - conv_out = ggml_add(ctx, conv_out, spk_b_2d); - } - - // Subpixel reshape: conv_out is ne=[T_enc, tf*up], data: flat[t + c*T_enc]. - // - // Reference PyTorch: x.view(B, C//up, up, T) then x.view(B, C//up, up*T) - // → up_pk[h, t'] = conv[h*up+u, t] where t' = u*T + t - // - // In ggml (column-major, ne[0] fastest): - // 1. reshape_3d(T_enc, up, tf): ne=[T_enc, up, tf] - // element(t,u,h) = flat[t + u*T_enc + h*up*T_enc] = flat[t + c*T_enc] ✓ - // 2. reshape_2d(T_out, tf): ne=[T_out, tf] - // element(t',h) = flat[t' + h*T_out] where t' = t + u*T_enc ✓ - // 3. transpose: ne=[tf, T_out] - // 4. cont: copies to flat[h + t'*tf] (2D cont works correctly) - // - // NOTE: 3D permute+cont is BROKEN in this ggml backend — the cont op - // does not actually rearrange data for 3D tensors. Using 2D - // transpose+cont avoids this bug. - ggml_tensor* upsampled = ggml_reshape_3d(ctx, conv_out, T_enc, up, tf); - upsampled = ggml_reshape_2d(ctx, upsampled, T_enc * up, tf); - upsampled = ggml_cont(ctx, ggml_transpose(ctx, upsampled)); + if (spk_b) conv_out = ggml_add(ctx, conv_out, spk_b); + + // Subpixel shuffle, NeMo SortformerModules.upsample_hidden: + // conv(x).transpose(1,2).reshape(B, T, up, tf).reshape(B, T*up, tf) + // so output frame t*up+u, hidden h reads conv channel u*tf+h at + // frame t. With conv_out time-major (flat[u*tf + h + t*tf*up]) this + // is a plain reshape: element (h, t*up+u) = flat[h + (t*up+u)*tf]. + ggml_tensor* upsampled = ggml_reshape_2d(ctx, conv_out, tf, (int64_t)T_enc * up); // ne[0]=tf, ne[1]=T_out // ---- forward_speaker_logits: relu → Linear(tf→tf) → relu → Linear(tf→ns) → sigmoid ---- diff --git a/tests/test_diarization_parity.cpp b/tests/test_diarization_parity.cpp index 828810c..abd6ed7 100644 --- a/tests/test_diarization_parity.cpp +++ b/tests/test_diarization_parity.cpp @@ -99,10 +99,10 @@ int main() { write_npy_f32(std::string(out_dir) + "/enc_out_pk.npy", enc_out.data(), {(int64_t)d_model, (int64_t)T_enc}); - // 3. Head + // 3. Head (full offline pipeline incl. NeMo peak-normalize + length trim) std::vector probs; int n_spk = 0, T_out = 0; - head.forward(enc_out, d_model, T_enc, probs, n_spk, T_out); + m->speaker_probs(audio.samples, probs, n_spk, T_out); std::printf("probs: [%d, %d]\n", n_spk, T_out); write_npy_f32(std::string(out_dir) + "/probs_pk.npy", probs.data(), {(int64_t)n_spk, (int64_t)T_out}); From 532b0ae7666af584f09d93589b45d03be6e9a20f Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:23:53 +0000 Subject: [PATCH 10/17] test(diarization): add accuracy test against a NeMo baseline scripts/gen_diar_baseline.py runs NeMo on a clip and stores the audio, the offline and streaming speaker probabilities and the diarize() segments in a baseline GGUF. test_diarization_accuracy checks the C++ offline pipeline against it: probability max/mean diff, segment count, speakers and boundaries (20 ms), and frame-level agreement (>= 99.5%). tests/fixtures/two_speakers.wav is 23.6 s of LibriSpeech (CC BY 4.0): speaker 1272 (1272-128104-0000, -0001, from the dev-clean set) and speaker 2086 (the existing speech.wav) alternating A-B-A-B with 0.5 s gaps. NeMo finds 5 segments across the 2 speakers. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- scripts/gen_diar_baseline.py | 100 ++++++++++++++++++ tests/CMakeLists.txt | 5 +- tests/fixtures/two_speakers.wav | Bin 0 -> 755404 bytes tests/test_diarization_accuracy.cpp | 152 ++++++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 scripts/gen_diar_baseline.py create mode 100644 tests/fixtures/two_speakers.wav create mode 100644 tests/test_diarization_accuracy.cpp diff --git a/scripts/gen_diar_baseline.py b/scripts/gen_diar_baseline.py new file mode 100644 index 0000000..52977f9 --- /dev/null +++ b/scripts/gen_diar_baseline.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Dump a NeMo speaker-diarization reference to a baseline GGUF. + +Used by tests/test_diarization_accuracy.cpp to check the C++ diarization +pipeline (nvidia/Nemotron-3-Diarization and compatible Sortformer models) +against NeMo on the same audio. + +Needs a NeMo with self_attention_model='rope' support (NeMo main / >= 3.1; +NeMo 3.0 cannot instantiate Nemotron-3-Diarization). + +Stored tensors (numpy shapes; the C++ side reads them outer..inner): + +* ``audio`` ``[S]`` the 16 kHz mono clip the reference used, + so the test does not depend on a wav path +* ``offline_probs`` ``[n_spk, T]`` offline forward() speaker probabilities + (``streaming_mode=False``), one frame per + 10 ms mel frame +* ``offline_segs`` ``[N, 3]`` offline diarize() segments as + (speaker, start_s, end_s) +* ``stream_probs`` ``[n_spk, T]`` streaming forward() probabilities + (``streaming_mode=True``, the model's + own chunk / speaker-cache config) +* ``stream_segs`` ``[N, 3]`` streaming diarize() segments + +``dither`` is forced to 0 so the mel is deterministic (the C++ side has no +dither). + +Usage: + python scripts/gen_diar_baseline.py \\ + --model /path/to/Nemotron-3-Diarization.nemo \\ + --audio tests/fixtures/two_speakers.wav \\ + --output /tmp/diar_baseline.gguf +""" +import argparse +import sys + +import numpy as np + +try: + import gguf + import soundfile as sf + import torch + from nemo.collections.asr.models import SortformerEncLabelModel +except ImportError as e: # pragma: no cover - env guard + print(f"gen_diar_baseline: missing dependency: {e}", file=sys.stderr) + sys.exit(2) + + +def _segments(model, path): + out = model.diarize(audio=[path], batch_size=1) + rows = [] + for s in out[0]: + start, end, spk = s.split() + rows.append([float(spk.split("_")[-1]), float(start), float(end)]) + return np.asarray(rows, dtype=np.float32).reshape(-1, 3) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--model", required=True, help=".nemo path or HF id") + ap.add_argument("--audio", required=True, help="16 kHz mono wav") + ap.add_argument("--output", required=True) + args = ap.parse_args() + + if args.model.endswith(".nemo"): + m = SortformerEncLabelModel.restore_from(args.model, map_location="cpu") + else: + m = SortformerEncLabelModel.from_pretrained(args.model, map_location="cpu") + m.eval() + m.preprocessor.featurizer.dither = 0.0 + + y, sr = sf.read(args.audio, dtype="float32") + if y.ndim != 1 or sr != 16000: + sys.exit(f"gen_diar_baseline: {args.audio} must be 16 kHz mono (got sr={sr}, shape={y.shape})") + x = torch.from_numpy(y)[None] + n = torch.tensor([len(y)]) + + results = {} + for name, streaming in (("offline", False), ("stream", True)): + m.streaming_mode = streaming + with torch.no_grad(): + preds = m.forward(x, n) # [1, T, n_spk] + results[f"{name}_probs"] = preds[0].numpy().T.copy() # [n_spk, T] + results[f"{name}_segs"] = _segments(m, args.audio) + print(f"{name}: probs {results[name + '_probs'].shape}, " + f"{len(results[name + '_segs'])} segments") + + w = gguf.GGUFWriter(args.output, "parakeet-diar-baseline") + w.add_tensor("audio", np.ascontiguousarray(y, dtype=np.float32)) + for k, v in results.items(): + w.add_tensor(k, np.ascontiguousarray(v, dtype=np.float32)) + w.write_header_to_file() + w.write_kv_data_to_file() + w.write_tensors_to_file() + w.close() + print(f"wrote {args.output}") + + +if __name__ == "__main__": + main() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dc1f50f..cb41231 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -72,6 +72,7 @@ pk_add_test(test_capi_batch_json) pk_add_test(test_capi_ctc_logits) pk_add_test(test_diarization) pk_add_test(test_diarization_parity) +pk_add_test(test_diarization_accuracy) pk_add_test(test_diar_layer0) pk_add_test(test_diar_layer0_bisect) pk_add_test(test_diar_head_bisect) @@ -138,7 +139,7 @@ set_tests_properties(test_model_loader test_mel test_mel_gpu test_subsampling te test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json test_capi_timestamps test_capi_batch_json test_capi_ctc_logits - test_combined_offline test_streaming_diarization + test_combined_offline test_streaming_diarization test_diarization_accuracy PROPERTIES LABELS "model") # These tests read fixtures/baselines via paths relative to the project root. set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_batch test_subsampling_batch_causal test_relpos_attention test_relpos_attention_batch test_conformer test_conformer_batch @@ -155,7 +156,7 @@ set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_bat test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json test_capi_timestamps test_capi_batch_json test_capi_ctc_logits - test_combined_offline test_streaming_diarization + test_combined_offline test_streaming_diarization test_diarization_accuracy PROPERTIES WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) # Python converter check (skips with exit 77 when the venv/model are absent). diff --git a/tests/fixtures/two_speakers.wav b/tests/fixtures/two_speakers.wav new file mode 100644 index 0000000000000000000000000000000000000000..f5b37def43ac46af3613af8c610aa71651d4b096 GIT binary patch literal 755404 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

1)wVcbo%4guQXClA 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�f6GpOkkWD5bdp^aKldr7CB?Kk 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

RBse$1+L>+o8+aFUx&xw7dr{XuE0Q^J4nD5kh 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μ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

;h zZp72yi=KG^ujb&JRg9v%$cD8e9^xI1$b9hexQ~59Pn?XmBco!aS|dXH0wZ@6F&J$? z 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 99.5% +// +// The default fixture is tests/fixtures/two_speakers.wav (LibriSpeech 1272 and +// 2086 alternating, A-B-A-B), where NeMo finds 5 segments across 2 speakers. +// +// Env: +// PARAKEET_TEST_DIAR_GGUF diarization GGUF (required) +// PARAKEET_TEST_BASELINE_DIAR baseline GGUF from gen_diar_baseline.py (required) +// PARAKEET_TEST_DIAR_PROB_TOL max abs prob diff (default 0.02; F32 measures +// ~5e-3, quantized models need more headroom) +// Skips (77) when either required variable is unset. +#include "diarization.hpp" +#include "parity.hpp" + +#include +#include +#include +#include +#include + +namespace { + +struct Seg { int spk; float start, end; }; + +std::vector to_segs(const std::vector& flat) { + std::vector out; + for (size_t i = 0; i + 2 < flat.size(); i += 3) + out.push_back({(int)flat[i], flat[i + 1], flat[i + 2]}); + std::sort(out.begin(), out.end(), [](const Seg& a, const Seg& b) { + return a.start != b.start ? a.start < b.start : a.spk < b.spk; + }); + return out; +} + +// Speaker activity on a 10 ms grid: grid[s * T + t]. +std::vector to_grid(const std::vector& segs, int n_spk, int T) { + std::vector g((size_t)n_spk * T, 0); + for (const Seg& s : segs) { + if (s.spk < 0 || s.spk >= n_spk) continue; + const int a = std::max(0, (int)std::lround(s.start * 100.0f)); + const int b = std::min(T, (int)std::lround(s.end * 100.0f)); + for (int t = a; t < b; ++t) g[(size_t)s.spk * T + t] = 1; + } + return g; +} + +} // namespace + +int main() { + const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + const char* base = std::getenv("PARAKEET_TEST_BASELINE_DIAR"); + if (!gguf || !base) { + std::fprintf(stderr, "test_diarization_accuracy: PARAKEET_TEST_DIAR_GGUF and/or " + "PARAKEET_TEST_BASELINE_DIAR not set; skip\n"); + return 77; + } + const char* tol_env = std::getenv("PARAKEET_TEST_DIAR_PROB_TOL"); + const float prob_tol = tol_env ? (float)std::atof(tol_env) : 0.02f; + + auto m = pk::DiarizationModel::load(gguf); + if (!m) { std::fprintf(stderr, "load failed: %s\n", gguf); return 1; } + + std::vector audio, ref_probs, ref_segs_flat; + std::vector shape; + if (!pktest::load_baseline(base, "audio", audio, shape)) return 1; + if (!pktest::load_baseline(base, "offline_probs", ref_probs, shape)) return 1; + const int ref_spk = (int)shape[0], ref_T = (int)shape[1]; + if (!pktest::load_baseline(base, "offline_segs", ref_segs_flat, shape)) return 1; + + int fails = 0; + + // 1. Frame probabilities. + std::vector probs; + int n_spk = 0, T = 0; + m->speaker_probs(audio, probs, n_spk, T); + std::printf("probs: ours [%d, %d], NeMo [%d, %d]\n", n_spk, T, ref_spk, ref_T); + if (n_spk != ref_spk || T != ref_T) { + std::fprintf(stderr, "FAIL: probability shape mismatch\n"); + return 1; + } + double max_diff = 0.0, sum_diff = 0.0; + for (size_t i = 0; i < probs.size(); ++i) { + const double d = std::fabs((double)probs[i] - ref_probs[i]); + max_diff = std::max(max_diff, d); + sum_diff += d; + } + const double mean_diff = sum_diff / probs.size(); + std::printf("probs: max_diff=%.5f mean_diff=%.6f (tol max %.3f, mean 0.002)\n", + max_diff, mean_diff, prob_tol); + if (max_diff > prob_tol || mean_diff > 2e-3) { + std::fprintf(stderr, "FAIL: probabilities diverge from NeMo\n"); + ++fails; + } + + // 2. Segments. + const std::vector ref = to_segs(ref_segs_flat); + pk::DiarizationResult r = m->diarize_pcm(audio, 16000); + std::vector ours; + for (const auto& s : r.segments) ours.push_back({s.speaker, s.start, s.end}); + std::printf("segments: ours %zu, NeMo %zu\n", ours.size(), ref.size()); + for (size_t i = 0; i < std::max(ours.size(), ref.size()); ++i) { + const bool has_o = i < ours.size(), has_r = i < ref.size(); + std::printf(" %s spk%d %6.2f-%6.2f NeMo spk%d %6.2f-%6.2f\n", + (has_o && has_r && ours[i].spk == ref[i].spk && + std::fabs(ours[i].start - ref[i].start) <= 0.02f && + std::fabs(ours[i].end - ref[i].end) <= 0.02f) ? "ok " : "DIFF", + has_o ? ours[i].spk : -1, has_o ? ours[i].start : 0.f, has_o ? ours[i].end : 0.f, + has_r ? ref[i].spk : -1, has_r ? ref[i].start : 0.f, has_r ? ref[i].end : 0.f); + } + if (ours.size() != ref.size()) { + std::fprintf(stderr, "FAIL: segment count differs\n"); + ++fails; + } else { + for (size_t i = 0; i < ref.size(); ++i) { + if (ours[i].spk != ref[i].spk || + std::fabs(ours[i].start - ref[i].start) > 0.02f || + std::fabs(ours[i].end - ref[i].end) > 0.02f) { + std::fprintf(stderr, "FAIL: segment %zu differs\n", i); + ++fails; + } + } + } + + // 3. Frame-level agreement over frames where either side has speech. + const std::vector go = to_grid(ours, n_spk, T), gr = to_grid(ref, n_spk, T); + int active = 0, agree = 0; + for (int t = 0; t < T; ++t) { + bool any = false, same = true; + for (int s = 0; s < n_spk; ++s) { + const char a = go[(size_t)s * T + t], b = gr[(size_t)s * T + t]; + any = any || a || b; + same = same && a == b; + } + if (any) { ++active; agree += same ? 1 : 0; } + } + const double agreement = active ? (double)agree / active : 1.0; + std::printf("frame agreement: %.2f%% of %d active frames\n", 100.0 * agreement, active); + if (agreement < 0.995) { + std::fprintf(stderr, "FAIL: frame agreement below 99.5%%\n"); + ++fails; + } + + std::printf(fails ? "test_diarization_accuracy: FAIL\n" : "test_diarization_accuracy: PASS\n"); + return fails ? 1 : 0; +} From 47df4078cea49a77d9e08df468c0f815896d964d Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:43:52 +0000 Subject: [PATCH 11/17] test(diarization): drop bisect and ggml layout probes test_diar_layer0, test_diar_layer0_bisect, test_diar_head_bisect and test_diarization_parity were one-off debugging dumps with no assertions. test_3d_permute, test_cont_check and test_subpixel_check documented "ggml bugs" that are not bugs: ggml_permute takes the destination of each source axis (the inverse of torch.permute), and flash_attn_ext is documented to return [head_dim, n_head, T]. The subpixel check also asserted the wrong upsample layout. test_diarization_accuracy and test_streaming_diarization replace them with checks against NeMo. test_diarization is now labelled "model". Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- tests/CMakeLists.txt | 9 +- tests/test_3d_permute.cpp | 157 ----------- tests/test_cont_check.cpp | 64 ----- tests/test_diar_head_bisect.cpp | 189 ------------- tests/test_diar_layer0.cpp | 256 ------------------ tests/test_diar_layer0_bisect.cpp | 434 ------------------------------ tests/test_diarization_parity.cpp | 133 --------- tests/test_subpixel_check.cpp | 88 ------ 8 files changed, 1 insertion(+), 1329 deletions(-) delete mode 100644 tests/test_3d_permute.cpp delete mode 100644 tests/test_cont_check.cpp delete mode 100644 tests/test_diar_head_bisect.cpp delete mode 100644 tests/test_diar_layer0.cpp delete mode 100644 tests/test_diar_layer0_bisect.cpp delete mode 100644 tests/test_diarization_parity.cpp delete mode 100644 tests/test_subpixel_check.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cb41231..96c92e9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -71,14 +71,7 @@ pk_add_test(test_capi_timestamps) pk_add_test(test_capi_batch_json) pk_add_test(test_capi_ctc_logits) pk_add_test(test_diarization) -pk_add_test(test_diarization_parity) pk_add_test(test_diarization_accuracy) -pk_add_test(test_diar_layer0) -pk_add_test(test_diar_layer0_bisect) -pk_add_test(test_diar_head_bisect) -pk_add_test(test_cont_check) -pk_add_test(test_subpixel_check) -pk_add_test(test_3d_permute) pk_add_test(test_sas_merge) pk_add_test(test_combined_offline) pk_add_test(test_streaming_diarization) @@ -139,7 +132,7 @@ set_tests_properties(test_model_loader test_mel test_mel_gpu test_subsampling te test_transcribe_ctc test_transcribe_rnnt test_transcribe_eou test_transcribe_nemotron test_streaming_decode test_streaming_eou_reset test_streaming_nemotron test_streaming_mel test_capi test_capi_batch test_capi_stream test_capi_stream_json test_capi_timestamps test_capi_batch_json test_capi_ctc_logits - test_combined_offline test_streaming_diarization test_diarization_accuracy + test_combined_offline test_streaming_diarization test_diarization_accuracy test_diarization PROPERTIES LABELS "model") # These tests read fixtures/baselines via paths relative to the project root. set_tests_properties(test_mel test_mel_gpu test_subsampling test_subsampling_batch test_subsampling_batch_causal test_relpos_attention test_relpos_attention_batch test_conformer test_conformer_batch diff --git a/tests/test_3d_permute.cpp b/tests/test_3d_permute.cpp deleted file mode 100644 index 4090fdd..0000000 --- a/tests/test_3d_permute.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// Test: permute + cont on a 3D tensor (the subpixel case) -#include -#include -#include -#include "backend.hpp" -#include "ggml_graph.hpp" -#include "ggml.h" - -int main() { - // 3D tensor: ne=[4, 2, 3] = [T, up, tf] - // Data: flat[t + u*4 + h*8] - // h=0: [10,11,12,13, 20,21,22,23] - // h=1: [30,31,32,33, 40,41,42,43] - // h=2: [50,51,52,53, 60,61,62,63] - // - // After permute(2,0,1,3): ne=[3, 4, 2] = [tf, T, up] - // element(h, t, u) = old element(t, u, h) = flat[t + u*4 + h*8] - // - // After cont: data should be contiguous - // new_flat[h + t*3 + u*12] = old_flat[t + u*4 + h*8] - // - // After reshape_2d(3, 8): ne=[3, 8] = [tf, T_out] - // element(h, t') = new_flat[h + t'*3] - // where t' = t + u*4 = t + u*T_enc - // - // Expected: - // h=0: t'=0: t=0,u=0 → flat[0+0*4+0*8] = 10 - // t'=1: t=1,u=0 → flat[1+0+0] = 11 - // t'=2: t=2,u=0 → flat[2] = 12 - // t'=3: t=3,u=0 → flat[3] = 13 - // t'=4: t=0,u=1 → flat[0+4+0] = 20 - // t'=5: t=1,u=1 → flat[1+4] = 21 - // t'=6: t=2,u=1 → flat[2+4] = 22 - // t'=7: t=3,u=1 → flat[3+4] = 23 - - const int T_enc = 4, up = 2, tf = 3; - float data[24]; - for (int h = 0; h < tf; h++) - for (int u = 0; u < up; u++) - for (int t = 0; t < T_enc; t++) - data[t + u*T_enc + h*up*T_enc] = (h*up + u + 1) * 10 + t; - - printf("Input data: "); - for (int i = 0; i < 24; i++) printf("%.0f ", data[i]); - printf("\n\n"); - - // Test 1: permute + cont + reshape_2d - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t ne[3] = {T_enc, up, tf}; - ggml_tensor* t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 3, ne, - data, 24 * sizeof(float)); - // t: ne=[T_enc, up, tf] = [4, 2, 3] - ggml_tensor* p = ggml_permute(ctx, t, 2, 0, 1, 3); - // p: ne=[tf, T_enc, up] = [3, 4, 2] - ggml_tensor* c = ggml_cont(ctx, p); - // c: ne=[3, 4, 2], contiguous - ggml_tensor* r = ggml_reshape_2d(ctx, c, tf, T_enc * up); - // r: ne=[tf, T_out] = [3, 8] - return r; - }, out); - - printf("Test 1 (permute+cont+reshape_2d):\n"); - printf("Expected:\n"); - for (int h = 0; h < tf; h++) { - printf(" h%d: ", h); - for (int tp = 0; tp < T_enc * up; tp++) { - int u = tp / T_enc; - int t = tp % T_enc; - printf("%.0f ", data[t + u*T_enc + h*up*T_enc]); - } - printf("\n"); - } - printf("Got (out[h + t'*tf]):\n"); - for (int h = 0; h < tf; h++) { - printf(" h%d: ", h); - for (int tp = 0; tp < T_enc * up; tp++) { - printf("%.0f ", out[h + tp * tf]); - } - printf("\n"); - } - - bool pass = true; - for (int h = 0; h < tf; h++) { - for (int tp = 0; tp < T_enc * up; tp++) { - int u = tp / T_enc; - int t = tp % T_enc; - float expected = data[t + u*T_enc + h*up*T_enc]; - if (out[h + tp * tf] != expected) { pass = false; break; } - } - } - printf("Result: %s\n\n", pass ? "PASS" : "FAIL"); - } - - // Test 2: Just cont (no permute) — should be identity copy - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t ne[3] = {T_enc, up, tf}; - ggml_tensor* t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 3, ne, - data, 24 * sizeof(float)); - return ggml_cont(ctx, t); - }, out); - - printf("Test 2 (cont identity):\n"); - printf("Expected: "); - for (int i = 0; i < 24; i++) printf("%.0f ", data[i]); - printf("\nGot: "); - for (int i = 0; i < 24; i++) printf("%.0f ", out[i]); - printf("\nResult: %s\n\n", data == out.data() ? "?" : - (memcmp(data, out.data(), 24*sizeof(float)) == 0 ? "PASS" : "FAIL")); - } - - // Test 3: permute + cont only (no reshape) - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t ne[3] = {T_enc, up, tf}; - ggml_tensor* t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 3, ne, - data, 24 * sizeof(float)); - ggml_tensor* p = ggml_permute(ctx, t, 2, 0, 1, 3); - ggml_tensor* c = ggml_cont(ctx, p); - return c; - }, out); - - // After permute(2,0,1,3) + cont: ne=[3, 4, 2] - // element(h, t, u) = old_element(t, u, h) = data[t + u*4 + h*8] - // Contiguous: out[h + t*3 + u*12] - printf("Test 3 (permute+cont, no reshape):\n"); - printf("Expected (h + t*3 + u*12):\n"); - for (int h = 0; h < tf; h++) - for (int u = 0; u < up; u++) - for (int t = 0; t < T_enc; t++) { - int idx = h + t*3 + u*12; - printf(" [%d] = %.0f (h=%d,t=%d,u=%d)\n", idx, - data[t + u*4 + h*8], h, t, u); - } - printf("Got:\n"); - for (int i = 0; i < 24; i++) { - // Find which (h,t,u) this should be - // i = h + t*3 + u*12 → h = i%3, t = (i/3)%4, u = i/12 - int h = i % 3, t = (i / 3) % 4, u = i / 12; - printf(" [%d] = %.0f (should be h=%d,t=%d,u=%d → %.0f)\n", - i, out[i], h, t, u, data[t + u*4 + h*8]); - } - - bool pass = true; - for (int i = 0; i < 24; i++) { - int h = i % 3, t = (i / 3) % 4, u = i / 12; - if (out[i] != data[t + u*4 + h*8]) { pass = false; } - } - printf("Result: %s\n", pass ? "PASS" : "FAIL"); - } - - return 0; -} diff --git a/tests/test_cont_check.cpp b/tests/test_cont_check.cpp deleted file mode 100644 index 750176a..0000000 --- a/tests/test_cont_check.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// Minimal test: verify ggml_cont actually rearranges data in this backend. -#include -#include -#include "backend.hpp" -#include "ggml_graph.hpp" -#include "ggml.h" - -int main() { - // Create a simple 2x4 tensor, transpose it, cont it, and check if data - // is actually rearranged. - // Original: ne=[4, 2] = [cols, rows], data: flat[c + r*4] - // 0 1 2 3 - // 4 5 6 7 - // flat = [0, 1, 2, 3, 4, 5, 6, 7] - // - // After transpose: ne=[2, 4], strides say data should be - // 0 4 - // 1 5 - // 2 6 - // 3 7 - // But data is still flat = [0, 1, 2, 3, 4, 5, 6, 7] - // - // After cont: data should be flat = [0, 4, 1, 5, 2, 6, 3, 7] - // (reading row-by-row: row 0 = [0, 4], row 1 = [1, 5], etc.) - - float input_data[8] = {0, 1, 2, 3, 4, 5, 6, 7}; - - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t ne[2] = {4, 2}; // ne[0]=4 (cols), ne[1]=2 (rows) - ggml_tensor* t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, ne, - input_data, 8 * sizeof(float)); - // t: ne=[4, 2], data=[0,1,2,3,4,5,6,7] - - // Transpose: ne=[2, 4], data unchanged - ggml_tensor* tt = ggml_transpose(ctx, t); - // tt: ne=[2, 4], but data is still [0,1,2,3,4,5,6,7] - - // Cont: should rearrange to [0,4,1,5,2,6,3,7] - ggml_tensor* ct = ggml_cont(ctx, tt); - // ct: ne=[2, 4], data should be [0,4,1,5,2,6,3,7] - - return ct; - }, out); - - if (!ok) { - printf("FAIL: run_graph returned false\n"); - return 1; - } - - printf("Expected: 0 4 1 5 2 6 3 7\n"); - printf("Got: "); - for (int i = 0; i < 8; i++) printf("%.0f ", out[i]); - printf("\n"); - - float expected[8] = {0, 4, 1, 5, 2, 6, 3, 7}; - bool pass = true; - for (int i = 0; i < 8; i++) { - if (out[i] != expected[i]) { pass = false; break; } - } - - printf("Result: %s\n", pass ? "PASS - cont works" : "FAIL - cont is no-op"); - return pass ? 0 : 1; -} diff --git a/tests/test_diar_head_bisect.cpp b/tests/test_diar_head_bisect.cpp deleted file mode 100644 index 211fea0..0000000 --- a/tests/test_diar_head_bisect.cpp +++ /dev/null @@ -1,189 +0,0 @@ -// test_diar_head_bisect.cpp — dump diarization head intermediates stage by stage. -#include "diarization.hpp" -#include "mel.hpp" -#include "model_loader.hpp" -#include "backend.hpp" -#include "audio_io.hpp" -#include "diarization_encoder.hpp" -#include "diarization_head.hpp" -#include "ggml_graph.hpp" -#include "ggml.h" - -#include -#include -#include -#include - -static void write_npy_f32(const std::string& path, const float* data, - const std::vector& shape) { - std::string magic = "\x93NUMPY"; - uint8_t version[2] = {1, 0}; - std::string dict = "{'descr': ' 0) dict += ", "; - dict += std::to_string(shape[i]); - } - if (shape.size() == 1) dict += ","; - dict += "), }"; - int overhead = 10; - int target = overhead + dict.size() + 1; - int padded = ((target + 63) / 64) * 64; - int n_pad = padded - target; - uint16_t hlen = (uint16_t)(dict.size() + n_pad + 1); - std::ofstream f(path, std::ios::binary); - f.write(magic.data(), 6); - f.write((char*)version, 2); - f.write((char*)&hlen, 2); - f.write(dict.data(), (std::streamsize)dict.size()); - for (int i = 0; i < n_pad; ++i) f.write(" ", 1); - f.write("\n", 1); - size_t n = 1; - for (auto s : shape) n *= s; - f.write((const char*)data, (std::streamsize)(n * sizeof(float))); -} - -int main() { - const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); - const char* wav_path = std::getenv("PARAKEET_TEST_DIAR_WAV"); - const char* out_dir = std::getenv("PARAKEET_TEST_DIAR_OUT"); - if (!gguf || !wav_path || !out_dir) return 77; - - auto m = pk::DiarizationModel::load(gguf); - if (!m) return 1; - pk::Audio audio; - if (!pk::load_audio_16k_mono(wav_path, audio)) return 1; - - const pk::ModelLoader& ml = m->loader(); - pk::MelFrontend mel(ml); - pk::DiarizationEncoder encoder(ml); - - std::vector feats; - int n_mels = 0, T = 0; - mel.compute(audio.samples, feats, n_mels, T); - - std::vector enc_out; - int d_model = 0, T_enc = 0; - encoder.forward(feats, n_mels, T, enc_out, d_model, T_enc); - - const auto& cfg = ml.config(); - int tf = (int)cfg.diarization.tf_d_model; - int n_spk = (int)cfg.diarization.n_speakers; - int up = (int)cfg.diarization.upsample_factor; - int T_out = T_enc * up; - - pk::ensure_weights_realized(ml); - - // Stage 1: encoder_proj output - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t xt_ne[2] = {T_enc, d_model}; - ggml_tensor* xt = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, xt_ne, - enc_out.data(), (size_t)d_model * T_enc * sizeof(float)); - ggml_tensor* ep_w = ml.tensor("sortformer_modules.encoder_proj.weight"); - ggml_tensor* W_proj = ggml_reshape_2d(ctx, ep_w, d_model, tf); - ggml_tensor* xt_t = ggml_cont(ctx, ggml_transpose(ctx, xt)); - ggml_tensor* proj = ggml_mul_mat(ctx, W_proj, xt_t); - ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); - if (ep_b) proj = ggml_add(ctx, proj, ep_b); - // proj: ne[0]=tf, ne[1]=T_enc - // Transpose to ne[0]=T_enc, ne[1]=tf to match PyTorch [T, tf] - proj = ggml_cont(ctx, ggml_transpose(ctx, proj)); - return proj; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/proj_pk.npy", out.data(), - {(int64_t)T_enc, (int64_t)tf}); - printf("proj: [%d, %d] first3: %.4f %.4f %.4f\n", T_enc, tf, out[0], out[1], out[2]); - } - - // Stage 2: conv output (pre-bias, pre-reshape) - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t xt_ne[2] = {T_enc, d_model}; - ggml_tensor* xt = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, xt_ne, - enc_out.data(), (size_t)d_model * T_enc * sizeof(float)); - ggml_tensor* ep_w = ml.tensor("sortformer_modules.encoder_proj.weight"); - ggml_tensor* W_proj = ggml_reshape_2d(ctx, ep_w, d_model, tf); - ggml_tensor* xt_t = ggml_cont(ctx, ggml_transpose(ctx, xt)); - ggml_tensor* proj = ggml_mul_mat(ctx, W_proj, xt_t); - ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); - if (ep_b) proj = ggml_add(ctx, proj, ep_b); - - // conv input: ne[0]=T_enc, ne[1]=tf, ne[2]=1 - ggml_tensor* conv_in = ggml_cont(ctx, ggml_transpose(ctx, proj)); - conv_in = ggml_reshape_3d(ctx, conv_in, T_enc, tf, 1); - - ggml_tensor* spk_w = ml.tensor("sortformer_modules.subpixel_upsample.weight"); - spk_w = ggml_cast(ctx, spk_w, GGML_TYPE_F16); - conv_in = ggml_cast(ctx, conv_in, GGML_TYPE_F16); - - ggml_tensor* conv_out = ggml_conv_1d(ctx, spk_w, conv_in, 1, 1, 1); - // conv_out: ne[0]=T_enc, ne[1]=tf*up, ne[2]=1 - // Reshape to 2D and transpose to [tf*up, T_enc] - conv_out = ggml_reshape_2d(ctx, conv_out, T_enc, tf * up); - conv_out = ggml_cont(ctx, ggml_transpose(ctx, conv_out)); - // ne[0]=tf*up, ne[1]=T_enc - ggml_tensor* spk_b = ml.tensor("sortformer_modules.subpixel_upsample.bias"); - if (spk_b) conv_out = ggml_add(ctx, conv_out, spk_b); - // Transpose to [T_enc, tf*up] to match PyTorch [T, OC] - conv_out = ggml_cont(ctx, ggml_transpose(ctx, conv_out)); - return conv_out; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/conv_pk.npy", out.data(), - {(int64_t)T_enc, (int64_t)(tf * up)}); - printf("conv: [%d, %d] first3: %.4f %.4f %.4f\n", T_enc, tf*up, out[0], out[1], out[2]); - } - - // Stage 3: upsampled (after subpixel reshape) - // Matches diarization_head.cpp: add bias directly (no transpose), then subpixel reshape - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t xt_ne[2] = {T_enc, d_model}; - ggml_tensor* xt = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, xt_ne, - enc_out.data(), (size_t)d_model * T_enc * sizeof(float)); - ggml_tensor* ep_w = ml.tensor("sortformer_modules.encoder_proj.weight"); - ggml_tensor* W_proj = ggml_reshape_2d(ctx, ep_w, d_model, tf); - ggml_tensor* xt_t = ggml_cont(ctx, ggml_transpose(ctx, xt)); - ggml_tensor* proj = ggml_mul_mat(ctx, W_proj, xt_t); - ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); - if (ep_b) proj = ggml_add(ctx, proj, ep_b); - - ggml_tensor* conv_in = ggml_cont(ctx, ggml_transpose(ctx, proj)); - conv_in = ggml_reshape_3d(ctx, conv_in, T_enc, tf, 1); - - ggml_tensor* spk_w = ml.tensor("sortformer_modules.subpixel_upsample.weight"); - spk_w = ggml_cast(ctx, spk_w, GGML_TYPE_F16); - conv_in = ggml_cast(ctx, conv_in, GGML_TYPE_F16); - - ggml_tensor* conv_out = ggml_conv_1d(ctx, spk_w, conv_in, 1, 1, 1); - // conv_out: ne=[T_enc, tf*up, 1], data: flat[t + c*T_enc] - conv_out = ggml_reshape_2d(ctx, conv_out, T_enc, tf * up); - - // Add bias directly (no transpose) - ggml_tensor* spk_b = ml.tensor("sortformer_modules.subpixel_upsample.bias"); - if (spk_b) { - ggml_tensor* spk_b_2d = ggml_reshape_2d(ctx, spk_b, 1, tf * up); - conv_out = ggml_add(ctx, conv_out, spk_b_2d); - } - - // Subpixel reshape (matches head code) - ggml_tensor* upsampled = ggml_reshape_3d(ctx, conv_out, T_enc, up, tf); - upsampled = ggml_reshape_2d(ctx, upsampled, T_enc * up, tf); - upsampled = ggml_cont(ctx, ggml_transpose(ctx, upsampled)); - // ne[0]=tf, ne[1]=T_out - // Transpose to [T_out, tf] to match PyTorch - upsampled = ggml_cont(ctx, ggml_transpose(ctx, upsampled)); - return upsampled; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/up_pk.npy", out.data(), - {(int64_t)T_out, (int64_t)tf}); - printf("up: [%d, %d] first3: %.4f %.4f %.4f\n", T_out, tf, out[0], out[1], out[2]); - } - - return 0; -} diff --git a/tests/test_diar_layer0.cpp b/tests/test_diar_layer0.cpp deleted file mode 100644 index dfc7fb2..0000000 --- a/tests/test_diar_layer0.cpp +++ /dev/null @@ -1,256 +0,0 @@ -// test_diar_layer0.cpp — dump intermediate outputs after embed_norm and layer 0 -// for comparison with the PyTorch reference. -#include "diarization.hpp" -#include "mel.hpp" -#include "model_loader.hpp" -#include "backend.hpp" -#include "audio_io.hpp" -#include "diarization_encoder.hpp" -#include "diarization_head.hpp" -#include "ggml_graph.hpp" -#include "graph_builder.hpp" -#include "ggml.h" - -#include -#include -#include -#include -#include - -static void write_npy_f32(const std::string& path, const float* data, - const std::vector& shape) { - std::string magic = "\x93NUMPY"; - uint8_t version[2] = {1, 0}; - std::string dict = "{'descr': ' 0) dict += ", "; - dict += std::to_string(shape[i]); - } - if (shape.size() == 1) dict += ","; - dict += "), }"; - int overhead = 10; - int target = overhead + dict.size() + 1; - int padded = ((target + 63) / 64) * 64; - int n_pad = padded - target; - uint16_t hlen = (uint16_t)(dict.size() + n_pad + 1); - std::ofstream f(path, std::ios::binary); - f.write(magic.data(), 6); - f.write((char*)version, 2); - f.write((char*)&hlen, 2); - f.write(dict.data(), (std::streamsize)dict.size()); - for (int i = 0; i < n_pad; ++i) f.write(" ", 1); - f.write("\n", 1); - size_t n = 1; - for (auto s : shape) n *= s; - f.write((const char*)data, (std::streamsize)(n * sizeof(float))); -} - -int main() { - const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); - const char* wav_path = std::getenv("PARAKEET_TEST_DIAR_WAV"); - const char* out_dir = std::getenv("PARAKEET_TEST_DIAR_OUT"); - if (!gguf || !wav_path || !out_dir) return 77; - - auto m = pk::DiarizationModel::load(gguf); - if (!m) return 1; - - pk::Audio audio; - if (!pk::load_audio_16k_mono(wav_path, audio)) return 1; - - const pk::ModelLoader& ml = m->loader(); - pk::MelFrontend mel(ml); - - std::vector feats; - int n_mels = 0, T = 0; - mel.compute(audio.samples, feats, n_mels, T); - - // Now build the graph manually up to embed_norm + layer 0 - const auto& cfg = ml.config(); - int d_model = (int)cfg.d_model; - int n_heads = (int)cfg.n_heads; - int head_dim = d_model / n_heads; - int n_layers = (int)cfg.n_layers; - int ff_dim = (int)cfg.ff_dim; - int factor = (int)cfg.subsampling_factor; - int n_mels_cfg = (int)cfg.n_mels; - float ln_eps = 1e-5f; - int n_rot = head_dim; // rotary_fraction=1.0 - - int pad = (factor - (T % factor)) % factor; - int T_padded = T + pad; - int Tp = T_padded / factor; - - std::vector positions(Tp); - for (int i = 0; i < Tp; ++i) positions[i] = i; - - pk::ensure_weights_realized(ml); - pk::GraphInputPool pool; - - // Output: after embed_norm [Tp, d_model] (channels-last, like PyTorch) - std::vector embed_norm_out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - // FeatureStacking - int64_t mel_ne[2] = {T_padded, n_mels_cfg}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); - for (int mm = 0; mm < n_mels_cfg; ++mm) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); // [d_model, Tp] - - // embed_norm - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - x = ggml_norm(ctx, x, ln_eps); - x = ggml_mul(ctx, x, g); - x = ggml_add(ctx, x, b); - // x is [d_model, Tp] (channels-first in ggml) - // PyTorch has [Tp, d_model] (channels-last) - // Transpose for comparison - x = ggml_cont(ctx, ggml_transpose(ctx, x)); - return x; - }, embed_norm_out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/embed_norm_pk.npy", - embed_norm_out.data(), {(int64_t)Tp, (int64_t)d_model}); - std::printf("embed_norm: [%d, %d]\n", Tp, d_model); - std::printf(" first 5: "); - for (int i = 0; i < 5; ++i) std::printf("%.4f ", embed_norm_out[i]); - std::printf("\n"); - - // Layer 0 full - std::vector layer0_out; - ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - // FeatureStacking + embed_norm - int64_t mel_ne[2] = {T_padded, n_mels_cfg}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); - for (int mm = 0; mm < n_mels_cfg; ++mm) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); - - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - x = ggml_norm(ctx, x, ln_eps); - x = ggml_mul(ctx, x, g); - x = ggml_add(ctx, x, b); - } - // x: [d_model, Tp] - - // Position tensor - int64_t pos_ne[1] = {Tp}; - ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, - positions.data(), (size_t)Tp * sizeof(int32_t)); - - // Layer 0 - std::string base = "encoder.layers.0."; - - // norm1 - { - ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_mul(ctx, h, ng); - h = ggml_add(ctx, h, nb); - - // QKV - ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); - ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); // [1536, Tp] - qkv = ggml_cont(ctx, qkv); - qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); // [512, 3, Tp] - - size_t qkv_ts = (size_t)3 * d_model * sizeof(float); - ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); - ggml_tensor* k = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)d_model * sizeof(float)); - ggml_tensor* v = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)2 * d_model * sizeof(float)); - - q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); - k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), head_dim, n_heads, Tp); - v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), head_dim, n_heads, Tp); - - // RoPE - q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, - 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, - 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - - // Manual attention: permute to [hd, Tp, H] - q = ggml_permute(ctx, q, 0, 2, 1, 3); - k = ggml_permute(ctx, k, 0, 2, 1, 3); - v = ggml_permute(ctx, v, 0, 2, 1, 3); - - // scores = mul_mat(k, q) → [Tp, Tp, H] - ggml_tensor* scores = ggml_mul_mat(ctx, k, q); - float attn_scale = 1.0f / std::sqrt((float)head_dim); - scores = ggml_scale(ctx, scores, attn_scale); - scores = ggml_soft_max(ctx, scores); - - // out = mul_mat(v_t, scores) - ggml_tensor* v_t = ggml_permute(ctx, v, 1, 0, 2, 3); // [Tp, hd, H] - v_t = ggml_cont(ctx, v_t); - ggml_tensor* attn_out = ggml_mul_mat(ctx, v_t, scores); // [Tp, hd, H] - attn_out = ggml_permute(ctx, attn_out, 1, 0, 2, 3); // [hd, Tp, H] - attn_out = ggml_cont(ctx, attn_out); - ggml_tensor* attn = ggml_reshape_2d(ctx, attn_out, (int64_t)d_model, (int64_t)Tp); - - // out_proj - ggml_tensor* op_w = pk::clone_weight(ctx, ml, (base + "attn.out_proj.weight").c_str()); - attn = ggml_mul_mat(ctx, op_w, attn); - ggml_tensor* op_b = pk::clone_weight_opt(ctx, ml, (base + "attn.out_proj.bias").c_str()); - if (op_b) attn = ggml_add(ctx, attn, op_b); - - // Residual - x = ggml_add(ctx, x, attn); - } - - // FFN - { - ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm2.weight").c_str()); - ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm2.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_mul(ctx, h, ng); - h = ggml_add(ctx, h, nb); - - ggml_tensor* f0_w = pk::clone_weight(ctx, ml, (base + "ffn.net.0.weight").c_str()); - h = ggml_mul_mat(ctx, f0_w, h); - ggml_tensor* f0_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.0.bias").c_str()); - if (f0_b) h = ggml_add(ctx, h, f0_b); - h = ggml_gelu(ctx, h); - - ggml_tensor* f3_w = pk::clone_weight(ctx, ml, (base + "ffn.net.3.weight").c_str()); - h = ggml_mul_mat(ctx, f3_w, h); - ggml_tensor* f3_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.3.bias").c_str()); - if (f3_b) h = ggml_add(ctx, h, f3_b); - - x = ggml_add(ctx, x, h); - } - - // x is [d_model, Tp] (ne[0]=d_model, ne[1]=Tp) - // The output flat buffer is time-major: [d0_t0, d1_t0, ..., d511_t0, d0_t1, ...] - // = flat[t * d_model + d] - // PyTorch has [Tp, d_model] = flat[t * d_model + d] — same! - // So NO transpose needed. Just return x directly. - return x; - }, layer0_out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/layer0_pk.npy", - layer0_out.data(), {(int64_t)Tp, (int64_t)d_model}); - std::printf("layer0: [%d, %d]\n", Tp, d_model); - std::printf(" first 5: "); - for (int i = 0; i < 5; ++i) std::printf("%.4f ", layer0_out[i]); - std::printf("\n"); - - return 0; -} diff --git a/tests/test_diar_layer0_bisect.cpp b/tests/test_diar_layer0_bisect.cpp deleted file mode 100644 index 56e0a4a..0000000 --- a/tests/test_diar_layer0_bisect.cpp +++ /dev/null @@ -1,434 +0,0 @@ -// test_diar_layer0_bisect.cpp — dump layer-0 intermediates stage by stage -// for comparison with PyTorch reference (dump_layer0_ref.py). -#include "diarization.hpp" -#include "mel.hpp" -#include "model_loader.hpp" -#include "backend.hpp" -#include "audio_io.hpp" -#include "diarization_encoder.hpp" -#include "diarization_head.hpp" -#include "ggml_graph.hpp" -#include "graph_builder.hpp" -#include "ggml.h" - -#include -#include -#include -#include -#include - -static void write_npy_f32(const std::string& path, const float* data, - const std::vector& shape) { - std::string magic = "\x93NUMPY"; - uint8_t version[2] = {1, 0}; - std::string dict = "{'descr': ' 0) dict += ", "; - dict += std::to_string(shape[i]); - } - if (shape.size() == 1) dict += ","; - dict += "), }"; - int overhead = 10; - int target = overhead + dict.size() + 1; - int padded = ((target + 63) / 64) * 64; - int n_pad = padded - target; - uint16_t hlen = (uint16_t)(dict.size() + n_pad + 1); - std::ofstream f(path, std::ios::binary); - f.write(magic.data(), 6); - f.write((char*)version, 2); - f.write((char*)&hlen, 2); - f.write(dict.data(), (std::streamsize)dict.size()); - for (int i = 0; i < n_pad; ++i) f.write(" ", 1); - f.write("\n", 1); - size_t n = 1; - for (auto s : shape) n *= s; - f.write((const char*)data, (std::streamsize)(n * sizeof(float))); -} - -int main() { - const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); - const char* wav_path = std::getenv("PARAKEET_TEST_DIAR_WAV"); - const char* out_dir = std::getenv("PARAKEET_TEST_DIAR_OUT"); - if (!gguf || !wav_path || !out_dir) return 77; - - auto m = pk::DiarizationModel::load(gguf); - if (!m) return 1; - - pk::Audio audio; - if (!pk::load_audio_16k_mono(wav_path, audio)) return 1; - - const pk::ModelLoader& ml = m->loader(); - pk::MelFrontend mel(ml); - - std::vector feats; - int n_mels = 0, T = 0; - mel.compute(audio.samples, feats, n_mels, T); - - const auto& cfg = ml.config(); - int d_model = (int)cfg.d_model; - int n_heads = (int)cfg.n_heads; - int head_dim = d_model / n_heads; - int factor = (int)cfg.subsampling_factor; - int n_mels_cfg = (int)cfg.n_mels; - float ln_eps = 1e-5f; - int n_rot = head_dim; - - int pad = (factor - (T % factor)) % factor; - int T_padded = T + pad; - int Tp = T_padded / factor; - - std::vector positions(Tp); - for (int i = 0; i < Tp; ++i) positions[i] = i; - - pk::ensure_weights_realized(ml); - pk::GraphInputPool pool; - - // ---- Stage 1: norm1 output ---- - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t mel_ne[2] = {T_padded, n_mels_cfg}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); - for (int mm = 0; mm < n_mels_cfg; ++mm) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); - - // embed_norm - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - x = ggml_norm(ctx, x, ln_eps); - x = ggml_mul(ctx, x, g); - x = ggml_add(ctx, x, b); - } - - // norm1 - std::string base = "encoder.layers.0."; - ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_mul(ctx, h, ng); - h = ggml_add(ctx, h, nb); - // h is [d_model, Tp] in ggml. PyTorch norm1 is [Tp, d_model]. - // ggml flat: [t * d_model + d] = same as PyTorch [t, d] row-major. - return h; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/l0_norm1_pk.npy", out.data(), - {(int64_t)Tp, (int64_t)d_model}); - printf("norm1: [Tp=%d, d=%d] first5: ", Tp, d_model); - for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); - printf("\n"); - } - - // ---- Stage 2: QKV ---- - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t mel_ne[2] = {T_padded, n_mels_cfg}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); - for (int mm = 0; mm < n_mels_cfg; ++mm) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - x = ggml_norm(ctx, x, ln_eps); - x = ggml_mul(ctx, x, g); - x = ggml_add(ctx, x, b); - } - std::string base = "encoder.layers.0."; - ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_mul(ctx, h, ng); - h = ggml_add(ctx, h, nb); - - // QKV - ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); - ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); // [3*d, Tp] - qkv = ggml_cont(ctx, qkv); - // PyTorch qkv: [B, T, 3*D] → flat[t * 3*D + j] - // ggml: [3*d, Tp] → flat[t * 3*d + j] — same! - return qkv; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/l0_qkv_pk.npy", out.data(), - {(int64_t)Tp, (int64_t)(3 * d_model)}); - printf("qkv: [Tp=%d, 3d=%d] first5: ", Tp, 3*d_model); - for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); - printf("\n"); - } - - // ---- Stage 3: Q pre-RoPE ---- - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t mel_ne[2] = {T_padded, n_mels_cfg}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); - for (int mm = 0; mm < n_mels_cfg; ++mm) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - x = ggml_norm(ctx, x, ln_eps); - x = ggml_mul(ctx, x, g); - x = ggml_add(ctx, x, b); - } - std::string base = "encoder.layers.0."; - ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_mul(ctx, h, ng); - h = ggml_add(ctx, h, nb); - - ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); - ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); - qkv = ggml_cont(ctx, qkv); - qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); - size_t qkv_ts = (size_t)3 * d_model * sizeof(float); - ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); - q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); - // q: [hd, H, Tp] in ggml - // PyTorch q: [H, T, hd] — flat[t * H * hd + h * hd + d] = [t * d_model + h * hd + d] - // ggml [hd, H, Tp]: flat[t * H * hd + h * hd + d] — same! - return q; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/l0_q_pk.npy", out.data(), - {(int64_t)Tp, (int64_t)d_model}); - printf("q_pre_rope: [Tp=%d, d=%d] first5: ", Tp, d_model); - for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); - printf("\n"); - } - - // ---- Stage 4: Q post-RoPE ---- - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t mel_ne[2] = {T_padded, n_mels_cfg}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); - for (int mm = 0; mm < n_mels_cfg; ++mm) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - x = ggml_norm(ctx, x, ln_eps); - x = ggml_mul(ctx, x, g); - x = ggml_add(ctx, x, b); - } - std::string base = "encoder.layers.0."; - ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_mul(ctx, h, ng); - h = ggml_add(ctx, h, nb); - - int64_t pos_ne[1] = {Tp}; - ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, - positions.data(), (size_t)Tp * sizeof(int32_t)); - - ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); - ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); - qkv = ggml_cont(ctx, qkv); - qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); - size_t qkv_ts = (size_t)3 * d_model * sizeof(float); - ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); - q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); - q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, - 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - return q; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/l0_q_rot_pk.npy", out.data(), - {(int64_t)Tp, (int64_t)d_model}); - printf("q_post_rope: [Tp=%d, d=%d] first5: ", Tp, d_model); - for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); - printf("\n"); - } - - // ---- Stage 5: attention output (pre out_proj) ---- - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t mel_ne[2] = {T_padded, n_mels_cfg}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); - for (int mm = 0; mm < n_mels_cfg; ++mm) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - x = ggml_norm(ctx, x, ln_eps); - x = ggml_mul(ctx, x, g); - x = ggml_add(ctx, x, b); - } - std::string base = "encoder.layers.0."; - ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_mul(ctx, h, ng); - h = ggml_add(ctx, h, nb); - - int64_t pos_ne[1] = {Tp}; - ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, - positions.data(), (size_t)Tp * sizeof(int32_t)); - - ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); - ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); - qkv = ggml_cont(ctx, qkv); - qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); - size_t qkv_ts = (size_t)3 * d_model * sizeof(float); - ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); - ggml_tensor* k = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)d_model * sizeof(float)); - ggml_tensor* v = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)2 * d_model * sizeof(float)); - q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); - k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), head_dim, n_heads, Tp); - v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), head_dim, n_heads, Tp); - q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, - 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, - 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - - // Manual attention (same as test_diar_layer0.cpp) - q = ggml_permute(ctx, q, 0, 2, 1, 3); // [hd, Tp, H] - k = ggml_permute(ctx, k, 0, 2, 1, 3); - v = ggml_permute(ctx, v, 0, 2, 1, 3); - ggml_tensor* scores = ggml_mul_mat(ctx, k, q); // [Tp, Tp, H] - float attn_scale = 1.0f / std::sqrt((float)head_dim); - scores = ggml_scale(ctx, scores, attn_scale); - scores = ggml_soft_max(ctx, scores); - ggml_tensor* v_t = ggml_permute(ctx, v, 1, 0, 2, 3); // [Tp, hd, H] - v_t = ggml_cont(ctx, v_t); - ggml_tensor* attn_out = ggml_mul_mat(ctx, v_t, scores); // [Tp, hd, H] -> [hd, Tp, H]? - // Actually mul_mat(v_t [Tp,hd,H], scores [Tp,Tp,H]) gives [hd, Tp, H] - attn_out = ggml_permute(ctx, attn_out, 1, 0, 2, 3); // hmm - - // Let's just use flash_attn_ext instead - (void)attn_out; // unused - float scale = 1.0f / std::sqrt((float)head_dim); - ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, scale, 0.0f, 0.0f); - attn = ggml_permute(ctx, attn, 0, 2, 1, 3); // [hd, H, Tp, 1] - attn = ggml_cont(ctx, attn); - attn = ggml_reshape_2d(ctx, attn, (int64_t)d_model, (int64_t)Tp); - // attn: [d_model, Tp], flat[t * d + d] = same as PyTorch [Tp, d_model] - return attn; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/l0_attn_pk.npy", out.data(), - {(int64_t)Tp, (int64_t)d_model}); - printf("attn: [Tp=%d, d=%d] first5: ", Tp, d_model); - for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); - printf("\n"); - } - - // ---- Stage 6: attn_out (after out_proj) ---- - { - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t mel_ne[2] = {T_padded, n_mels_cfg}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels_cfg * T_padded); - for (int mm = 0; mm < n_mels_cfg; ++mm) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)mm * T_padded + t] = feats[(size_t)mm * T + t]; - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels_cfg * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels_cfg * factor, Tp); - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - x = ggml_norm(ctx, x, ln_eps); - x = ggml_mul(ctx, x, g); - x = ggml_add(ctx, x, b); - } - std::string base = "encoder.layers.0."; - ggml_tensor* ng = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* nb = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_mul(ctx, h, ng); - h = ggml_add(ctx, h, nb); - - int64_t pos_ne[1] = {Tp}; - ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, - positions.data(), (size_t)Tp * sizeof(int32_t)); - - ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); - ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); - qkv = ggml_cont(ctx, qkv); - qkv = ggml_reshape_3d(ctx, qkv, d_model, 3, Tp); - size_t qkv_ts = (size_t)3 * d_model * sizeof(float); - ggml_tensor* q = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, 0); - ggml_tensor* k = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)d_model * sizeof(float)); - ggml_tensor* v = ggml_view_2d(ctx, qkv, d_model, Tp, qkv_ts, (size_t)2 * d_model * sizeof(float)); - q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), head_dim, n_heads, Tp); - k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), head_dim, n_heads, Tp); - v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), head_dim, n_heads, Tp); - q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, - 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, GGML_ROPE_TYPE_NEOX, 0, - 10000.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - q = ggml_permute(ctx, q, 0, 2, 1, 3); - k = ggml_permute(ctx, k, 0, 2, 1, 3); - v = ggml_permute(ctx, v, 0, 2, 1, 3); - float scale = 1.0f / std::sqrt((float)head_dim); - ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, scale, 0.0f, 0.0f); - attn = ggml_permute(ctx, attn, 0, 2, 1, 3); - attn = ggml_cont(ctx, attn); - attn = ggml_reshape_2d(ctx, attn, (int64_t)d_model, (int64_t)Tp); - ggml_tensor* op_w = pk::clone_weight(ctx, ml, (base + "attn.out_proj.weight").c_str()); - attn = ggml_mul_mat(ctx, op_w, attn); - ggml_tensor* op_b = pk::clone_weight_opt(ctx, ml, (base + "attn.out_proj.bias").c_str()); - if (op_b) attn = ggml_add(ctx, attn, op_b); - return attn; - }, out); - assert(ok); - write_npy_f32(std::string(out_dir) + "/l0_attn_out_pk.npy", out.data(), - {(int64_t)Tp, (int64_t)d_model}); - printf("attn_out: [Tp=%d, d=%d] first5: ", Tp, d_model); - for (int i = 0; i < 5; ++i) printf("%.4f ", out[i]); - printf("\n"); - } - - return 0; -} diff --git a/tests/test_diarization_parity.cpp b/tests/test_diarization_parity.cpp deleted file mode 100644 index abd6ed7..0000000 --- a/tests/test_diarization_parity.cpp +++ /dev/null @@ -1,133 +0,0 @@ -// test_diarization_parity.cpp — dump intermediate outputs for parity comparison. -// -// Env: PARAKEET_TEST_DIAR_GGUF (required), PARAKEET_TEST_DIAR_WAV (required), -// PARAKEET_TEST_DIAR_OUT (required, output directory). -#include "diarization.hpp" -#include "mel.hpp" -#include "model_loader.hpp" -#include "backend.hpp" -#include "audio_io.hpp" -#include "diarization_encoder.hpp" -#include "diarization_head.hpp" - -#include -#include -#include -#include -#include - -static void write_npy_f32(const std::string& path, const float* data, - const std::vector& shape) { - // NumPy v1 format: magic(6) + version(2) + header_len(2) + header - // Header = dict_string + padding_spaces + \n, total padded to multiple of 64. - std::string magic = "\x93NUMPY"; - uint8_t version[2] = {1, 0}; - std::string dict = "{'descr': ' 0) dict += ", "; - dict += std::to_string(shape[i]); - } - if (shape.size() == 1) dict += ","; - dict += "), }"; - // header = dict + padding + \n, total must be multiple of 64 - // total_file = 6 (magic) + 2 (version) + 2 (hlen) + header_len - // We want header_len such that 10 + header_len is multiple of 64. - // header_len = dict.size() + n_pad + 1(\n) - int overhead = 10; // magic + version + hlen - int target = overhead + dict.size() + 1; // +1 for \n - int padded = ((target + 63) / 64) * 64; - int n_pad = padded - target; - uint16_t hlen = (uint16_t)(dict.size() + n_pad + 1); - - std::ofstream f(path, std::ios::binary); - f.write(magic.data(), 6); - f.write((char*)version, 2); - f.write((char*)&hlen, 2); - f.write(dict.data(), (std::streamsize)dict.size()); - for (int i = 0; i < n_pad; ++i) f.write(" ", 1); - f.write("\n", 1); - size_t n = 1; - for (auto s : shape) n *= s; - f.write((const char*)data, (std::streamsize)(n * sizeof(float))); -} - -int main() { - const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); - const char* wav_path = std::getenv("PARAKEET_TEST_DIAR_WAV"); - const char* out_dir = std::getenv("PARAKEET_TEST_DIAR_OUT"); - if (!gguf || !wav_path || !out_dir) { - std::fprintf(stderr, "PARAKEET_TEST_DIAR_GGUF, PARAKEET_TEST_DIAR_WAV, " - "PARAKEET_TEST_DIAR_OUT all required\n"); - return 77; - } - - // Load model - std::unique_ptr m = pk::DiarizationModel::load(gguf); - if (!m) { - std::fprintf(stderr, "DiarizationModel::load failed\n"); - return 1; - } - - // Load audio - pk::Audio audio; - if (!pk::load_audio_16k_mono(wav_path, audio)) { - std::fprintf(stderr, "failed to load audio: %s\n", wav_path); - return 1; - } - std::printf("audio: %zu samples, %.2fs\n", audio.samples.size(), - (float)audio.samples.size() / 16000.0f); - - // Build components from the loader (same as DiarizationModel::run) - const pk::ModelLoader& ml = m->loader(); - pk::MelFrontend mel(ml); - pk::DiarizationEncoder encoder(ml); - pk::DiarizationHead head(ml); - - // 1. Mel - std::vector feats; - int n_mels = 0, T = 0; - mel.compute(audio.samples, feats, n_mels, T); - std::printf("mel: [%d, %d]\n", n_mels, T); - write_npy_f32(std::string(out_dir) + "/mel_pk.npy", feats.data(), - {(int64_t)n_mels, (int64_t)T}); - - // 2. Encoder - std::vector enc_out; - int d_model = 0, T_enc = 0; - encoder.forward(feats, n_mels, T, enc_out, d_model, T_enc); - std::printf("enc_out: [%d, %d]\n", d_model, T_enc); - write_npy_f32(std::string(out_dir) + "/enc_out_pk.npy", enc_out.data(), - {(int64_t)d_model, (int64_t)T_enc}); - - // 3. Head (full offline pipeline incl. NeMo peak-normalize + length trim) - std::vector probs; - int n_spk = 0, T_out = 0; - m->speaker_probs(audio.samples, probs, n_spk, T_out); - std::printf("probs: [%d, %d]\n", n_spk, T_out); - write_npy_f32(std::string(out_dir) + "/probs_pk.npy", probs.data(), - {(int64_t)n_spk, (int64_t)T_out}); - - // 4. Segments (reuse the model's diarize_path which does the full pipeline) - pk::DiarizationResult r = m->diarize_path(wav_path); - std::printf("segments: %zu\n", r.segments.size()); - - std::ofstream sf(std::string(out_dir) + "/segments_pk.json"); - sf << "[\n"; - for (size_t i = 0; i < r.segments.size(); ++i) { - sf << " {\"speaker\": " << r.segments[i].speaker - << ", \"start\": " << r.segments[i].start - << ", \"end\": " << r.segments[i].end << "}"; - if (i + 1 < r.segments.size()) sf << ","; - sf << "\n"; - } - sf << "]\n"; - - for (size_t i = 0; i < r.segments.size() && i < 20; ++i) { - std::printf(" spk %d: %.2f - %.2f\n", - r.segments[i].speaker, - r.segments[i].start, r.segments[i].end); - } - - std::printf("\nDone. Outputs in %s/\n", out_dir); - return 0; -} diff --git a/tests/test_subpixel_check.cpp b/tests/test_subpixel_check.cpp deleted file mode 100644 index 5c4bc40..0000000 --- a/tests/test_subpixel_check.cpp +++ /dev/null @@ -1,88 +0,0 @@ -// Test: subpixel reshape WITHOUT 3D permute -// Uses reshape_3d → reshape_2d → 2D transpose+cont (avoids 3D cont bug) -#include -#include -#include -#include "backend.hpp" -#include "ggml_graph.hpp" -#include "ggml.h" - -int main() { - // Small test: T_enc=4, up=2, tf=3 - // conv_out has ne=[T_enc, tf*up] = [4, 6], data: flat[t + c*4] - // c = h*up + u - // - // Reference subpixel: - // x.view(B, C//up, up, T) → x.view(B, C//up, up*T) - // up_pk[h, t'] = conv[h*up+u, t] where t' = u*T + t - // - // In ggml (column-major): - // 1. reshape_3d(T_enc, up, tf): ne=[T_enc, up, tf], element(t,u,h) = flat[t + u*T + h*up*T] - // = flat[t + (h*up+u)*T] = flat[t + c*T] ✓ - // 2. reshape_2d(T_out, tf): ne=[T_out, tf], element(t',h) = flat[t' + h*T_out] - // t' = t + u*T → flat[t + u*T + h*up*T] = flat[t + c*T] ✓ - // 3. transpose: ne=[tf, T_out], element(h,t') = flat[t' + h*T_out] (view, strides change) - // 4. cont: copies to flat[h + t'*tf] (2D cont, which WORKS) - - const int T_enc = 4, up = 2, tf = 3; - const int T_out = T_enc * up; - - float conv_data[24]; - for (int c = 0; c < tf * up; c++) - for (int t = 0; t < T_enc; t++) - conv_data[t + c * T_enc] = (c + 1) * 10 + t; - - float bias_data[6] = {0}; - - std::vector out; - bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - int64_t ne[2] = {T_enc, tf * up}; - ggml_tensor* conv_out = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, ne, - conv_data, 24 * sizeof(float)); - - int64_t bne[2] = {1, tf * up}; - ggml_tensor* bias = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, bne, - bias_data, 6 * sizeof(float)); - conv_out = ggml_add(ctx, conv_out, bias); - - // Subpixel: reshape_3d → reshape_2d → transpose → cont - ggml_tensor* upsampled = ggml_reshape_3d(ctx, conv_out, T_enc, up, tf); - upsampled = ggml_reshape_2d(ctx, upsampled, T_enc * up, tf); // ne=[T_out, tf] - upsampled = ggml_cont(ctx, ggml_transpose(ctx, upsampled)); // ne=[tf, T_out] - return upsampled; - }, out); - - if (!ok) { printf("FAIL: run_graph returned false\n"); return 1; } - - // out has ne=[tf, T_out], data: flat[h + t'*tf] - printf("Expected:\n"); - for (int h = 0; h < tf; h++) { - printf(" h%d: ", h); - for (int tp = 0; tp < T_out; tp++) { - int u = tp / T_enc; - int t = tp % T_enc; - int c = h * up + u; - printf("%.0f ", conv_data[t + c * T_enc]); - } - printf("\n"); - } - printf("Got:\n"); - for (int h = 0; h < tf; h++) { - printf(" h%d: ", h); - for (int tp = 0; tp < T_out; tp++) { - printf("%.0f ", out[h + tp * tf]); - } - printf("\n"); - } - - bool pass = true; - for (int h = 0; h < tf; h++) - for (int tp = 0; tp < T_out; tp++) { - int u = tp / T_enc; - int t = tp % T_enc; - int c = h * up + u; - if (out[h + tp * tf] != conv_data[t + c * T_enc]) { pass = false; } - } - printf("Result: %s\n", pass ? "PASS" : "FAIL"); - return pass ? 0 : 1; -} From f6098fd23e71d239ec272de70a9d122d96726ad7 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:43:52 +0000 Subject: [PATCH 12/17] fix(diarization): match NeMo cache-aware streaming, rework the C-API Streaming diarization did not follow NeMo's Sortformer streaming: - chunk_len, spkcache_len, fifo_len and spkcache_update_period are encoder frames in NeMo (264 = 21.12 s), not mel frames, and the speaker cache was 8x too small. - The cache stored channels-first embeddings but was concatenated as if time-major, so [cache | chunk] was scrambled after one chunk. - The cache must hold pre-norm embeddings: NeMo applies embed_norm inside the encoder, after the bypassed pre-encoder. - NeMo runs the head over the whole [cache | fifo | chunk] sequence and feeds the downsampled predictions to the cache update. The head ran on the chunk alone. - The converter wrote no streaming keys for .nemo input, and read them from a "streaming" section that does not exist. spkcache_sil_frames_per_spk is 1 for Nemotron-3, not 3. StreamingDiarization now mirrors streaming_update and _compress_spkcache (sync mode). Top-k ties go to the earlier frame so the result is deterministic. Segments carry across chunk boundaries. On the fixture and on 31 s and 68 s clips (the last compresses the cache twice) the streaming segments match NeMo streaming exactly. The encoder builds one shared graph for the offline and streaming entry points, works time-major end to end, reads RoPE base, rotary fraction and pre_block_norm from the GGUF, and throws on graph failure instead of asserting. The converter drops the dead ASR-path diarization code and accepts a HF id. C-API (ABI 7, nothing released used 8): - SAS JSON uses the shared JSON writers: text is escaped and long utterances are no longer cut at 512 bytes. - transcribe_and_diarize returns a status. Zero utterances is success, not NULL. free_sas_results takes the count and frees every .text. - Streaming diarization takes 16 kHz PCM and computes the mel with StreamingMel (bit-identical to the whole-clip mel), instead of asking the caller for mel chunks. - Streaming SAS transcribes the uncommitted audio when a diarization chunk completes and commits only words that end 1 s before the cut, so words at chunk edges are not split. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- include/parakeet_capi.h | 243 ++++----- scripts/convert_parakeet_to_gguf.py | 148 ++---- src/diarization.cpp | 8 +- src/diarization_encoder.cpp | 514 ++++++------------ src/diarization_encoder.hpp | 75 ++- src/diarization_head.cpp | 115 +---- src/diarization_head.hpp | 16 +- src/diarization_streaming.cpp | 711 ++++++++++--------------- src/diarization_streaming.hpp | 186 +++---- src/model_loader.cpp | 49 +- src/model_loader.hpp | 13 +- src/parakeet_capi.cpp | 743 ++++++++++++++------------- tests/test_combined_offline.cpp | 358 ++++++------- tests/test_streaming_diarization.cpp | 255 +++++---- 14 files changed, 1420 insertions(+), 2014 deletions(-) diff --git a/include/parakeet_capi.h b/include/parakeet_capi.h index e422d0f..31c23b3 100644 --- a/include/parakeet_capi.h +++ b/include/parakeet_capi.h @@ -47,22 +47,11 @@ typedef struct parakeet_ctx parakeet_ctx; // KenLM) that need the raw distribution rather than this library's own // greedy/beam decode. Freed with the new parakeet_capi_free_logits. The // original entry points are unchanged. -// v6.1: added offline speaker diarization entry points -// (parakeet_capi_diarize_path / parakeet_capi_diarize_pcm) for -// nvidia/Nemotron-3-Diarization and compatible Sortformer models. A -// parakeet_ctx now holds EITHER an ASR model (pk::Model) OR a diarization -// model (pk::DiarizationModel); the diarize functions dispatch on which -// is loaded. parakeet_capi_load auto-detects the arch. No existing ASR -// signatures changed. -// v7: added speaker-attributed ASR (SAS) entry points -// (parakeet_capi_transcribe_and_diarize / -// parakeet_capi_transcribe_and_diarize_json). Takes two contexts -// (an ASR ctx + a diarization ctx), runs both models on the same -// audio, and merges word timestamps with speaker segments. -// v8: added streaming diarization entry points -// (parakeet_capi_diarize_stream_begin / _feed / _free) and streaming -// speaker-attributed ASR (parakeet_capi_sas_stream_begin / _feed / _free). -// Streaming diarization uses the AOSC mechanism (spkcache + FIFO). +// v7: added speaker diarization (parakeet_capi_diarize_*), speaker-attributed +// ASR (parakeet_capi_transcribe_and_diarize*, parakeet_capi_sas_stream_*) +// for nvidia/Nemotron-3-Diarization. A parakeet_ctx now holds either an +// ASR or a diarization model; parakeet_capi_load detects which. No +// existing signatures changed. int parakeet_capi_abi_version(void); // Load a GGUF model. Returns an owning context, or NULL on failure. @@ -338,162 +327,130 @@ char* parakeet_capi_stream_finalize_json(parakeet_stream* s); // Free a streaming session. Safe on NULL. void parakeet_capi_stream_free(parakeet_stream* s); +// Free a string previously returned by parakeet_capi_transcribe_* / +// parakeet_capi_stream_* / parakeet_capi_diarize_* / +// parakeet_capi_transcribe_and_diarize_json. Safe on NULL. +void parakeet_capi_free_string(char* s); + +// Human-readable description of the last error on `ctx`, or "" if none. +// The returned pointer is owned by the context and valid until the next call on +// it (or until parakeet_capi_free). Returns "" if `ctx` is NULL. +const char* parakeet_capi_last_error(parakeet_ctx* ctx); + // --------------------------------------------------------------------------- -// Offline speaker diarization (nvidia/Nemotron-3-Diarization and compatible -// Sortformer models). A parakeet_ctx loaded from a diarization GGUF holds a -// pk::DiarizationModel instead of an ASR pk::Model. The diarize functions -// below are the only valid entry points for such a context (the transcribe -// functions return NULL); conversely, diarize functions on an ASR context -// return NULL. parakeet_capi_load auto-detects the arch. +// Speaker diarization (nvidia/Nemotron-3-Diarization and compatible Sortformer +// models), ABI v7. +// +// A parakeet_ctx loaded from a diarization GGUF holds a diarization model +// instead of an ASR model (parakeet_capi_load detects the arch). The functions +// below are the only valid entry points for such a context; the transcribe_* +// and stream_* functions fail on it with a last_error message, and the +// diarize_* functions fail on an ASR context. +// +// Times are seconds from the start of the audio; speakers are 0-based indices +// in order of first appearance, up to the model's capacity (8). // --------------------------------------------------------------------------- -// Diarize a WAV file. Returns a malloc'd UTF-8 JSON document (free with -// parakeet_capi_free_string) of the shape: -// {"speakers":8, -// "segments":[{"speaker":0,"start":0.10,"end":1.30}, ...]} -// where "speakers" is the model's max-speaker capacity, "speaker" is a 0-based -// speaker index, and "start"/"end" are seconds (2 decimals, matching NeMo's -// round(ts, 2)). Segments are sorted by start time then speaker. On error -// returns NULL and sets the context's last error. +// Offline diarization of a WAV file. Returns a malloc'd UTF-8 JSON document +// (free with parakeet_capi_free_string): +// {"speakers":8,"segments":[{"speaker":0,"start":0.50,"end":5.52}, ...]} +// "speakers" is the model's capacity. Segments are sorted by start time then +// speaker, with times rounded to 10 ms. NULL on error (see last_error). char* parakeet_capi_diarize_path(parakeet_ctx* ctx, const char* wav_path); -// Diarize in-memory mono float PCM (`samples`, length `n_samples`). If -// `sample_rate != 16000` the audio is linearly resampled to 16 kHz first. -// Returns the same JSON shape as parakeet_capi_diarize_path. Free with -// parakeet_capi_free_string; NULL on error. +// Same for in-memory mono float PCM; resampled to 16 kHz when +// `sample_rate != 16000`. char* parakeet_capi_diarize_pcm(parakeet_ctx* ctx, const float* samples, int n_samples, int sample_rate); -// Free a string previously returned by parakeet_capi_transcribe_* / -// parakeet_capi_diarize_* / parakeet_capi_transcribe_and_diarize_* / -// parakeet_capi_stream_*. Safe on NULL. -void parakeet_capi_free_string(char* s); - -// --------------------------------------------------------------------------- -// Speaker-attributed ASR (SAS): run both ASR and diarization on the same -// audio, then merge word timestamps with speaker segments ("who said what"). -// -// Takes two separately-loaded contexts: `asr_ctx` (an ASR model) and -// `diar_ctx` (a diarization model). Both must have been loaded successfully -// via parakeet_capi_load. The audio is fed to each model independently, so -// mel is computed twice (once per model). Phase 3.3 (mel sharing) will -// optimize this when the two models' mel configs match. -// --------------------------------------------------------------------------- - -// Speaker-attributed result: one utterance = one speaker + text + time span. +// Speaker-attributed ASR ("who said what"): one utterance is a run of +// consecutive words from one speaker. typedef struct parakeet_sas_result { - int speaker; // 0-based speaker index, -1 = no speaker found - char* text; // utterance text (space-joined words) - float start; // utterance start (seconds) - float end; // utterance end (seconds) + int speaker; // 0-based speaker index, -1 = no diarized speaker overlaps + char* text; // utterance text (space-joined words), owned by the array + float start; // first word start (seconds) + float end; // last word end (seconds) float conf; // min word confidence } parakeet_sas_result; -// Run ASR + diarization and merge. Returns a malloc'd array of -// parakeet_sas_result (free with parakeet_capi_free_sas_results). -// *n_results receives the count. Returns NULL on error. -parakeet_sas_result* parakeet_capi_transcribe_and_diarize( - parakeet_ctx* asr_ctx, - parakeet_ctx* diar_ctx, - const float* samples, int n_samples, int sample_rate, - int* n_results); - -// Free a result array from parakeet_capi_transcribe_and_diarize. Safe on NULL. -void parakeet_capi_free_sas_results(parakeet_sas_result* results); - -// JSON variant with full per-word + per-utterance detail. Returns a malloc'd -// UTF-8 JSON document (free with parakeet_capi_free_string) of the shape: +// Run ASR (`asr_ctx`) and diarization (`diar_ctx`) on the same mono float PCM +// and assign each ASR word to the speaker whose segments overlap it most. +// On success returns 0 and sets *out (malloc'd array, free with +// parakeet_capi_free_sas_results) and *n_out; *out may be NULL when +// *n_out == 0. On error returns non-zero and sets last_error on the context +// that failed. +int parakeet_capi_transcribe_and_diarize(parakeet_ctx* asr_ctx, parakeet_ctx* diar_ctx, + const float* samples, int n_samples, + int sample_rate, + parakeet_sas_result** out, int* n_out); + +// Free an array from parakeet_capi_transcribe_and_diarize or +// parakeet_capi_sas_stream_feed, including every .text. Safe on NULL. +void parakeet_capi_free_sas_results(parakeet_sas_result* results, int n); + +// JSON variant with per-utterance and per-word detail (free with +// parakeet_capi_free_string; NULL on error): // {"speakers":8, -// "utterances":[ -// {"speaker":0,"text":"hello world","start":0.12,"end":0.85,"conf":0.95}, -// ...], -// "words":[ -// {"speaker":0,"text":"hello","start":0.12,"end":0.45,"conf":0.97}, -// ...]} -// Returns NULL on error. -char* parakeet_capi_transcribe_and_diarize_json( - parakeet_ctx* asr_ctx, - parakeet_ctx* diar_ctx, - const float* samples, int n_samples, int sample_rate); - -// Human-readable description of the last error on `ctx`, or "" if none. -// The returned pointer is owned by the context and valid until the next call on -// it (or until parakeet_capi_free). Returns "" if `ctx` is NULL. -const char* parakeet_capi_last_error(parakeet_ctx* ctx); - -// --------------------------------------------------------------------------- -// v8: Streaming diarization (AOSC + FIFO) -// -// Streaming diarization processes audio in chunks. The caller: -// 1. parakeet_capi_diarize_stream_begin(ctx) → parakeet_diar_stream* -// 2. parakeet_capi_diarize_stream_feed(stream, mel, n_mels, n_frames, is_last) -// → parakeet_diar_segment* (segments for this chunk) -// 3. Repeat step 2 for each chunk -// 4. parakeet_capi_diarize_stream_free(stream) -// -// The mel features must be pre-computed by the caller (128-dim, 16kHz). -// Each chunk should be exactly `chunk_len` mel frames (available from -// parakeet_capi_diar_stream_chunk_len()). The final chunk may be shorter. +// "utterances":[{"speaker":0,"text":"hello world","start":0.12,"end":0.85,"conf":0.95}], +// "words":[{"speaker":0,"text":"hello","start":0.12,"end":0.45,"conf":0.97}]} +char* parakeet_capi_transcribe_and_diarize_json(parakeet_ctx* asr_ctx, parakeet_ctx* diar_ctx, + const float* samples, int n_samples, + int sample_rate); + +// --- Streaming diarization ------------------------------------------------- +// NeMo cache-aware streaming (speaker cache + FIFO) over live 16 kHz mono +// float PCM. Audio is processed in the model's chunks +// (parakeet_capi_diarize_stream_chunk_samples; 21.12 s for +// Nemotron-3-Diarization), so segments arrive once per chunk. Speaker indices +// stay consistent across chunks. The stream borrows `diar_ctx`: free the +// stream first, and do not use one context from two threads at once. typedef struct parakeet_diar_segment { - int speaker; // 0-indexed speaker ID - float start; // wall-clock seconds from stream start + int speaker; + float start; // seconds from stream start float end; } parakeet_diar_segment; typedef struct parakeet_diar_stream parakeet_diar_stream; -// Begin a streaming diarization session. Returns NULL on error. +// NULL on error (last_error on diar_ctx). parakeet_diar_stream* parakeet_capi_diarize_stream_begin(parakeet_ctx* diar_ctx); -// Feed one chunk of mel features. Returns segments for this chunk -// (caller must free the returned array with parakeet_capi_free_diar_segments). -// `mel` is row-major [n_mels, n_frames]: mel[m*n_frames + t]. -// `is_last` marks the final chunk (no spkcache update after it). -// `out_count` receives the number of returned segments. -// Returns NULL if no segments were produced (out_count = 0). -parakeet_diar_segment* parakeet_capi_diarize_stream_feed( - parakeet_diar_stream* stream, - const float* mel, int n_mels, int n_frames, - int is_last, int* out_count); +// Samples per processing chunk (the segment latency). 0 on NULL. +int parakeet_capi_diarize_stream_chunk_samples(parakeet_diar_stream* s); -// Get the expected chunk length in mel frames. -int parakeet_capi_diar_stream_chunk_len(parakeet_diar_stream* stream); +// Feed PCM; `is_last` flushes the tail and closes open segments. Returns 0 and +// sets *out / *n_out to the segments that ENDED since the previous call +// (free with parakeet_capi_free_diar_segments; *out may be NULL when +// *n_out == 0). Non-zero on error (last_error on the stream's diar_ctx). +int parakeet_capi_diarize_stream_feed(parakeet_diar_stream* s, const float* pcm, + int n_samples, int is_last, + parakeet_diar_segment** out, int* n_out); -// Get the expected number of mel features. -int parakeet_capi_diar_stream_n_mels(parakeet_diar_stream* stream); - -// Free segments returned by parakeet_capi_diarize_stream_feed. void parakeet_capi_free_diar_segments(parakeet_diar_segment* segs); +void parakeet_capi_diarize_stream_free(parakeet_diar_stream* s); -// Free a streaming diarization session. -void parakeet_capi_diarize_stream_free(parakeet_diar_stream* stream); - -// --------------------------------------------------------------------------- -// v8: Streaming speaker-attributed ASR (Phase 3.4) -// -// Combines streaming diarization with chunked ASR. The caller feeds audio -// chunks; internally, both ASR and diarization process the audio and the -// results are merged using the same SAS merge logic as the offline path. +// --- Streaming speaker-attributed ASR --------------------------------------- +// Streaming diarization plus ASR over the same live 16 kHz PCM. Each time a +// diarization chunk completes, the not-yet-committed audio is transcribed; +// all words but the last (which may still be cut by the chunk edge) are +// committed with their speakers, and the rest is carried into the next +// chunk. `is_last` commits everything. Borrows both contexts. typedef struct parakeet_sas_stream parakeet_sas_stream; -// Begin a streaming SAS session. Returns NULL on error. -parakeet_sas_stream* parakeet_capi_sas_stream_begin( - parakeet_ctx* asr_ctx, parakeet_ctx* diar_ctx); - -// Feed one chunk of PCM samples (mono float, 16 kHz). -// Returns speaker-attributed utterances for this chunk. -// Caller must free each result's .text with parakeet_capi_free_string, -// then the array with parakeet_capi_free_sas_results. -// Returns NULL if nothing was produced (out_count = 0). -parakeet_sas_result* parakeet_capi_sas_stream_feed( - parakeet_sas_stream* stream, - const float* pcm, int n_samples, - int is_last, int* out_count); - -// Free a streaming SAS session. -void parakeet_capi_sas_stream_free(parakeet_sas_stream* stream); +// NULL on error (last_error on the context that failed). +parakeet_sas_stream* parakeet_capi_sas_stream_begin(parakeet_ctx* asr_ctx, + parakeet_ctx* diar_ctx); + +// Returns 0 and sets *out / *n_out to the utterances committed by this call +// (free with parakeet_capi_free_sas_results(*out, *n_out)). Consecutive calls +// can each return an utterance from the same speaker. Non-zero on error. +int parakeet_capi_sas_stream_feed(parakeet_sas_stream* s, const float* pcm, + int n_samples, int is_last, + parakeet_sas_result** out, int* n_out); + +void parakeet_capi_sas_stream_free(parakeet_sas_stream* s); #ifdef __cplusplus } // extern "C" diff --git a/scripts/convert_parakeet_to_gguf.py b/scripts/convert_parakeet_to_gguf.py index 7301d25..f2ebfc1 100644 --- a/scripts/convert_parakeet_to_gguf.py +++ b/scripts/convert_parakeet_to_gguf.py @@ -41,12 +41,6 @@ print("PARAKEET_CONVERT_DEPS_MISSING", file=sys.stderr) sys.exit(2) -# SortformerEncLabelModel import is optional — the installed NeMo may be too old -# to support self_attention_model='rope'. The converter detects diarization from -# the .nemo tar's model_config.yaml and loads state_dict directly, bypassing the -# model class entirely. -SortformerEncLabelModel = None - import io import tarfile @@ -130,15 +124,7 @@ def _get(cfg, key, default=None): def detect_arch(m): - """Map a NeMo model to one of ctc/rnnt/tdt/hybrid_rnnt_ctc/hybrid_tdt_ctc/diarization.""" - # Diarization model (SortformerEncLabelModel): has sortformer_modules, no - # tokenizer/vocab, no joint/CTC decoder — output is speaker sigmoid logits. - if SortformerEncLabelModel is not None and isinstance(m, SortformerEncLabelModel): - return "diarization" - # Fallback: detect by state_dict keys (works even if the import above failed) - sd = m.state_dict() - if any(k.startswith("sortformer_modules.") for k in sd) and not hasattr(m, "tokenizer"): - return "diarization" + """Map a NeMo ASR model to one of ctc/rnnt/tdt/hybrid_rnnt_ctc/hybrid_tdt_ctc.""" cfg = m.cfg # model: prompt-conditioned RNNT checkpoints (nemotron) carry an unconfigured # aux_ctc stub (num_classes=-1, empty vocabulary) but NO ctc decoder and zero @@ -276,10 +262,19 @@ def main(): # tar, bypassing SortformerEncLabelModel.restore_from() (which fails on # NeMo versions that don't support self_attention_model='rope'). # ------------------------------------------------------------------ - is_diar = is_local and args.model.endswith(".nemo") and _is_diarization_nemo(args.model) + nemo_path = args.model if is_local and args.model.endswith(".nemo") else None + if nemo_path is None and not is_local and "/" in args.model: + # HF id: diarization repos ship .nemo; ASR ids fall through to + # ASRModel.from_pretrained below when this is absent. + try: + from huggingface_hub import hf_hub_download + nemo_path = hf_hub_download(args.model, args.model.split("/")[-1] + ".nemo") + except Exception: + nemo_path = None + is_diar = nemo_path is not None and _is_diarization_nemo(nemo_path) if is_diar: - sd, model_cfg = _load_diarization_from_tar(args.model) + sd, model_cfg = _load_diarization_from_tar(nemo_path) arch = "diarization" w = gguf.GGUFWriter(args.output, "parakeet") @@ -358,6 +353,29 @@ def main(): w.add_float32("parakeet.diar.onset_threshold", 0.5) w.add_float32("parakeet.diar.offset_threshold", 0.5) + # Streaming speaker-cache config (SortformerModules), in encoder frames. + # Defaults are the SortformerModules constructor defaults. + def sf(key, default): + return _get_cfg_value(sf_cfg, key, default) + w.add_uint32("parakeet.diar.chunk_len", int(sf("chunk_len", 188))) + w.add_uint32("parakeet.diar.spkcache_len", int(sf("spkcache_len", 188))) + w.add_uint32("parakeet.diar.fifo_len", int(sf("fifo_len", 0))) + w.add_uint32("parakeet.diar.spkcache_update_period", + int(sf("spkcache_update_period", 188))) + w.add_uint32("parakeet.diar.spkcache_sil_frames_per_spk", + int(sf("spkcache_sil_frames_per_spk", 3))) + w.add_float32("parakeet.diar.sil_threshold", float(sf("sil_threshold", 0.2))) + w.add_float32("parakeet.diar.pred_score_threshold", + float(sf("pred_score_threshold", 0.25))) + w.add_float32("parakeet.diar.scores_boost_latest", + float(sf("scores_boost_latest", 0.05))) + w.add_float32("parakeet.diar.strong_boost_rate", float(sf("strong_boost_rate", 0.75))) + w.add_float32("parakeet.diar.weak_boost_rate", float(sf("weak_boost_rate", 1.5))) + w.add_float32("parakeet.diar.min_pos_scores_rate", + float(sf("min_pos_scores_rate", 0.5))) + w.add_bool("parakeet.diar.use_learnable_sil_emb", + bool(sf("use_learnable_sil_emb", False))) + # Write tensors from state_dict written = 0 quantized = 0 @@ -394,24 +412,14 @@ def main(): # ------------------------------------------------------------------ # ASR path: load via NeMo model class (as before) # ------------------------------------------------------------------ - m = None - if SortformerEncLabelModel is not None: - try: - if is_local: - m = SortformerEncLabelModel.restore_from(args.model, map_location="cpu") - else: - m = SortformerEncLabelModel.from_pretrained(args.model, map_location="cpu") - except Exception: - m = None # not a diarization model, fall through to ASRModel - if m is None: - try: - if is_local: - m = ASRModel.restore_from(args.model, map_location="cpu") - else: - m = ASRModel.from_pretrained(args.model, map_location="cpu") - except Exception as e: # pragma: no cover - network/cache guard - print(f"PARAKEET_MODEL_UNAVAILABLE: {e}", file=sys.stderr) - sys.exit(2) + try: + if is_local: + m = ASRModel.restore_from(args.model, map_location="cpu") + else: + m = ASRModel.from_pretrained(args.model, map_location="cpu") + except Exception as e: # pragma: no cover - network/cache guard + print(f"PARAKEET_MODEL_UNAVAILABLE: {e}", file=sys.stderr) + sys.exit(2) m.eval() arch = detect_arch(m) @@ -538,62 +546,12 @@ def _int_list(v): w.add_float32("parakeet.preprocessor.log_zero_guard", float(lzg) if isinstance(lzg, (int, float)) else 2 ** -24) - # vocab / tokenizer (ASR models only — diarization has no tokenizer) - vocab = 0 - if arch != "diarization": - vocab = int(m.tokenizer.vocab_size) - w.add_uint32("parakeet.vocab_size", vocab) - w.add_uint32("parakeet.blank_id", vocab) # blank always == vocab_size - pieces = [m.tokenizer.ids_to_tokens([i])[0] for i in range(vocab)] - w.add_array("parakeet.tokenizer.pieces", [str(p) for p in pieces]) - - # diarization config (SortformerEncLabelModel) - if arch == "diarization": - sf = m.sortformer_modules - # Speaker head dimensions - tf_d_model = int(sf.tf_d_model) if hasattr(sf, "tf_d_model") else 192 - n_spk = int(sf.n_speakers) if hasattr(sf, "n_speakers") else 8 - # Upsample factor = subsampling_factor (high_resolution=True → 10ms frames) - upsample = int(_get(enc, "subsampling_factor", 8)) - # Thresholds from cfg or NeMo defaults - diar_cfg = _get(cfg, "diarizer", {}) or {} - cfg_clustering = _get(diar_cfg, "clustering", {}) or {} - onset = float(_get(diar_cfg, "onset", 0.5)) - offset = float(_get(diar_cfg, "offset", 0.5)) - w.add_uint32("parakeet.diar.n_speakers", n_spk) - w.add_uint32("parakeet.diar.tf_d_model", tf_d_model) - w.add_uint32("parakeet.diar.upsample_factor", upsample) - w.add_float32("parakeet.diar.frame_resolution_sec", 0.01) - w.add_float32("parakeet.diar.onset_threshold", onset) - w.add_float32("parakeet.diar.offset_threshold", offset) - - # AOSC streaming config (Phase 2) - # Nemotron-3-Diarization defaults from NeMo config - streaming_cfg = _get(cfg, "streaming", {}) or {} - chunk_len = int(_get(streaming_cfg, "chunk_len", 264)) - spkcache_len = int(_get(streaming_cfg, "spkcache_len", chunk_len)) - fifo_len = int(_get(streaming_cfg, "fifo_len", 0)) - spkcache_update = int(_get(streaming_cfg, "spkcache_update_period", chunk_len)) - sil_per_spk = int(_get(streaming_cfg, "spkcache_sil_frames_per_spk", 3)) - sil_thresh = float(_get(streaming_cfg, "sil_threshold", 0.2)) - pred_score_thresh = float(_get(streaming_cfg, "pred_score_threshold", 0.25)) - scores_boost = float(_get(streaming_cfg, "scores_boost_latest", 0.05)) - strong_boost = float(_get(streaming_cfg, "strong_boost_rate", 0.75)) - weak_boost = float(_get(streaming_cfg, "weak_boost_rate", 1.5)) - min_pos = float(_get(streaming_cfg, "min_pos_scores_rate", 0.5)) - learnable_sil = bool(_get(streaming_cfg, "use_learnable_sil_emb", True)) - w.add_uint32("parakeet.diar.chunk_len", chunk_len) - w.add_uint32("parakeet.diar.spkcache_len", spkcache_len) - w.add_uint32("parakeet.diar.fifo_len", fifo_len) - w.add_uint32("parakeet.diar.spkcache_update_period", spkcache_update) - w.add_uint32("parakeet.diar.spkcache_sil_frames_per_spk", sil_per_spk) - w.add_float32("parakeet.diar.sil_threshold", sil_thresh) - w.add_float32("parakeet.diar.pred_score_threshold", pred_score_thresh) - w.add_float32("parakeet.diar.scores_boost_latest", scores_boost) - w.add_float32("parakeet.diar.strong_boost_rate", strong_boost) - w.add_float32("parakeet.diar.weak_boost_rate", weak_boost) - w.add_float32("parakeet.diar.min_pos_scores_rate", min_pos) - w.add_bool("parakeet.diar.use_learnable_sil_emb", learnable_sil) + # vocab / tokenizer + vocab = int(m.tokenizer.vocab_size) + w.add_uint32("parakeet.vocab_size", vocab) + w.add_uint32("parakeet.blank_id", vocab) # blank always == vocab_size + pieces = [m.tokenizer.ids_to_tokens([i])[0] for i in range(vocab)] + w.add_array("parakeet.tokenizer.pieces", [str(p) for p in pieces]) # transducer config if arch in ("rnnt", "tdt", "hybrid_rnnt_ctc", "hybrid_tdt_ctc"): @@ -627,15 +585,7 @@ def _int_list(v): written = 0 quantized = 0 keep_buffers = {"preprocessor.featurizer.fb", "preprocessor.featurizer.window"} - # Frozen/unused weights to skip (diarization: hidden_to_spks is a frozen - # placeholder that is never called in offline inference). - skip_names = set() - if arch == "diarization": - skip_names.add("sortformer_modules.hidden_to_spks.weight") - skip_names.add("sortformer_modules.hidden_to_spks.bias") for name, t in sd.items(): - if name in skip_names: - continue if name.startswith("preprocessor.") and name not in keep_buffers: continue # skip preprocessor internals except fb/window if not hasattr(t, "detach"): diff --git a/src/diarization.cpp b/src/diarization.cpp index 64cbb9e..b4bf7e5 100644 --- a/src/diarization.cpp +++ b/src/diarization.cpp @@ -95,14 +95,14 @@ void DiarizationModel::speaker_probs(const std::vector& samples, } if (T_mel == 0) return; - // 2. Diarization encoder -> enc_out [d_model, T_enc] (channels-first) + // 2. Diarization encoder -> enc_out [T_enc, d_model] (time-major) std::vector enc_out; - int d_model = 0, T_enc = 0; - encoder_->forward(feats, n_mels, T_mel, enc_out, d_model, T_enc); + int T_enc = 0; + encoder_->forward(feats, n_mels, T_mel, enc_out, T_enc); // 3. Diarization head -> probs [n_spk, T_out] (post-sigmoid) int T_out = 0; - head_->forward(enc_out, d_model, T_enc, probs, n_spk, T_out); + head_->forward(enc_out, T_enc, probs, n_spk, T_out); // High-resolution output has one frame per mel frame; drop the frames the // FeatureStacking pad added past T_mel (NeMo slices preds to the mel length). diff --git a/src/diarization_encoder.cpp b/src/diarization_encoder.cpp index c91be3c..8339dd2 100644 --- a/src/diarization_encoder.cpp +++ b/src/diarization_encoder.cpp @@ -4,404 +4,182 @@ #include "ggml_graph.hpp" #include "ggml.h" -#include #include +#include #include #include namespace pk { -// ============================================================================ -// DiarizationEncoder — pre-LN RoPE Transformer for Nemotron-3-Diarization. -// -// mel [n_mels=128, T] -// → FeatureStacking: transpose, pad, reshape [1024, T/8], Linear(1024→512) -// → embed_norm: LayerNorm(512, eps=1e-5) -// → 31× TransformerBlock (pre-norm): -// x = x + attn(norm1(x)) -// x = x + ffn(norm2(x)) -// → final_norm: LayerNorm(512, eps=1e-5) -// → transpose → output [d_model, T_enc] (channels-first: enc_out[c*Tp+t]) -// ============================================================================ - DiarizationEncoder::DiarizationEncoder(const ModelLoader& ml) : ml_(ml) { const auto& cfg = ml.config(); d_model_ = (int)cfg.d_model; n_layers_ = (int)cfg.n_layers; n_heads_ = (int)cfg.n_heads; - head_dim_ = d_model_ / n_heads_; - ff_dim_ = (int)cfg.ff_dim; subsampling_factor_ = (int)cfg.subsampling_factor; n_mels_ = (int)cfg.n_mels; - qkv_bias_ = cfg.use_bias; - pre_block_norm_ = true; - rope_base_ = 10000.0f; - rotary_fraction_ = 1.0f; + pre_block_norm_ = cfg.pre_block_norm; + rope_base_ = cfg.rope_base; ln_eps_ = 1e-5f; - assert(n_layers_ > 0 && d_model_ > 0); - assert(subsampling_factor_ > 0); - assert(head_dim_ * n_heads_ == d_model_); + if (n_layers_ <= 0 || d_model_ <= 0 || n_heads_ <= 0 || subsampling_factor_ <= 0 || + d_model_ % n_heads_ != 0) { + throw std::runtime_error("parakeet: invalid diarization encoder config"); + } + if (!cfg.self_attention_model.empty() && cfg.self_attention_model != "rope") { + throw std::runtime_error("parakeet: unsupported diarization self_attention_model '" + + cfg.self_attention_model + "'"); + } + head_dim_ = d_model_ / n_heads_; + n_rot_ = (int)(head_dim_ * cfg.rotary_fraction); } -void DiarizationEncoder::forward(const std::vector& mel, int n_mels, int T, - std::vector& enc_out, - int& d_model, int& T_enc) const { - assert(n_mels == n_mels_); - assert((int)mel.size() == n_mels * T); - - const int factor = subsampling_factor_; // 8 - const int pad = (factor - (T % factor)) % factor; - const int T_padded = T + pad; - const int Tp = T_padded / factor; - - std::vector positions(Tp); - for (int i = 0; i < Tp; ++i) positions[i] = i; - - const ModelLoader& ml = ml_; - const int d = d_model_; - const int H = n_heads_; - const int hd = head_dim_; - const int nls = n_layers_; - const float ln_eps = ln_eps_; - const float rope_base = rope_base_; - const int n_rot = (int)(hd * rotary_fraction_); - - pk::ensure_weights_realized(ml); - GraphInputPool pool; - - bool ok = pk::run_graph(0, 0, - [&](ggml_context* ctx) -> ggml_tensor* { - // --- 1. FeatureStacking --- - // mel is [n_mels, T] row-major: mel[m*T + t] - // Copy into padded buffer [n_mels, T_padded] - int64_t mel_ne[2] = {T_padded, n_mels}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels * T_padded); - for (int m = 0; m < n_mels; ++m) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)m * T_padded + t] = mel[(size_t)m * T + t]; - - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - // mel_t: ne[0]=T_padded, ne[1]=n_mels - - // NeMo FeatureStacking transposes [C, T] → [T, C] before reshape. - // After transpose: ne[0]=n_mels, ne[1]=T_padded (time-major) - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - - // Reshape [T_padded, n_mels] → [n_mels*factor, Tp] = [1024, Tp] - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels * factor, Tp); - - // Linear(1024 → 512, no bias) - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); // [d_model, Tp] - - // --- 2. embed_norm --- - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - ggml_tensor* y = ggml_norm(ctx, x, ln_eps); - x = ggml_add(ctx, ggml_mul(ctx, y, g), b); - } - - // --- 3. Position tensor for RoPE --- - int64_t pos_ne[1] = {Tp}; - ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, - positions.data(), (size_t)Tp * sizeof(int32_t)); - - // --- 4. N × TransformerBlock (pre-norm) --- - for (int i = 0; i < nls; ++i) { - std::string base = "encoder.layers." + std::to_string(i) + "."; - - // norm1 - { - ggml_tensor* g = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* b = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_add(ctx, ggml_mul(ctx, h, g), b); - - // Fused QKV (no bias) - ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); - ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); // [3*d, Tp] - qkv = ggml_cont(ctx, qkv); - qkv = ggml_reshape_3d(ctx, qkv, d, 3, Tp); // [d, 3, Tp] - - // Split Q, K, V - size_t ts = (size_t)3 * d * sizeof(float); - ggml_tensor* q = ggml_view_2d(ctx, qkv, d, Tp, ts, 0); - ggml_tensor* k = ggml_view_2d(ctx, qkv, d, Tp, ts, (size_t)d * sizeof(float)); - ggml_tensor* v = ggml_view_2d(ctx, qkv, d, Tp, ts, (size_t)2 * d * sizeof(float)); - - // Reshape to [hd, H, Tp] - q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), hd, H, Tp); - k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), hd, H, Tp); - v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), hd, H, Tp); - - // RoPE (GPT-NeoX) - q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, - GGML_ROPE_TYPE_NEOX, 0, rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, - GGML_ROPE_TYPE_NEOX, 0, rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - - // Permute to [hd, Tp, H] (head in batch dim) - q = ggml_permute(ctx, q, 0, 2, 1, 3); - k = ggml_permute(ctx, k, 0, 2, 1, 3); - v = ggml_permute(ctx, v, 0, 2, 1, 3); - - // flash_attn_ext: q [hd, Tp, H], k [hd, Tp, H], v [hd, Tp, H] - // Result: ne = [hd, H, Tp, 1] (ggml.c line 5357-5358) - // Memory order: flat[t*H*hd + h*hd + d] = [T, H, hd] - // This is ALREADY the correct PyTorch merge order - // (attn.transpose(1,2).contiguous().view(B,T,d_model)). - // Do NOT permute — just cont + reshape. - float scale = 1.0f / std::sqrt((float)hd); - ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, - scale, 0.0f, 0.0f); - attn = ggml_cont(ctx, attn); - attn = ggml_reshape_2d(ctx, attn, (int64_t)d, (int64_t)Tp); - - // out_proj (with bias) - ggml_tensor* op_w = pk::clone_weight(ctx, ml, (base + "attn.out_proj.weight").c_str()); - attn = ggml_mul_mat(ctx, op_w, attn); - ggml_tensor* op_b = pk::clone_weight_opt(ctx, ml, (base + "attn.out_proj.bias").c_str()); - if (op_b) attn = ggml_add(ctx, attn, op_b); - - // Residual - x = ggml_add(ctx, x, attn); - } - - // norm2 + FFN - { - ggml_tensor* g = pk::clone_weight(ctx, ml, (base + "norm2.weight").c_str()); - ggml_tensor* b = pk::clone_weight(ctx, ml, (base + "norm2.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_add(ctx, ggml_mul(ctx, h, g), b); - - // FFN: Linear(d→ff, bias) → GELU → Linear(ff→d, bias) - ggml_tensor* f0_w = pk::clone_weight(ctx, ml, (base + "ffn.net.0.weight").c_str()); - h = ggml_mul_mat(ctx, f0_w, h); - ggml_tensor* f0_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.0.bias").c_str()); - if (f0_b) h = ggml_add(ctx, h, f0_b); - h = ggml_gelu(ctx, h); - - ggml_tensor* f3_w = pk::clone_weight(ctx, ml, (base + "ffn.net.3.weight").c_str()); - h = ggml_mul_mat(ctx, f3_w, h); - ggml_tensor* f3_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.3.bias").c_str()); - if (f3_b) h = ggml_add(ctx, h, f3_b); - - x = ggml_add(ctx, x, h); - } - } - - // --- 5. final_norm --- - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.final_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.final_norm.bias"); - ggml_tensor* y = ggml_norm(ctx, x, ln_eps); - x = ggml_add(ctx, ggml_mul(ctx, y, g), b); - } - - // --- 6. Transpose to channels-first --- - // x: [d_model, Tp] (ne[0]=d, ne[1]=Tp) - // ggml flat: [d0_t0, d1_t0, ..., d511_t0, d0_t1, ...] = time-major - // We need channels-first: enc_out[c*Tp + t] - // transpose → [Tp, d_model], cont → flat[c*Tp + t] ✓ - x = ggml_cont(ctx, ggml_transpose(ctx, x)); - return x; - }, enc_out); - - assert(ok && "diarization encoder graph failed"); - (void)ok; - - d_model = d_model_; - T_enc = Tp; +static ggml_tensor* layer_norm(ggml_context* ctx, const ModelLoader& ml, ggml_tensor* x, + const std::string& name, float eps) { + ggml_tensor* g = pk::clone_weight(ctx, ml, (name + ".weight").c_str()); + ggml_tensor* b = pk::clone_weight(ctx, ml, (name + ".bias").c_str()); + return ggml_add(ctx, ggml_mul(ctx, ggml_norm(ctx, x, eps), g), b); } -// ============================================================================ -// Streaming split: pre_encode + transformer_forward -// ============================================================================ - -void DiarizationEncoder::pre_encode(const std::vector& mel, int n_mels, int T, - std::vector& emb, int& d_model, int& T_enc) const { - assert(n_mels == n_mels_); - assert((int)mel.size() == n_mels * T); +static ggml_tensor* linear(ggml_context* ctx, const ModelLoader& ml, ggml_tensor* x, + const std::string& name) { + x = ggml_mul_mat(ctx, pk::clone_weight(ctx, ml, (name + ".weight").c_str()), x); + ggml_tensor* b = pk::clone_weight_opt(ctx, ml, (name + ".bias").c_str()); + return b ? ggml_add(ctx, x, b) : x; +} +// mel: ne=[T_padded, n_mels] (the zero-padded [n_mels, T] frontend layout). +// Returns ne=[d_model, T_padded / factor]. +ggml_tensor* DiarizationEncoder::build_pre_encode(ggml_context* ctx, ggml_tensor* mel, + int T_padded) const { const int factor = subsampling_factor_; - const int pad = (factor - (T % factor)) % factor; - const int T_padded = T + pad; - const int Tp = T_padded / factor; - - const ModelLoader& ml = ml_; - const int d = d_model_; - const float ln_eps = ln_eps_; - - pk::ensure_weights_realized(ml); - GraphInputPool pool; - - bool ok = pk::run_graph(0, 0, - [&](ggml_context* ctx) -> ggml_tensor* { - // --- 1. FeatureStacking --- - int64_t mel_ne[2] = {T_padded, n_mels}; - std::vector& mel_padded = pool.alloc_f32((size_t)n_mels * T_padded); - for (int m = 0; m < n_mels; ++m) - for (int t = 0; t < T; ++t) - mel_padded[(size_t)m * T_padded + t] = mel[(size_t)m * T + t]; - - ggml_tensor* mel_t = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, mel_ne, - mel_padded.data(), (size_t)n_mels * T_padded * sizeof(float)); - mel_t = ggml_cont(ctx, mel_t); - mel_t = ggml_cont(ctx, ggml_transpose(ctx, mel_t)); - ggml_tensor* stacked = ggml_reshape_2d(ctx, mel_t, (int64_t)n_mels * factor, Tp); - - // Linear(1024 → 512, no bias) - ggml_tensor* proj_w = pk::clone_weight(ctx, ml, "encoder.pre_encode.proj.weight"); - ggml_tensor* x = ggml_mul_mat(ctx, proj_w, stacked); - - // --- 2. embed_norm --- - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.embed_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.embed_norm.bias"); - ggml_tensor* y = ggml_norm(ctx, x, ln_eps); - x = ggml_add(ctx, ggml_mul(ctx, y, g), b); - - // Transpose to channels-first: [d_model, T_enc] - // pre_encode returns channels-first: emb[c*T_enc + t] - x = ggml_cont(ctx, ggml_transpose(ctx, x)); - return x; - }, emb); - - assert(ok && "diarization pre_encode graph failed"); - (void)ok; - - d_model = d_model_; - T_enc = Tp; + // FeatureStacking: [C, T] -> [T, C] -> reshape [T/f, C*f] (f frames stacked). + ggml_tensor* x = ggml_cont(ctx, ggml_transpose(ctx, mel)); // ne=[n_mels, T_padded] + x = ggml_reshape_2d(ctx, x, (int64_t)n_mels_ * factor, T_padded / factor); + return ggml_mul_mat(ctx, pk::clone_weight(ctx, ml_, "encoder.pre_encode.proj.weight"), x); } -void DiarizationEncoder::transformer_forward(const std::vector& emb, int d_model, int T_enc, - std::vector& enc_out) const { - assert(d_model == d_model_); - assert((int)emb.size() == d_model * T_enc); +// x: pre-encoded ne=[d_model, T]; pos: I32 [T]. embed_norm -> blocks -> +// final_norm, returning ne=[d_model, T]. embed_norm lives here, not in +// pre_encode, because NeMo applies it after the (bypassable) pre-encoder: +// the streaming speaker cache holds pre-norm embeddings. +ggml_tensor* DiarizationEncoder::build_blocks(ggml_context* ctx, ggml_tensor* x, + ggml_tensor* pos) const { + const int d = d_model_, H = n_heads_, hd = head_dim_; + const int64_t T = x->ne[1]; + const float scale = 1.0f / std::sqrt((float)hd); + if (pre_block_norm_) x = layer_norm(ctx, ml_, x, "encoder.embed_norm", ln_eps_); + + for (int i = 0; i < n_layers_; ++i) { + const std::string base = "encoder.layers." + std::to_string(i) + "."; + + // Attention: x = x + out_proj(attn(norm1(x))) + ggml_tensor* h = layer_norm(ctx, ml_, x, base + "norm1", ln_eps_); + ggml_tensor* qkv = linear(ctx, ml_, h, base + "attn.w_qkv"); // ne=[3d, T] + const size_t row = qkv->nb[1]; + ggml_tensor* q = ggml_view_3d(ctx, qkv, hd, H, T, hd * sizeof(float), row, 0); + ggml_tensor* k = ggml_view_3d(ctx, qkv, hd, H, T, hd * sizeof(float), row, + (size_t)d * sizeof(float)); + ggml_tensor* v = ggml_view_3d(ctx, qkv, hd, H, T, hd * sizeof(float), row, + (size_t)2 * d * sizeof(float)); + q = ggml_rope_ext(ctx, ggml_cont(ctx, q), pos, nullptr, n_rot_, GGML_ROPE_TYPE_NEOX, + 0, rope_base_, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + k = ggml_rope_ext(ctx, ggml_cont(ctx, k), pos, nullptr, n_rot_, GGML_ROPE_TYPE_NEOX, + 0, rope_base_, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); + + // flash_attn_ext takes q/k/v as [hd, T, H] and returns [hd, H, T] + // (heads already interleaved per frame), which reshapes to [d, T]. + q = ggml_permute(ctx, q, 0, 2, 1, 3); + k = ggml_permute(ctx, k, 0, 2, 1, 3); + v = ggml_permute(ctx, ggml_cont(ctx, v), 0, 2, 1, 3); + ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, scale, 0.0f, 0.0f); + attn = ggml_reshape_2d(ctx, ggml_cont(ctx, attn), d, T); + x = ggml_add(ctx, x, linear(ctx, ml_, attn, base + "attn.out_proj")); + + // Feed-forward: x = x + W2 gelu(W1 norm2(x)) + h = layer_norm(ctx, ml_, x, base + "norm2", ln_eps_); + h = ggml_gelu(ctx, linear(ctx, ml_, h, base + "ffn.net.0")); + x = ggml_add(ctx, x, linear(ctx, ml_, h, base + "ffn.net.3")); + } + return layer_norm(ctx, ml_, x, "encoder.final_norm", ln_eps_); +} - const ModelLoader& ml = ml_; - const int d = d_model_; - const int H = n_heads_; - const int hd = head_dim_; - const int nls = n_layers_; - const float ln_eps = ln_eps_; - const float rope_base = rope_base_; - const int n_rot = (int)(hd * rotary_fraction_); +// Copy mel [n_mels, T] into a zero-padded [n_mels, T_padded] graph input. +static ggml_tensor* mel_input(ggml_context* ctx, GraphInputPool& pool, + const std::vector& mel, int n_mels, int T, int T_padded) { + std::vector& padded = pool.alloc_f32((size_t)n_mels * T_padded); + for (int m = 0; m < n_mels; ++m) + std::copy_n(mel.begin() + (size_t)m * T, T, padded.begin() + (size_t)m * T_padded); + int64_t ne[2] = {T_padded, n_mels}; + return pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, ne, padded.data(), + padded.size() * sizeof(float)); +} - std::vector positions(T_enc); - for (int i = 0; i < T_enc; ++i) positions[i] = i; +static std::vector positions(int T) { + std::vector p(T); + for (int i = 0; i < T; ++i) p[i] = i; + return p; +} - pk::ensure_weights_realized(ml); +void DiarizationEncoder::forward(const std::vector& mel, int n_mels, int T, + std::vector& enc_out, int& T_enc) const { + if (n_mels != n_mels_ || mel.size() != (size_t)n_mels * T || T <= 0) + throw std::runtime_error("parakeet: diarization encoder got a bad mel shape"); + const int f = subsampling_factor_; + const int T_padded = (T + f - 1) / f * f; + T_enc = T_padded / f; + const std::vector pos_data = positions(T_enc); + + pk::ensure_weights_realized(ml_); GraphInputPool pool; + const bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + ggml_tensor* x = build_pre_encode(ctx, mel_input(ctx, pool, mel, n_mels, T, T_padded), + T_padded); + int64_t pos_ne[1] = {T_enc}; + ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, + const_cast(pos_data.data()), + pos_data.size() * sizeof(int32_t)); + return build_blocks(ctx, x, pos); + }, enc_out); + if (!ok) throw std::runtime_error("parakeet: diarization encoder graph failed"); +} - bool ok = pk::run_graph(0, 0, - [&](ggml_context* ctx) -> ggml_tensor* { - // Input: emb is channels-first [d_model, T_enc] → emb[c*T_enc + t] - // ggml column-major: ne[0]=T_enc (fastest), ne[1]=d_model - // → flat[t + c*T_enc] = emb[c*T_enc + t] ✓ - int64_t emb_ne[2] = {T_enc, d}; - ggml_tensor* x = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, emb_ne, - const_cast(emb.data()), (size_t)d * T_enc * sizeof(float)); - // Transpose to time-major: ne[0]=d_model, ne[1]=T_enc - // (same layout as forward() uses after pre-encode) - x = ggml_cont(ctx, ggml_transpose(ctx, x)); - // x: ne[0]=d, ne[1]=T_enc — time-major - - // Position tensor for RoPE - int64_t pos_ne[1] = {(int64_t)T_enc}; - ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, - positions.data(), (size_t)T_enc * sizeof(int32_t)); - - // N × TransformerBlock (pre-norm) - for (int i = 0; i < nls; ++i) { - std::string base = "encoder.layers." + std::to_string(i) + "."; - - // norm1 - { - ggml_tensor* g = pk::clone_weight(ctx, ml, (base + "norm1.weight").c_str()); - ggml_tensor* b = pk::clone_weight(ctx, ml, (base + "norm1.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_add(ctx, ggml_mul(ctx, h, g), b); - - // Fused QKV (no bias) - ggml_tensor* qkv_w = pk::clone_weight(ctx, ml, (base + "attn.w_qkv.weight").c_str()); - ggml_tensor* qkv = ggml_mul_mat(ctx, qkv_w, h); - qkv = ggml_cont(ctx, qkv); - qkv = ggml_reshape_3d(ctx, qkv, d, 3, T_enc); - - size_t ts = (size_t)3 * d * sizeof(float); - ggml_tensor* q = ggml_view_2d(ctx, qkv, d, T_enc, ts, 0); - ggml_tensor* k = ggml_view_2d(ctx, qkv, d, T_enc, ts, (size_t)d * sizeof(float)); - ggml_tensor* v = ggml_view_2d(ctx, qkv, d, T_enc, ts, (size_t)2 * d * sizeof(float)); - - q = ggml_reshape_3d(ctx, ggml_cont(ctx, q), hd, H, T_enc); - k = ggml_reshape_3d(ctx, ggml_cont(ctx, k), hd, H, T_enc); - v = ggml_reshape_3d(ctx, ggml_cont(ctx, v), hd, H, T_enc); - - q = ggml_rope_ext(ctx, q, pos, nullptr, n_rot, - GGML_ROPE_TYPE_NEOX, 0, rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - k = ggml_rope_ext(ctx, k, pos, nullptr, n_rot, - GGML_ROPE_TYPE_NEOX, 0, rope_base, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); - - q = ggml_permute(ctx, q, 0, 2, 1, 3); - k = ggml_permute(ctx, k, 0, 2, 1, 3); - v = ggml_permute(ctx, v, 0, 2, 1, 3); - - float scale = 1.0f / std::sqrt((float)hd); - ggml_tensor* attn = ggml_flash_attn_ext(ctx, q, k, v, nullptr, - scale, 0.0f, 0.0f); - attn = ggml_cont(ctx, attn); - attn = ggml_reshape_2d(ctx, attn, (int64_t)d, (int64_t)T_enc); - - ggml_tensor* op_w = pk::clone_weight(ctx, ml, (base + "attn.out_proj.weight").c_str()); - attn = ggml_mul_mat(ctx, op_w, attn); - ggml_tensor* op_b = pk::clone_weight_opt(ctx, ml, (base + "attn.out_proj.bias").c_str()); - if (op_b) attn = ggml_add(ctx, attn, op_b); - - x = ggml_add(ctx, x, attn); - } - - // norm2 + FFN - { - ggml_tensor* g = pk::clone_weight(ctx, ml, (base + "norm2.weight").c_str()); - ggml_tensor* b = pk::clone_weight(ctx, ml, (base + "norm2.bias").c_str()); - ggml_tensor* h = ggml_norm(ctx, x, ln_eps); - h = ggml_add(ctx, ggml_mul(ctx, h, g), b); - - ggml_tensor* f0_w = pk::clone_weight(ctx, ml, (base + "ffn.net.0.weight").c_str()); - h = ggml_mul_mat(ctx, f0_w, h); - ggml_tensor* f0_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.0.bias").c_str()); - if (f0_b) h = ggml_add(ctx, h, f0_b); - h = ggml_gelu(ctx, h); - - ggml_tensor* f3_w = pk::clone_weight(ctx, ml, (base + "ffn.net.3.weight").c_str()); - h = ggml_mul_mat(ctx, f3_w, h); - ggml_tensor* f3_b = pk::clone_weight_opt(ctx, ml, (base + "ffn.net.3.bias").c_str()); - if (f3_b) h = ggml_add(ctx, h, f3_b); - - x = ggml_add(ctx, x, h); - } - } - - // final_norm - { - ggml_tensor* g = pk::clone_weight(ctx, ml, "encoder.final_norm.weight"); - ggml_tensor* b = pk::clone_weight(ctx, ml, "encoder.final_norm.bias"); - ggml_tensor* y = ggml_norm(ctx, x, ln_eps); - x = ggml_add(ctx, ggml_mul(ctx, y, g), b); - } - - // Transpose to channels-first - x = ggml_cont(ctx, ggml_transpose(ctx, x)); - return x; - }, enc_out); +void DiarizationEncoder::pre_encode(const std::vector& mel, int n_mels, int T, + std::vector& emb, int& T_enc) const { + if (n_mels != n_mels_ || mel.size() != (size_t)n_mels * T || T <= 0) + throw std::runtime_error("parakeet: diarization pre_encode got a bad mel shape"); + const int f = subsampling_factor_; + const int T_padded = (T + f - 1) / f * f; + T_enc = T_padded / f; + + pk::ensure_weights_realized(ml_); + GraphInputPool pool; + const bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + return build_pre_encode(ctx, mel_input(ctx, pool, mel, n_mels, T, T_padded), T_padded); + }, emb); + if (!ok) throw std::runtime_error("parakeet: diarization pre_encode graph failed"); +} - assert(ok && "diarization transformer_forward graph failed"); - (void)ok; +void DiarizationEncoder::transformer_forward(const std::vector& emb, int T_enc, + std::vector& enc_out) const { + if (emb.size() != (size_t)d_model_ * T_enc || T_enc <= 0) + throw std::runtime_error("parakeet: diarization transformer got a bad input shape"); + const std::vector pos_data = positions(T_enc); + + pk::ensure_weights_realized(ml_); + const bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { + int64_t ne[2] = {d_model_, T_enc}; + ggml_tensor* x = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, ne, + const_cast(emb.data()), + emb.size() * sizeof(float)); + int64_t pos_ne[1] = {T_enc}; + ggml_tensor* pos = pk::graph_input_tensor(ctx, GGML_TYPE_I32, 1, pos_ne, + const_cast(pos_data.data()), + pos_data.size() * sizeof(int32_t)); + return build_blocks(ctx, x, pos); + }, enc_out); + if (!ok) throw std::runtime_error("parakeet: diarization transformer graph failed"); } } // namespace pk diff --git a/src/diarization_encoder.hpp b/src/diarization_encoder.hpp index 6fc1e49..c559341 100644 --- a/src/diarization_encoder.hpp +++ b/src/diarization_encoder.hpp @@ -10,62 +10,59 @@ namespace pk { // DiarizationEncoder — pre-LN RoPE Transformer encoder for Nemotron-3-Diarization. // // This is NOT the FastConformer encoder used by ASR. Nemotron-3-Diarization uses -// a TransformerEncoder (not ConformerEncoder) with: -// - FeatureStacking subsampling (8× stack → Linear, no bias) -// - Pre-block LayerNorm (embed_norm) -// - N × TransformerBlock (pre-norm): x = x + attn(norm1(x)); x = x + ffn(norm2(x)) -// - MultiHeadAttention: fused QKV (no bias) → RoPE → flash_attn → out_proj (bias) -// - FeedForward: Linear → GELU → Linear (both with bias) -// - Post-block LayerNorm (final_norm) -// - RoPE: GPT-NeoX convention (GGML_ROPE_TYPE_NEOX), theta=10000, rotary_fraction=1.0 +// a NeMo TransformerEncoder with: +// - FeatureStacking subsampling (8x stack of mel frames -> Linear, no bias) +// - embed_norm LayerNorm (pre_block_norm) +// - N x TransformerBlock (pre-norm): x = x + attn(norm1(x)); x = x + ffn(norm2(x)) +// - attention: fused QKV (optional bias) -> RoPE (GPT-NeoX) -> softmax attention +// -> out_proj (bias) +// - FeedForward: Linear -> GELU -> Linear (both with bias) +// - final_norm LayerNorm // -// Input: mel features [n_mels, T] (row-major: mel[m*T + t]) -// Output: enc_out [d_model, T_enc] (row-major: enc_out[c*T_enc + t], channels-first) +// All sequences are TIME-MAJOR row-major [T, d_model] (x[t*d_model + c]), which +// is ggml's natural ne=[d_model, T] layout, so no transposes are needed between +// stages. The mel input keeps the frontend's [n_mels, T] layout. class DiarizationEncoder { public: explicit DiarizationEncoder(const ModelLoader& ml); - // mel: row-major [n_mels, T] — mel[m*T + t] - // enc_out: row-major [d_model, T_enc] — enc_out[c*T_enc + t] (channels-first) + // Full encoder: mel [n_mels, T] (mel[m*T + t]) -> enc_out [T_enc, d_model]. + // T_enc = ceil(T / subsampling). void forward(const std::vector& mel, int n_mels, int T, - std::vector& enc_out, int& d_model, int& T_enc) const; + std::vector& enc_out, int& T_enc) const; - // --- Streaming split: pre_encode + transformer_forward --- - - // Pre-encoder: FeatureStacking + Linear(1024→512) + embed_norm. - // mel: row-major [n_mels, T] — mel[m*T + t] - // emb: row-major [d_model, T_enc] — emb[c*T_enc + t] (channels-first) - // T_enc = T_padded / subsampling_factor + // Streaming split. pre_encode = FeatureStacking + projection (no + // embed_norm), i.e. what NeMo stores in the speaker cache / FIFO. + // mel [n_mels, T] -> emb [T_enc, d_model] void pre_encode(const std::vector& mel, int n_mels, int T, - std::vector& emb, int& d_model, int& T_enc) const; + std::vector& emb, int& T_enc) const; - // Transformer blocks + final_norm (the second half of the encoder). - // emb: row-major [d_model, T_enc] — emb[c*T_enc + t] (channels-first) - // enc_out: row-major [d_model, T_enc] — enc_out[c*T_enc + t] - void transformer_forward(const std::vector& emb, int d_model, int T_enc, + // embed_norm + transformer blocks + final_norm over pre-encoded embeddings + // (NeMo frontend_encoder with bypass_pre_encode=True). + // emb [T_enc, d_model] -> enc_out [T_enc, d_model] + void transformer_forward(const std::vector& emb, int T_enc, std::vector& enc_out) const; int subsampling() const { return subsampling_factor_; } int n_mels() const { return n_mels_; } int d_model() const { return d_model_; } - int n_layers() const { return n_layers_; } - int n_heads() const { return n_heads_; } - int head_dim() const { return head_dim_; } private: + // Graph builders shared by the entry points above. + ggml_tensor* build_pre_encode(ggml_context* ctx, ggml_tensor* mel, int T_padded) const; + ggml_tensor* build_blocks(ggml_context* ctx, ggml_tensor* x, ggml_tensor* pos) const; + const ModelLoader& ml_; - int d_model_; // encoder d_model (512) - int n_layers_; // number of transformer blocks (31) - int n_heads_; // attention heads (8) - int head_dim_; // d_model / n_heads (64) - int ff_dim_; // feed-forward inner dim (2048) - int subsampling_factor_; // FeatureStacking factor (8) - int n_mels_; // mel features (128) - bool qkv_bias_; // QKV projection bias (false) - bool pre_block_norm_; // apply embed_norm before blocks (true) - float rope_base_; // RoPE theta (10000.0) - float rotary_fraction_; // fraction of head_dim rotated (1.0) - float ln_eps_; // LayerNorm epsilon (1e-5) + int d_model_; + int n_layers_; + int n_heads_; + int head_dim_; + int subsampling_factor_; + int n_mels_; + bool pre_block_norm_; + float rope_base_; + int n_rot_; // rotated dims per head (head_dim * rotary_fraction) + float ln_eps_; }; } // namespace pk diff --git a/src/diarization_head.cpp b/src/diarization_head.cpp index 0ea2ffc..d61d003 100644 --- a/src/diarization_head.cpp +++ b/src/diarization_head.cpp @@ -2,7 +2,6 @@ #include "ggml_graph.hpp" #include "backend.hpp" #include "ggml.h" -#include #include #include #include @@ -17,52 +16,32 @@ DiarizationHead::DiarizationHead(const ModelLoader& ml) : ml_(ml) { upsample_ = (int)cfg.diarization.upsample_factor; } -void DiarizationHead::forward(const std::vector& enc_out, int d_model, int T_enc, - std::vector& probs, int& n_spk, int& T_out) const { - assert(d_model == d_model_); - assert((int)enc_out.size() == d_model * T_enc); +void DiarizationHead::forward(const std::vector& enc_out, int T_enc, + std::vector& probs, int& n_spk, int& T_out) const { + if (T_enc <= 0 || enc_out.size() != (size_t)d_model_ * T_enc) + throw std::runtime_error("parakeet: diarization head got a bad input shape"); n_spk = n_spk_; const int up = upsample_; T_out = T_enc * up; - // Memory budget: encoder_proj [d_model, tf], subpixel conv [tf*up, tf, 3], - // intermediate [tf*up, T_enc], speaker linears, output [n_spk, T_out]. - const size_t mem_bytes = - (size_t)128 * 1024 * 1024 + - (size_t)(d_model * tf_d_model_ + tf_d_model_ * up * tf_d_model_ * 3 + - tf_d_model_ * up * T_enc + tf_d_model_ * T_out + - n_spk_ * T_out) * sizeof(float) * 4; - const ModelLoader& ml = ml_; const int tf = tf_d_model_; - const int ns = n_spk_; pk::ensure_weights_realized(ml); - bool ok = pk::run_graph(mem_bytes, /*n_threads=*/4, + const bool ok = pk::run_graph(0, 0, [&](ggml_context* ctx) -> ggml_tensor* { - // ---- Input: enc_out [d_model, T_enc] row-major, enc_out[c*T_enc + t] - // ggml: ne[0]=T_enc (fastest), ne[1]=d_model - int64_t xt_ne[2] = {T_enc, d_model}; - ggml_tensor* xt = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, xt_ne, - enc_out.data(), (size_t)d_model * T_enc * sizeof(float)); - - // ---- encoder_proj: Linear(d_model → tf_d_model) ---- - // Weight: PyTorch [tf, d_model] → ggml ne[0]=d_model, ne[1]=tf - ggml_tensor* ep_w = ml.tensor("sortformer_modules.encoder_proj.weight"); - if (!ep_w) throw std::runtime_error("parakeet: missing sortformer_modules.encoder_proj.weight"); - ggml_tensor* W_proj = ggml_reshape_2d(ctx, ep_w, d_model, tf); - - // Transpose xt so ne[0]=d_model (contraction dim) - ggml_tensor* xt_t = ggml_cont(ctx, ggml_transpose(ctx, xt)); - // xt_t: ne[0]=d_model, ne[1]=T_enc - - ggml_tensor* proj = ggml_mul_mat(ctx, W_proj, xt_t); - // proj: ne[0]=tf, ne[1]=T_enc - - // Add encoder_proj bias [tf] - ggml_tensor* ep_b = ml.tensor("sortformer_modules.encoder_proj.bias"); + // enc_out is time-major [T_enc, d_model] = ggml ne=[d_model, T_enc]. + int64_t x_ne[2] = {d_model_, T_enc}; + ggml_tensor* x = pk::graph_input_tensor(ctx, GGML_TYPE_F32, 2, x_ne, + const_cast(enc_out.data()), + enc_out.size() * sizeof(float)); + + // ---- encoder_proj: Linear(d_model -> tf) ---- + ggml_tensor* ep_w = pk::clone_weight(ctx, ml, "sortformer_modules.encoder_proj.weight"); + ggml_tensor* proj = ggml_mul_mat(ctx, ep_w, x); // ne=[tf, T_enc] + ggml_tensor* ep_b = pk::clone_weight_opt(ctx, ml, "sortformer_modules.encoder_proj.bias"); if (ep_b) proj = ggml_add(ctx, proj, ep_b); // ---- subpixel_upsample: Conv1d(tf -> tf*up, k=3, pad=1) + bias ---- @@ -72,8 +51,7 @@ void DiarizationHead::forward(const std::vector& enc_out, int d_model, in conv_in = ggml_reshape_3d(ctx, conv_in, T_enc, tf, 1); // GGUF stores the PyTorch [OC, IC, k] weight as ggml ne=[k, IC, OC]. - ggml_tensor* spk_w = ml.tensor("sortformer_modules.subpixel_upsample.weight"); - if (!spk_w) throw std::runtime_error("parakeet: missing sortformer_modules.subpixel_upsample.weight"); + ggml_tensor* spk_w = pk::clone_weight(ctx, ml, "sortformer_modules.subpixel_upsample.weight"); ggml_tensor* cols = ggml_im2col(ctx, spk_w, conv_in, /*s0*/1, /*s1*/0, /*p0*/1, /*p1*/0, /*d0*/1, /*d1*/0, /*is_2D*/false, GGML_TYPE_F32); @@ -85,7 +63,7 @@ void DiarizationHead::forward(const std::vector& enc_out, int d_model, in ggml_tensor* conv_out = ggml_mul_mat(ctx, w2d, cols); // conv_out: ne=[OC=tf*up, T_enc], flat[c + t*OC] - ggml_tensor* spk_b = ml.tensor("sortformer_modules.subpixel_upsample.bias"); + ggml_tensor* spk_b = pk::clone_weight_opt(ctx, ml, "sortformer_modules.subpixel_upsample.bias"); if (spk_b) conv_out = ggml_add(ctx, conv_out, spk_b); // Subpixel shuffle, NeMo SortformerModules.upsample_hidden: @@ -96,64 +74,21 @@ void DiarizationHead::forward(const std::vector& enc_out, int d_model, in ggml_tensor* upsampled = ggml_reshape_2d(ctx, conv_out, tf, (int64_t)T_enc * up); // ne[0]=tf, ne[1]=T_out - // ---- forward_speaker_logits: relu → Linear(tf→tf) → relu → Linear(tf→ns) → sigmoid ---- - - // First ReLU + // ---- forward_speaker_logits: relu -> Linear(tf->tf) -> relu -> Linear(tf->ns) -> sigmoid ---- ggml_tensor* h = ggml_relu(ctx, upsampled); - - // first_hidden_to_hidden: Linear(tf → tf) - ggml_tensor* fh_w = ml.tensor("sortformer_modules.first_hidden_to_hidden.weight"); - if (!fh_w) throw std::runtime_error("parakeet: missing sortformer_modules.first_hidden_to_hidden.weight"); - ggml_tensor* W1 = ggml_reshape_2d(ctx, fh_w, tf, tf); - // W1: ne[0]=tf, ne[1]=tf. h: ne[0]=tf, ne[1]=T_out - h = ggml_mul_mat(ctx, W1, h); - // h: ne[0]=tf, ne[1]=T_out - ggml_tensor* fh_b = ml.tensor("sortformer_modules.first_hidden_to_hidden.bias"); + h = ggml_mul_mat(ctx, pk::clone_weight(ctx, ml, "sortformer_modules.first_hidden_to_hidden.weight"), h); + ggml_tensor* fh_b = pk::clone_weight_opt(ctx, ml, "sortformer_modules.first_hidden_to_hidden.bias"); if (fh_b) h = ggml_add(ctx, h, fh_b); - - // Second ReLU h = ggml_relu(ctx, h); - - // single_hidden_to_spks: Linear(tf → ns) - ggml_tensor* ss_w = ml.tensor("sortformer_modules.single_hidden_to_spks.weight"); - if (!ss_w) throw std::runtime_error("parakeet: missing sortformer_modules.single_hidden_to_spks.weight"); - ggml_tensor* W2 = ggml_reshape_2d(ctx, ss_w, tf, ns); - // W2: ne[0]=tf, ne[1]=ns. h: ne[0]=tf, ne[1]=T_out - h = ggml_mul_mat(ctx, W2, h); - // h: ne[0]=ns, ne[1]=T_out - ggml_tensor* ss_b = ml.tensor("sortformer_modules.single_hidden_to_spks.bias"); + h = ggml_mul_mat(ctx, pk::clone_weight(ctx, ml, "sortformer_modules.single_hidden_to_spks.weight"), h); + ggml_tensor* ss_b = pk::clone_weight_opt(ctx, ml, "sortformer_modules.single_hidden_to_spks.bias"); if (ss_b) h = ggml_add(ctx, h, ss_b); - - // Sigmoid → speaker probabilities - ggml_tensor* sig = ggml_sigmoid(ctx, h); - // sig: ne[0]=ns, ne[1]=T_out → memory: probs[t*ns + s] - // postprocess expects row-major: probs[s*T_out + t] - // Transpose to ne=[T_out, ns] + cont → flat[t + s*T_out] - sig = ggml_cont(ctx, ggml_transpose(ctx, sig)); - - return sig; + // ne=[ns, T_out] -> speaker-major [ns][T_out] for the callers. + return ggml_cont(ctx, ggml_transpose(ctx, ggml_sigmoid(ctx, h))); }, probs); - assert(ok && "diarization_head graph failed"); - (void)ok; -} - -void DiarizationHead::forward_range(const std::vector& enc_out, int d_model, int T_enc_total, - int start_enc, int count_enc, - std::vector& probs, int& n_spk, int& T_out) const { - assert(d_model == d_model_); - assert(start_enc >= 0 && count_enc > 0 && start_enc + count_enc <= T_enc_total); - - // Extract the sub-range [start_enc, start_enc+count_enc) from enc_out. - // enc_out is channels-first: enc_out[c*T_enc_total + t]. - std::vector sub((size_t)d_model * count_enc); - for (int c = 0; c < d_model; ++c) - for (int t = 0; t < count_enc; ++t) - sub[(size_t)c * count_enc + t] = - enc_out[(size_t)c * T_enc_total + (start_enc + t)]; - - forward(sub, d_model, count_enc, probs, n_spk, T_out); + if (!ok) throw std::runtime_error("parakeet: diarization head graph failed"); } } // namespace pk diff --git a/src/diarization_head.hpp b/src/diarization_head.hpp index cf82d7d..e4c7703 100644 --- a/src/diarization_head.hpp +++ b/src/diarization_head.hpp @@ -7,7 +7,7 @@ namespace pk { // Diarization head — NeMo SortformerModules forward_speaker_logits + upsample. // // Architecture (offline path, transformer_encoder is None for Nemotron-3): -// enc_out [d_model, T_enc] (channels-first, from FastConformer encoder) +// enc_out [T_enc, d_model] (time-major, from DiarizationEncoder) // → encoder_proj: Linear(d_model → tf_d_model) → [tf_d_model, T_enc] // → subpixel_upsample: Conv1d(tf_d_model → tf_d_model*upsample, k=3, pad=1) // → reshape → [tf_d_model, T_enc * upsample] @@ -25,18 +25,12 @@ class DiarizationHead { public: explicit DiarizationHead(const ModelLoader& ml); - // enc_out: row-major [d_model, T_enc] — enc_out[c*T_enc + t] (channels-first) - // probs: row-major [n_speakers, T_out] — probs[s*T_out + t] (post-sigmoid) - void forward(const std::vector& enc_out, int d_model, int T_enc, + // enc_out: time-major [T_enc, d_model] (enc_out[t*d_model + c]) + // probs: speaker-major [n_speakers, T_enc * upsample] (probs[s*T_out + t]), + // post-sigmoid + void forward(const std::vector& enc_out, int T_enc, std::vector& probs, int& n_spk, int& T_out) const; - // Compute the per-frame probabilities for a sub-range of the encoder output. - // Same as forward() but operates on enc_out[start_enc .. start_enc+count_enc-1]. - // Used by the streaming path to get probs for just the chunk portion. - void forward_range(const std::vector& enc_out, int d_model, int T_enc_total, - int start_enc, int count_enc, - std::vector& probs, int& n_spk, int& T_out) const; - private: const ModelLoader& ml_; int d_model_; // encoder d_model (512) diff --git a/src/diarization_streaming.cpp b/src/diarization_streaming.cpp index e392c4c..4cf82fb 100644 --- a/src/diarization_streaming.cpp +++ b/src/diarization_streaming.cpp @@ -1,515 +1,330 @@ #include "diarization_streaming.hpp" #include "backend.hpp" -#include "ggml_graph.hpp" #include "ggml.h" +#include "ggml-backend.h" #include -#include #include #include +#include #include namespace pk { +namespace { + +constexpr float kInf = std::numeric_limits::infinity(); + +// Append rows [lo, hi) of a row-major [*, width] buffer to `dst`. +void append_rows(std::vector& dst, const std::vector& src, int width, + int lo, int hi) { + dst.insert(dst.end(), src.begin() + (size_t)lo * width, src.begin() + (size_t)hi * width); +} + +// Drop the first `n` rows of a row-major [*, width] buffer. +void drop_rows(std::vector& v, int width, int n) { + v.erase(v.begin(), v.begin() + (size_t)n * width); +} + +float round2(float x) { return std::round(x * 100.0f) / 100.0f; } + +} // namespace + StreamingDiarization::StreamingDiarization(const ModelLoader& ml) - : ml_(ml) - , encoder_(ml) - , head_(ml) -{ + : ml_(ml), encoder_(ml), head_(ml) { const auto& cfg = ml.config(); const auto& d = cfg.diarization; + d_model_ = (int)cfg.d_model; + n_spk_ = (int)d.n_speakers; + subsampling_ = encoder_.subsampling(); + upsample_ = (int)d.upsample_factor; + n_mels_ = encoder_.n_mels(); + chunk_len_ = d.chunk_len; + spkcache_len_ = d.spkcache_len; + fifo_len_ = d.fifo_len; + update_period_ = d.spkcache_update_period; + sil_frames_per_spk_ = d.spkcache_sil_frames_per_spk; + frame_sec_ = d.frame_resolution_sec; + onset_ = d.onset_threshold; + offset_ = d.offset_threshold; + sil_threshold_ = d.sil_threshold; + pred_score_threshold_ = d.pred_score_threshold; + scores_boost_latest_ = d.scores_boost_latest; + strong_boost_rate_ = d.strong_boost_rate; + weak_boost_rate_ = d.weak_boost_rate; + min_pos_scores_rate_ = d.min_pos_scores_rate; + + if (chunk_len_ <= 0 || spkcache_len_ <= 0 || fifo_len_ < 0 || update_period_ <= 0 || + upsample_ != subsampling_ || spkcache_len_ / n_spk_ - sil_frames_per_spk_ <= 0) { + throw std::runtime_error("parakeet: invalid diarization streaming config"); + } - d_model_ = (int)cfg.d_model; - tf_d_model_ = (int)d.tf_d_model; - n_spk_ = (int)d.n_speakers; - n_layers_ = (int)cfg.n_layers; - n_heads_ = (int)cfg.n_heads; - head_dim_ = d_model_ / n_heads_; - ff_dim_ = (int)cfg.ff_dim; - subsampling_factor_ = encoder_.subsampling(); - upsample_factor_ = (int)d.upsample_factor; - n_mels_ = encoder_.n_mels(); - chunk_len_ = d.chunk_len; - spkcache_len_ = d.spkcache_len; - fifo_len_ = d.fifo_len; - frame_sec_ = d.frame_resolution_sec; - onset_threshold_ = d.onset_threshold; - offset_threshold_ = d.offset_threshold; - - // AOSC tuning - spkcache_sil_frames_per_spk_ = d.spkcache_sil_frames_per_spk; - sil_threshold_ = d.sil_threshold; - pred_score_threshold_ = d.pred_score_threshold; - scores_boost_latest_ = d.scores_boost_latest; - strong_boost_rate_ = d.strong_boost_rate; - weak_boost_rate_ = d.weak_boost_rate; - min_pos_scores_rate_ = d.min_pos_scores_rate; - max_index_ = 99999; - - // Load the learned silence embedding if present if (d.use_learnable_sil_emb) { const ggml_tensor* t = ml.tensor("sortformer_modules.learnable_sil_emb"); - if (t) { - has_silence_emb_ = true; - silence_emb_.resize(d_model_); - ensure_weights_realized(ml); - const float* data = (const float*)t->data; - std::memcpy(silence_emb_.data(), data, d_model_ * sizeof(float)); - } + if (!t || t->type != GGML_TYPE_F32 || ggml_nelements(t) != d_model_) + throw std::runtime_error("parakeet: learnable_sil_emb missing or not F32"); + ensure_weights_realized(ml); + learnable_sil_emb_.resize(d_model_); + ggml_backend_tensor_get(t, learnable_sil_emb_.data(), 0, d_model_ * sizeof(float)); + use_learnable_sil_emb_ = true; } - - mean_sil_emb_.assign(d_model_, 0.0f); - n_sil_frames_ = 0; - total_mel_frames_ = 0; - spkcache_enc_len_ = 0; - spkcache_preds_valid_ = false; - fifo_enc_len_ = 0; + reset(); } -StreamingDiarization::~StreamingDiarization() = default; - void StreamingDiarization::reset() { - spkcache_embs_.clear(); - spkcache_preds_.clear(); - spkcache_enc_len_ = 0; - spkcache_preds_valid_ = false; - fifo_embs_.clear(); - fifo_preds_.clear(); - fifo_enc_len_ = 0; + spkcache_.clear(); spkcache_preds_.clear(); + fifo_.clear(); fifo_preds_.clear(); + spkcache_compressed_ = false; mean_sil_emb_.assign(d_model_, 0.0f); n_sil_frames_ = 0; - total_mel_frames_ = 0; + frames_done_ = 0; + last_probs_.clear(); + last_frames_ = 0; + active_.assign(n_spk_, 0); + start_frame_.assign(n_spk_, 0); } std::vector StreamingDiarization::feed_mel_chunk( - const std::vector& mel_chunk, int n_mels, int n_frames, - bool is_last) { - - assert(n_mels == n_mels_); - assert((int)mel_chunk.size() == n_mels * n_frames); - - const int factor = subsampling_factor_; // 8 - // Pad the chunk to a multiple of subsampling_factor - const int pad = (factor - (n_frames % factor)) % factor; - const int T_padded = n_frames + pad; - const int chunk_enc_len = T_padded / factor; - - // --- 1. Pre-encode the chunk --- - std::vector chunk_emb; - int d_model = 0, T_enc_chunk = 0; - encoder_.pre_encode(mel_chunk, n_mels, n_frames, chunk_emb, d_model, T_enc_chunk); - assert(d_model == d_model_); - assert(T_enc_chunk == chunk_enc_len); - - // --- 2. Concatenate [spkcache | chunk] for the encoder input --- - // Build the combined mel input: [spkcache_mel | chunk_mel] - // But wait — we work at the EMBEDDING level for spkcache, not mel level. - // The spkcache stores pre-encoded embeddings. So we concatenate at the - // embedding level: [spkcache_embs | chunk_emb], then run transformer_forward. - - int spk_enc = spkcache_enc_len_; - int total_enc = spk_enc + chunk_enc_len; - - std::vector combined_emb((size_t)d_model_ * total_enc); - if (spk_enc > 0) { - std::memcpy(combined_emb.data(), spkcache_embs_.data(), - (size_t)d_model_ * spk_enc * sizeof(float)); - } - std::memcpy(combined_emb.data() + (size_t)d_model_ * spk_enc, - chunk_emb.data(), - (size_t)d_model_ * chunk_enc_len * sizeof(float)); - - // --- 3. Run the transformer over [spkcache | chunk] --- - std::vector enc_out; - encoder_.transformer_forward(combined_emb, d_model_, total_enc, enc_out); - - // --- 4. Run the diarization head on the CHUNK portion only --- - // enc_out is [d_model, total_enc] channels-first. - // The chunk portion starts at frame spk_enc. - std::vector probs; - int n_spk = 0, T_out = 0; - head_.forward_range(enc_out, d_model_, total_enc, spk_enc, chunk_enc_len, - probs, n_spk, T_out); - assert(n_spk == n_spk_); - - // --- 5. Also get probs for the spkcache portion (for scoring) --- - std::vector full_probs; - int full_n_spk = 0, full_T_out = 0; - if (spk_enc > 0) { - head_.forward_range(enc_out, d_model_, total_enc, 0, spk_enc, - full_probs, full_n_spk, full_T_out); - assert(full_n_spk == n_spk_); + const std::vector& mel, int n_mels, int n_frames, bool is_last) { + if (n_mels != n_mels_ || n_frames <= 0 || n_frames > chunk_mel_frames() || + mel.size() != (size_t)n_mels * n_frames) { + throw std::runtime_error("parakeet: bad streaming diarization chunk shape"); } - // --- 6. Post-process the chunk probs into segments --- - float time_offset = total_mel_frames_ * frame_sec_; - auto segments = postprocess_chunk(probs, n_spk_, T_out, time_offset); - - // --- 7. Update stream state (FIFO → spkcache → compress) --- - if (!is_last) { - // Build the full prediction output [n_spk, total_out] for the spkcache - // portion + chunk portion, used by stream_state_update. - int spk_out = 0; - if (spk_enc > 0 && full_T_out > 0) { - spk_out = full_T_out; + // 1. Pre-encode the chunk -> [cl, d]. + std::vector chunk_emb; + int cl = 0; + encoder_.pre_encode(mel, n_mels, n_frames, chunk_emb, cl); + + // 2. Transformer + head over [spkcache | fifo | chunk]. + const int S = (int)(spkcache_.size() / d_model_); + const int F = (int)(fifo_.size() / d_model_); + const int total = S + F + cl; + std::vector seq; + seq.reserve((size_t)total * d_model_); + seq.insert(seq.end(), spkcache_.begin(), spkcache_.end()); + seq.insert(seq.end(), fifo_.begin(), fifo_.end()); + seq.insert(seq.end(), chunk_emb.begin(), chunk_emb.end()); + + std::vector enc; + encoder_.transformer_forward(seq, total, enc); + std::vector hp; // [n_spk, total * up] + int n_spk = 0, T_hr = 0; + head_.forward(enc, total, hp, n_spk, T_hr); + + // 3. Encoder-resolution predictions [total, n_spk]: mean over each block of + // `up` high-resolution frames (NeMo downsample_preds). + std::vector preds((size_t)total * n_spk_); + for (int t = 0; t < total; ++t) + for (int s = 0; s < n_spk_; ++s) { + double acc = 0.0; + for (int u = 0; u < upsample_; ++u) acc += hp[(size_t)s * T_hr + t * upsample_ + u]; + preds[(size_t)t * n_spk_ + s] = (float)(acc / upsample_); } - // The chunk portion of the prediction for scoring during compression - stream_state_update(chunk_emb, chunk_enc_len, - probs, T_out, - full_probs, full_T_out); - } + // 4. This chunk's high-resolution slice, trimmed to the real mel frames. + const int base = (S + F) * upsample_; + last_frames_ = n_frames; + last_probs_.resize((size_t)n_spk_ * n_frames); + for (int s = 0; s < n_spk_; ++s) + std::copy_n(hp.begin() + (size_t)s * T_hr + base, n_frames, + last_probs_.begin() + (size_t)s * n_frames); - total_mel_frames_ += n_frames; - return segments; -} + // 5. Cache update for the next chunk. + if (!is_last) streaming_update(chunk_emb, cl, preds, S, F); -void StreamingDiarization::boost_topk_scores( - float* scores, int n_frames, int n_spk, - int k_per_spk, float scale_factor, float offset) const { - if (k_per_spk <= 0 || k_per_spk > n_frames) return; - float boost = -scale_factor * std::log(offset); + std::vector out; + track_segments(last_probs_, n_frames, is_last, out); + return out; +} - for (int s = 0; s < n_spk; ++s) { - std::vector> sv(n_frames); - for (int t = 0; t < n_frames; ++t) { - sv[t] = {scores[(size_t)t * n_spk + s], t}; +// SortformerModules.streaming_update (sync mode, lc = rc = 0). +void StreamingDiarization::streaming_update(const std::vector& chunk_emb, + int cl, const std::vector& preds, + int S, int F) { + const int d = d_model_, ns = n_spk_; + // FIFO predictions are refreshed from this step's output. + fifo_preds_.assign(preds.begin() + (size_t)S * ns, preds.begin() + (size_t)(S + F) * ns); + fifo_.insert(fifo_.end(), chunk_emb.begin(), chunk_emb.end()); + append_rows(fifo_preds_, preds, ns, S + F, S + F + cl); + + if (F + cl <= fifo_len_) return; + + int pop = std::max(update_period_, cl - fifo_len_ + F); + pop = std::min(pop, F + cl); + + if (!use_learnable_sil_emb_) { + // _get_silence_profile: running mean of popped frames whose summed + // speaker probability is below sil_threshold. + std::vector sum(d, 0.0); + long long count = 0; + for (int t = 0; t < pop; ++t) { + float p = 0.0f; + for (int s = 0; s < ns; ++s) p += fifo_preds_[(size_t)t * ns + s]; + if (p < sil_threshold_) { + ++count; + for (int c = 0; c < d; ++c) sum[c] += fifo_[(size_t)t * d + c]; + } } - std::nth_element(sv.begin(), sv.begin() + k_per_spk, sv.end(), - [](const std::pair& a, const std::pair& b) { - return a.first > b.first; - }); - for (int i = 0; i < k_per_spk; ++i) { - scores[(size_t)sv[i].second * n_spk + s] += boost; + if (count > 0) { + const long long n_new = n_sil_frames_ + count; + for (int c = 0; c < d; ++c) + mean_sil_emb_[c] = (float)(((double)mean_sil_emb_[c] * n_sil_frames_ + sum[c]) / n_new); + n_sil_frames_ = n_new; } } -} -void StreamingDiarization::update_silence_profile( - const float* pop_embs, const float* pop_preds, - int pop_len) { - for (int t = 0; t < pop_len; ++t) { - float pred_sum = 0; - for (int s = 0; s < n_spk_; ++s) { - pred_sum += pop_preds[(size_t)t * n_spk_ + s]; - } - if (pred_sum < sil_threshold_) { - ++n_sil_frames_; - float w_old = (float)(n_sil_frames_ - 1) / (float)n_sil_frames_; - float w_new = 1.0f / (float)n_sil_frames_; - for (int d = 0; d < d_model_; ++d) { - mean_sil_emb_[d] = w_old * mean_sil_emb_[d] + - w_new * pop_embs[(size_t)t * d_model_ + d]; - } - } + if (!spkcache_compressed_) { + // Until the first compression the cache predictions are this step's. + spkcache_preds_.assign(preds.begin(), preds.begin() + (size_t)S * ns); + } + append_rows(spkcache_, fifo_, d, 0, pop); + append_rows(spkcache_preds_, fifo_preds_, ns, 0, pop); + drop_rows(fifo_, d, pop); + drop_rows(fifo_preds_, ns, pop); + + if ((int)(spkcache_.size() / d) > spkcache_len_) { + compress_spkcache(); + spkcache_compressed_ = true; } } +// SortformerModules._compress_spkcache (eval: no speaker permutation, no noise). void StreamingDiarization::compress_spkcache() { - const int n_frames = spkcache_enc_len_; - // Target spkcache size in ENCODER frames - const int target_enc_len = spkcache_len_ / subsampling_factor_; - const int sil_per_spk = spkcache_sil_frames_per_spk_; - const int per_spk = target_enc_len / n_spk_ - sil_per_spk; + const int d = d_model_, ns = n_spk_; + const int n = (int)(spkcache_preds_.size() / ns); + const int per_spk = spkcache_len_ / ns - sil_frames_per_spk_; const int strong_k = (int)std::floor(per_spk * strong_boost_rate_); const int weak_k = (int)std::floor(per_spk * weak_boost_rate_); - const int min_pos_k = (int)std::floor(per_spk * min_pos_scores_rate_); - - // 1. Compute log-based importance scores [n_frames, n_spk] - std::vector scores((size_t)n_frames * n_spk_); - for (int t = 0; t < n_frames; ++t) { - const float* p = &spkcache_preds_[(size_t)t * n_spk_]; - float log_1_sum = 0; - for (int s = 0; s < n_spk_; ++s) - log_1_sum += std::log(std::max(1.0f - p[s], pred_score_threshold_)); - for (int s = 0; s < n_spk_; ++s) { - float lp = std::log(std::max(p[s], pred_score_threshold_)); - float l1p = std::log(std::max(1.0f - p[s], pred_score_threshold_)); - scores[(size_t)t * n_spk_ + s] = lp - l1p + log_1_sum - std::log(0.5f); + const int min_pos = (int)std::floor(per_spk * min_pos_scores_rate_); + const float* P = spkcache_preds_.data(); + + // _get_log_pred_scores + std::vector sc((size_t)n * ns); + for (int t = 0; t < n; ++t) { + float log1_sum = 0.0f; + for (int s = 0; s < ns; ++s) + log1_sum += std::log(std::max(1.0f - P[(size_t)t * ns + s], pred_score_threshold_)); + for (int s = 0; s < ns; ++s) { + const float p = P[(size_t)t * ns + s]; + sc[(size_t)t * ns + s] = std::log(std::max(p, pred_score_threshold_)) - + std::log(std::max(1.0f - p, pred_score_threshold_)) + + log1_sum - std::log(0.5f); } } - // 2. Disable non-speech scores (preds <= 0.5 → -inf) - for (int t = 0; t < n_frames; ++t) - for (int s = 0; s < n_spk_; ++s) - if (spkcache_preds_[(size_t)t * n_spk_ + s] <= 0.5f) - scores[(size_t)t * n_spk_ + s] = -INFINITY; - - // Disable non-positive scores if speaker has enough positive ones - for (int s = 0; s < n_spk_; ++s) { - int pos_cnt = 0; - for (int t = 0; t < n_frames; ++t) - if (scores[(size_t)t * n_spk_ + s] > 0) ++pos_cnt; - if (pos_cnt >= min_pos_k) { - for (int t = 0; t < n_frames; ++t) { - if (scores[(size_t)t * n_spk_ + s] <= 0 && - spkcache_preds_[(size_t)t * n_spk_ + s] > 0.5f) - scores[(size_t)t * n_spk_ + s] = -INFINITY; - } + // _disable_low_scores + for (int s = 0; s < ns; ++s) { + int pos = 0; + for (int t = 0; t < n; ++t) { + float& v = sc[(size_t)t * ns + s]; + if (!(P[(size_t)t * ns + s] > 0.5f)) v = -kInf; + if (v > 0.0f) ++pos; } - } - - // 3. Boost latest frames (beyond target_enc_len) - if (scores_boost_latest_ > 0) { - for (int t = target_enc_len; t < n_frames; ++t) - for (int s = 0; s < n_spk_; ++s) { - float& sc = scores[(size_t)t * n_spk_ + s]; - if (sc != -INFINITY) sc += scores_boost_latest_; + if (pos >= min_pos) + for (int t = 0; t < n; ++t) { + float& v = sc[(size_t)t * ns + s]; + if (!(v > 0.0f) && P[(size_t)t * ns + s] > 0.5f) v = -kInf; } } - // 4. Strong boost: top-K per speaker (scale=2) - boost_topk_scores(scores.data(), n_frames, n_spk_, strong_k, 2.0f, 0.5f); - - // 5. Weak boost: top-K per speaker (scale=1) - int wk = std::min(weak_k, n_frames); - boost_topk_scores(scores.data(), n_frames, n_spk_, wk, 1.0f, 0.5f); - - // 6. Add silence placeholder frames at end (+inf for each speaker) - int n_sil_pad = sil_per_spk; - int n_total = n_frames + n_sil_pad; - scores.resize((size_t)n_total * n_spk_); - for (int t = n_frames; t < n_total; ++t) - for (int s = 0; s < n_spk_; ++s) - scores[(size_t)t * n_spk_ + s] = INFINITY; - - // 7. Flatten as (n_spk, n_total) and find top target_enc_len entries - int flat_len = n_spk_ * n_total; - std::vector> flat(flat_len); - for (int s = 0; s < n_spk_; ++s) - for (int t = 0; t < n_total; ++t) - flat[(size_t)s * n_total + t] = {scores[(size_t)t * n_spk_ + s], - s * n_total + t}; - - std::nth_element(flat.begin(), flat.begin() + target_enc_len, flat.end(), - [](const std::pair& a, const std::pair& b) { - return a.first > b.first; - }); - - // Replace -inf entries with max_index, keep valid entries - std::vector topk_indices(target_enc_len); - for (int i = 0; i < target_enc_len; ++i) { - if (flat[i].first == -INFINITY) { - topk_indices[i] = max_index_; - } else { - topk_indices[i] = flat[i].second; - } - } - - // Sort to preserve original frame order - std::sort(topk_indices.begin(), topk_indices.end()); - - // Convert to frame indices and determine disabled mask - int n_frames_no_sil = n_total - n_sil_pad; - std::vector is_disabled(target_enc_len, false); - for (int i = 0; i < target_enc_len; ++i) { - if (topk_indices[i] == max_index_) { - is_disabled[i] = true; - } - topk_indices[i] = topk_indices[i] % n_total; - if (topk_indices[i] >= n_frames_no_sil) { - is_disabled[i] = true; - } - if (is_disabled[i]) { - topk_indices[i] = 0; // placeholder for gather + // Boost frames newly added since the last compression. + if (scores_boost_latest_ > 0.0f) + for (size_t i = (size_t)spkcache_len_ * ns; i < sc.size(); ++i) sc[i] += scores_boost_latest_; + + // _boost_topk_scores: add -scale*log(0.5) to each speaker's top-k frames. + auto boost = [&](int k, float scale) { + k = std::min(k, n); + if (k <= 0) return; + const float add = -scale * std::log(0.5f); + std::vector idx(n); + for (int s = 0; s < ns; ++s) { + for (int t = 0; t < n; ++t) idx[t] = t; + // Ties (common: confident frames clamp to the same score) go to + // the earlier frame, so the result is deterministic. + std::nth_element(idx.begin(), idx.begin() + (k - 1), idx.end(), [&](int a, int b) { + const float va = sc[(size_t)a * ns + s], vb = sc[(size_t)b * ns + s]; + return va != vb ? va > vb : a < b; + }); + for (int i = 0; i < k; ++i) sc[(size_t)idx[i] * ns + s] += add; } - } - - // 8. Gather embeddings and predictions - std::vector new_embs((size_t)target_enc_len * d_model_); - std::vector new_preds((size_t)target_enc_len * n_spk_); - - // Use learned silence emb if available, otherwise running mean - const float* sil_emb = has_silence_emb_ ? silence_emb_.data() : mean_sil_emb_.data(); - - for (int i = 0; i < target_enc_len; ++i) { - int tidx = topk_indices[i]; - if (is_disabled[i]) { - std::memcpy(&new_embs[(size_t)i * d_model_], sil_emb, d_model_ * sizeof(float)); - std::memset(&new_preds[(size_t)i * n_spk_], 0, n_spk_ * sizeof(float)); + }; + boost(strong_k, 2.0f); + boost(weak_k, 1.0f); + + // Append sil_frames_per_spk frames of +inf per speaker (reserved silence slots). + const int n_tot = n + sil_frames_per_spk_; + // _get_topk_indices over the speaker-major flattening (index = s*n_tot + t). + std::vector> flat((size_t)ns * n_tot); + for (int s = 0; s < ns; ++s) + for (int t = 0; t < n_tot; ++t) + flat[(size_t)s * n_tot + t] = {t < n ? sc[(size_t)t * ns + s] : kInf, s * n_tot + t}; + const int K = spkcache_len_; + std::nth_element(flat.begin(), flat.begin() + (K - 1), flat.end(), + [](const std::pair& a, const std::pair& b) { + return a.first != b.first ? a.first > b.first : a.second < b.second; + }); + constexpr int kMaxIndex = std::numeric_limits::max(); + std::vector top(K); + for (int i = 0; i < K; ++i) top[i] = flat[i].first == -kInf ? kMaxIndex : flat[i].second; + std::sort(top.begin(), top.end()); + + // _gather_spkcache_and_preds + const std::vector& sil = use_learnable_sil_emb_ ? learnable_sil_emb_ : mean_sil_emb_; + std::vector new_embs((size_t)K * d), new_preds((size_t)K * ns, 0.0f); + for (int i = 0; i < K; ++i) { + const int t = top[i] == kMaxIndex ? -1 : top[i] % n_tot; + if (t < 0 || t >= n) { + std::copy(sil.begin(), sil.end(), new_embs.begin() + (size_t)i * d); } else { - std::memcpy(&new_embs[(size_t)i * d_model_], - &spkcache_embs_[(size_t)tidx * d_model_], - d_model_ * sizeof(float)); - std::memcpy(&new_preds[(size_t)i * n_spk_], - &spkcache_preds_[(size_t)tidx * n_spk_], - n_spk_ * sizeof(float)); + std::copy_n(spkcache_.begin() + (size_t)t * d, d, new_embs.begin() + (size_t)i * d); + std::copy_n(spkcache_preds_.begin() + (size_t)t * ns, ns, new_preds.begin() + (size_t)i * ns); } } - - spkcache_embs_ = std::move(new_embs); - spkcache_preds_ = std::move(new_preds); - spkcache_enc_len_ = target_enc_len; + spkcache_.swap(new_embs); + spkcache_preds_.swap(new_preds); } -void StreamingDiarization::stream_state_update( - const std::vector& chunk_preenc, int chunk_enc_len, - const std::vector& chunk_preds, int chunk_out_len, - const std::vector& full_pred_out, int full_out_len) { - - int old_sc_len = spkcache_enc_len_; - int old_fifo_len = fifo_enc_len_; - - // With fifo_len=0, the FIFO immediately overflows on every chunk. - // The chunk embeddings are popped from the FIFO and appended to the spkcache. - - // Extract chunk predictions (chunk_preds is already [n_spk, chunk_out_len]) - // We need predictions in [chunk_enc_len, n_spk] format. - // chunk_out_len should equal chunk_enc_len * upsample_factor, but the - // predictions used for scoring are at encoder resolution, so we subsample. - // - // Actually, the spkcache stores ENCODER-resolution predictions. - // The diarization head outputs at upsampled resolution (T_out = chunk_enc_len * upsample). - // For spkcache scoring, NeMo uses the pre-sigmoid predictions at encoder resolution. - // We approximate by taking the mean of each upsample_factor block. - std::vector chunk_preds_enc((size_t)chunk_enc_len * n_spk_, 0.0f); - if (chunk_out_len == chunk_enc_len) { - // Already at encoder resolution - std::memcpy(chunk_preds_enc.data(), chunk_preds.data(), - chunk_preds.size() * sizeof(float)); - } else if (chunk_out_len > 0 && chunk_out_len % chunk_enc_len == 0) { - int up = chunk_out_len / chunk_enc_len; - for (int t = 0; t < chunk_enc_len; ++t) { - for (int s = 0; s < n_spk_; ++s) { - float sum = 0; - for (int u = 0; u < up; ++u) { - int idx = t * up + u; - sum += chunk_preds[(size_t)s * chunk_out_len + idx]; - } - chunk_preds_enc[(size_t)t * n_spk_ + s] = sum / up; - } - } - } - - // Append chunk to FIFO - int new_fifo_total = old_fifo_len + chunk_enc_len; - std::vector updated_fifo((size_t)(old_fifo_len + chunk_enc_len) * d_model_); - std::vector updated_fifo_preds((size_t)(old_fifo_len + chunk_enc_len) * n_spk_); - - if (old_fifo_len > 0) { - std::memcpy(updated_fifo.data(), fifo_embs_.data(), - (size_t)old_fifo_len * d_model_ * sizeof(float)); - std::memcpy(updated_fifo_preds.data(), fifo_preds_.data(), - (size_t)old_fifo_len * n_spk_ * sizeof(float)); - } - std::memcpy(updated_fifo.data() + (size_t)old_fifo_len * d_model_, - chunk_preenc.data(), - (size_t)chunk_enc_len * d_model_ * sizeof(float)); - std::memcpy(updated_fifo_preds.data() + (size_t)old_fifo_len * n_spk_, - chunk_preds_enc.data(), - (size_t)chunk_enc_len * n_spk_ * sizeof(float)); - - // FIFO target in encoder frames - int fifo_enc_target = fifo_len_ / subsampling_factor_; - - if (new_fifo_total > fifo_enc_target) { - int pop_out_len = spkcache_len_ / subsampling_factor_; // spkcache_update_period - pop_out_len = std::max(pop_out_len, chunk_enc_len - fifo_enc_target + old_fifo_len); - pop_out_len = std::min(pop_out_len, new_fifo_total); - - const float* pop_embs = updated_fifo.data(); - const float* pop_preds = updated_fifo_preds.data(); - - update_silence_profile(pop_embs, pop_preds, pop_out_len); - - int remaining_fifo = new_fifo_total - pop_out_len; - fifo_embs_.resize((size_t)remaining_fifo * d_model_); - fifo_preds_.resize((size_t)remaining_fifo * n_spk_); - if (remaining_fifo > 0) { - std::memcpy(fifo_embs_.data(), - updated_fifo.data() + (size_t)pop_out_len * d_model_, - (size_t)remaining_fifo * d_model_ * sizeof(float)); - std::memcpy(fifo_preds_.data(), - updated_fifo_preds.data() + (size_t)pop_out_len * n_spk_, - (size_t)remaining_fifo * n_spk_ * sizeof(float)); - } - fifo_enc_len_ = remaining_fifo; - - // Append popped frames to spkcache - int new_sc_len = old_sc_len + pop_out_len; - spkcache_embs_.resize((size_t)new_sc_len * d_model_); - std::memcpy(spkcache_embs_.data() + (size_t)old_sc_len * d_model_, - pop_embs, (size_t)pop_out_len * d_model_ * sizeof(float)); - - if (spkcache_preds_valid_) { - spkcache_preds_.resize((size_t)new_sc_len * n_spk_); - std::memcpy(spkcache_preds_.data() + (size_t)old_sc_len * n_spk_, - pop_preds, (size_t)pop_out_len * n_spk_ * sizeof(float)); - } - spkcache_enc_len_ = new_sc_len; - - // Check if compression needed - int target_enc = spkcache_len_ / subsampling_factor_; - if (new_sc_len > target_enc) { - if (!spkcache_preds_valid_) { - // First time: init spkcache_preds from full prediction output - spkcache_preds_.resize((size_t)new_sc_len * n_spk_); - // Copy predictions for old spkcache frames from full_pred_out - if (old_sc_len > 0 && full_out_len >= old_sc_len) { - // full_pred_out is [n_spk, full_out_len] — need to transpose to - // [old_sc_len, n_spk] - for (int t = 0; t < old_sc_len; ++t) - for (int s = 0; s < n_spk_; ++s) - spkcache_preds_[(size_t)t * n_spk_ + s] = - full_pred_out[(size_t)s * full_out_len + t]; - } - // Copy pop_out predictions (already in [pop_out_len, n_spk]) - std::memcpy(spkcache_preds_.data() + (size_t)old_sc_len * n_spk_, - pop_preds, (size_t)pop_out_len * n_spk_ * sizeof(float)); - spkcache_preds_valid_ = true; +// Hysteresis binarization carried across chunks, matching the offline +// DiarizationModel::postprocess on the concatenated probabilities. +void StreamingDiarization::track_segments(const std::vector& probs, int n_frames, + bool is_last, + std::vector& out) { + const size_t first = out.size(); + for (int s = 0; s < n_spk_; ++s) { + const float* p = probs.data() + (size_t)s * n_frames; + for (int t = 0; t < n_frames; ++t) { + const long long f = frames_done_ + t; + if (!active_[s] && p[t] >= onset_) { + active_[s] = 1; + start_frame_[s] = f; + } else if (active_[s] && p[t] < offset_) { + active_[s] = 0; + out.push_back({s, round2(start_frame_[s] * frame_sec_), round2(f * frame_sec_)}); } - compress_spkcache(); } - } else { - fifo_embs_ = std::move(updated_fifo); - fifo_preds_ = std::move(updated_fifo_preds); - fifo_enc_len_ = new_fifo_total; } -} - -std::vector StreamingDiarization::postprocess_chunk( - const std::vector& probs, int n_spk, int T_out, - float time_offset) const { - - std::vector segments; - - for (int s = 0; s < n_spk; ++s) { - const float* p = probs.data() + (size_t)s * T_out; - - bool active = false; - int start_frame = 0; - - for (int t = 0; t < T_out; ++t) { - const bool on = (p[t] >= onset_threshold_); - if (on && !active) { - start_frame = t; - active = true; - } else if (!on && active) { - float start_sec = time_offset + start_frame * frame_sec_; - float end_sec = time_offset + t * frame_sec_; - segments.push_back({s, start_sec, end_sec}); - active = false; + frames_done_ += n_frames; + if (is_last) { + for (int s = 0; s < n_spk_; ++s) + if (active_[s]) { + active_[s] = 0; + out.push_back({s, round2(start_frame_[s] * frame_sec_), + round2(frames_done_ * frame_sec_)}); } - } - if (active) { - float start_sec = time_offset + start_frame * frame_sec_; - float end_sec = time_offset + T_out * frame_sec_; - segments.push_back({s, start_sec, end_sec}); - } } - - std::sort(segments.begin(), segments.end(), + std::sort(out.begin() + first, out.end(), [](const StreamingSpeakerSegment& a, const StreamingSpeakerSegment& b) { - if (a.start != b.start) return a.start < b.start; - return a.speaker < b.speaker; + return a.start != b.start ? a.start < b.start : a.speaker < b.speaker; }); +} - for (auto& seg : segments) { - seg.start = std::round(seg.start * 100.0f) / 100.0f; - seg.end = std::round(seg.end * 100.0f) / 100.0f; - } - - return segments; +std::vector StreamingDiarization::open_segments() const { + std::vector out; + for (int s = 0; s < n_spk_; ++s) + if (active_[s]) + out.push_back({s, round2(start_frame_[s] * frame_sec_), round2(frames_done_ * frame_sec_)}); + return out; } } // namespace pk diff --git a/src/diarization_streaming.hpp b/src/diarization_streaming.hpp index 9cd8973..21e8007 100644 --- a/src/diarization_streaming.hpp +++ b/src/diarization_streaming.hpp @@ -2,161 +2,97 @@ #include "model_loader.hpp" #include "diarization_encoder.hpp" #include "diarization_head.hpp" -#include "mel.hpp" -#include -#include #include -#include namespace pk { -// A speaker segment emitted during streaming. Timestamps are wall-clock seconds -// from the start of the stream. +// A finished speaker segment on the stream's timeline (seconds from stream start). struct StreamingSpeakerSegment { int speaker; float start; float end; }; -// Streaming diarization state for the Nemotron-3-Diarization Sortformer. +// StreamingDiarization — NeMo Sortformer cache-aware streaming ("AOSC"), +// synchronous mode (SortformerEncLabelModel.forward_streaming_step + +// SortformerModules.streaming_update), for nvidia/Nemotron-3-Diarization. // -// The AOSC ("Attention-Only Streaming with Compression") mechanism: +// Each chunk of mel frames is pre-encoded (FeatureStacking + projection + +// embed_norm) and the transformer + speaker head run over +// [speaker cache | FIFO | chunk]. The chunk's slice of the high-resolution +// output is the result for that chunk; the downsampled predictions drive the +// FIFO -> speaker-cache update and the score-based cache compression that +// keeps the speaker identities stable across chunks. // -// 1. Audio is processed in chunks of `chunk_len` mel frames (264). -// 2. Each chunk is concatenated with the speaker cache: [spkcache | chunk]. -// The full bidirectional encoder runs over this concatenated sequence. -// 3. The encoder output for the chunk portion (last chunk_enc_len frames) -// is passed through the diarization head to get per-frame speaker probs. -// 4. The probs for the spkcache portion are stored for compression scoring. -// 5. After processing, the chunk embeddings are appended to a FIFO buffer. -// When the FIFO overflows (exceeds fifo_len), the oldest frames are popped -// and appended to the spkcache. -// 6. When the spkcache exceeds spkcache_len, AOSC compression is triggered: -// frames are scored per-speaker (log-odds), top-K are selected globally, -// and the spkcache is rebuilt to spkcache_len frames. +// chunk_len / spkcache_len / fifo_len / spkcache_update_period come from the +// GGUF and are in ENCODER frames (80 ms), as in NeMo. With the Nemotron-3 +// config a chunk is 264 encoder frames = 2112 mel frames = 21.12 s. // -// For Nemotron-3-Diarization: -// fifo_len=0, spkcache_len=264, chunk_len=264 -// subsampling_factor=8, upsample_factor=8 -// use_learnable_sil_emb=true -// -// With fifo_len=0, every chunk immediately overflows the FIFO, so every chunk's -// embeddings are appended to the spkcache and compression runs after every chunk. -// -// The streaming path reuses the SAME encoder and head as the offline path — -// only the chunking and spkcache management differ. +// The mel must be the un-normalized log-mel of the stream (NeMo does not +// peak-normalize in streaming mode), e.g. from pk::StreamingMel. class StreamingDiarization { public: explicit StreamingDiarization(const ModelLoader& ml); - ~StreamingDiarization(); - // Reset the stream state (clear spkcache, frame counter). void reset(); - // Feed one chunk of mel features [n_mels, n_frames] (row-major: - // mel[m*n_frames + t]). The caller must provide exactly `chunk_len` frames - // (or fewer for the final chunk). Returns the speaker segments for this - // chunk (with wall-clock timestamps). - // - // is_last marks the final chunk — the spkcache is not updated after it. + // Feed the next chunk: row-major [n_mels, n_frames] (mel[m*n_frames + t]), + // 0 < n_frames <= chunk_mel_frames(). Only the final chunk may be short. + // Returns the speaker segments that ENDED in this chunk; with is_last, + // every still-open segment is closed at the end of the stream. std::vector feed_mel_chunk( - const std::vector& mel_chunk, int n_mels, int n_frames, - bool is_last = false); + const std::vector& mel, int n_mels, int n_frames, bool is_last); + + // Speaker probabilities of the last fed chunk, speaker-major + // [n_speakers, last_chunk_frames()] (one frame per mel frame, 10 ms). + const std::vector& last_chunk_probs() const { return last_probs_; } + int last_chunk_frames() const { return last_frames_; } - // Chunk parameters (from GGUF config). - int chunk_len() const { return chunk_len_; } - int spkcache_len() const { return spkcache_len_; } - int fifo_len() const { return fifo_len_; } + // Segments that are still active at the current end of the stream, with + // `end` set to the stream time consumed so far. + std::vector open_segments() const; + + int chunk_mel_frames() const { return chunk_len_ * subsampling_; } int n_mels() const { return n_mels_; } int n_speakers() const { return n_spk_; } - - // The frame-to-second conversion: each output frame is hop_length/sample_rate - // seconds = 160/16000 = 0.01s. float frame_sec() const { return frame_sec_; } + // Mel frames consumed so far. + long long frames_done() const { return frames_done_; } private: + void streaming_update(const std::vector& chunk_emb, int chunk_frames, + const std::vector& preds, int spkcache_frames, + int fifo_frames); + void compress_spkcache(); + void track_segments(const std::vector& probs, int n_frames, bool is_last, + std::vector& out); + const ModelLoader& ml_; DiarizationEncoder encoder_; DiarizationHead head_; - int d_model_; // encoder d_model (512) - int tf_d_model_; // sortformer tf_d_model (192) - int n_spk_; // number of speakers (8) - int n_layers_; // transformer blocks (31) - int n_heads_; // attention heads (8) - int head_dim_; // d_model / n_heads (64) - int ff_dim_; // feed-forward dim (2048) - int subsampling_factor_; // 8 - int upsample_factor_; // 8 - int n_mels_; // 128 - int chunk_len_; // 264 mel frames - int spkcache_len_; // 264 mel frames - int fifo_len_; // 0 for Nemotron-3 - float frame_sec_; // 0.01s per output frame - float onset_threshold_; - float offset_threshold_; - - // --- AOSC streaming config --- - int spkcache_sil_frames_per_spk_; // 3 - float sil_threshold_; // 0.2 - float pred_score_threshold_; // 0.25 - float scores_boost_latest_; // 0.05 - float strong_boost_rate_; // 0.75 - float weak_boost_rate_; // 1.5 - float min_pos_scores_rate_; // 0.5 - int max_index_; // 99999 (placeholder for disabled slots) - - // --- Spkcache state --- - // Encoder embeddings [d_model, spkcache_enc_len] (channels-first: emb[c*len + t]) - std::vector spkcache_embs_; - std::vector spkcache_preds_; // [n_spk, spkcache_enc_len] - int spkcache_enc_len_ = 0; - bool spkcache_preds_valid_ = false; - - // --- FIFO state --- - // With fifo_len=0, this overflows every chunk. - std::vector fifo_embs_; // [d_model, fifo_enc_len] - std::vector fifo_preds_; // [n_spk, fifo_enc_len] - int fifo_enc_len_ = 0; - - // --- Silence profile --- - std::vector mean_sil_emb_; // [d_model] - int n_sil_frames_ = 0; - - // --- The learned silence embedding (model parameter) --- - bool has_silence_emb_ = false; - std::vector silence_emb_; // [d_model] - - // Running count of total mel frames consumed (for wall-clock timestamps). - int total_mel_frames_ = 0; - - // --- Internal helpers --- - - // AOSC: boost top-K scores per speaker - void boost_topk_scores(float* scores, int n_frames, int n_spk, - int k_per_spk, float scale_factor, float offset) const; - - // AOSC: compress spkcache from current length to spkcache_len_ frames. - void compress_spkcache(); - - // Update running silence profile from popped embeddings. - void update_silence_profile(const float* pop_embs, const float* pop_preds, - int pop_len); - - // Update stream state after processing one chunk (FIFO → spkcache → compress). - // chunk_preenc: pre-encoded embeddings for the chunk [d_model, chunk_enc_len] - // chunk_preds: per-speaker probs for the chunk [n_spk, chunk_out_len] - // full_pred_out: full prediction output for [spkcache | chunk] [n_spk, total_out_len] - // (used to extract spkcache predictions for scoring) - void stream_state_update( - const std::vector& chunk_preenc, int chunk_enc_len, - const std::vector& chunk_preds, int chunk_out_len, - const std::vector& full_pred_out, int full_out_len); - - // Post-process per-frame speaker probabilities into segments for this chunk. - std::vector postprocess_chunk( - const std::vector& probs, int n_spk, int T_out, - float time_offset) const; + int d_model_, n_spk_, subsampling_, upsample_, n_mels_; + int chunk_len_, spkcache_len_, fifo_len_, update_period_, sil_frames_per_spk_; + float frame_sec_, onset_, offset_; + float sil_threshold_, pred_score_threshold_, scores_boost_latest_; + float strong_boost_rate_, weak_boost_rate_, min_pos_scores_rate_; + bool use_learnable_sil_emb_ = false; + std::vector learnable_sil_emb_; // [d_model] + + // Streaming state. Embeddings are time-major [frames, d_model]; + // predictions are [frames, n_spk] at encoder resolution. + std::vector spkcache_, spkcache_preds_; + std::vector fifo_, fifo_preds_; + bool spkcache_compressed_ = false; + std::vector mean_sil_emb_; // [d_model] + long long n_sil_frames_ = 0; + + // Output state. + long long frames_done_ = 0; // mel frames consumed + std::vector last_probs_; + int last_frames_ = 0; + std::vector active_; // per speaker + std::vector start_frame_; // per speaker, when active }; } // namespace pk diff --git a/src/model_loader.cpp b/src/model_loader.cpp index ab63ae6..2de057c 100644 --- a/src/model_loader.cpp +++ b/src/model_loader.cpp @@ -207,39 +207,22 @@ bool ModelLoader::load(const std::string& path){ d.frame_resolution_sec = kv_f32(gguf_, "parakeet.diar.frame_resolution_sec", 0.01f); d.onset_threshold = kv_f32(gguf_, "parakeet.diar.onset_threshold", 0.5f); d.offset_threshold = kv_f32(gguf_, "parakeet.diar.offset_threshold", 0.5f); - // AOSC streaming config (Phase 2) - // If streaming KVs are absent, use Nemotron-3-Diarization defaults - // so the streaming C-API works out of the box. - if (gguf_find_key(gguf_, "parakeet.diar.chunk_len") >= 0) { - d.streaming_capable = true; - d.chunk_len = (int32_t)kv_u32(gguf_, "parakeet.diar.chunk_len"); - d.spkcache_len = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_len"); - d.fifo_len = (int32_t)kv_u32(gguf_, "parakeet.diar.fifo_len", 0); - d.spkcache_update_period = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_update_period", (uint32_t)d.chunk_len); - d.spkcache_sil_frames_per_spk = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_sil_frames_per_spk", 3); - d.sil_threshold = kv_f32(gguf_, "parakeet.diar.sil_threshold", 0.2f); - d.pred_score_threshold = kv_f32(gguf_, "parakeet.diar.pred_score_threshold", 0.25f); - d.scores_boost_latest = kv_f32(gguf_, "parakeet.diar.scores_boost_latest", 0.05f); - d.strong_boost_rate = kv_f32(gguf_, "parakeet.diar.strong_boost_rate", 0.75f); - d.weak_boost_rate = kv_f32(gguf_, "parakeet.diar.weak_boost_rate", 1.5f); - d.min_pos_scores_rate = kv_f32(gguf_, "parakeet.diar.min_pos_scores_rate", 0.5f); - d.use_learnable_sil_emb = kv_bool(gguf_, "parakeet.diar.use_learnable_sil_emb", false); - } else { - // Nemotron-3-Diarization defaults - d.streaming_capable = true; - d.chunk_len = 264; - d.spkcache_len = 264; - d.fifo_len = 0; - d.spkcache_update_period = 264; - d.spkcache_sil_frames_per_spk = 3; - d.sil_threshold = 0.2f; - d.pred_score_threshold = 0.25f; - d.scores_boost_latest = 0.05f; - d.strong_boost_rate = 0.75f; - d.weak_boost_rate = 1.5f; - d.min_pos_scores_rate = 0.5f; - d.use_learnable_sil_emb = (gguf_find_key(gguf_, "sortformer_modules.learnable_sil_emb") >= 0); - } + // Streaming (speaker cache) config, in ENCODER frames as in NeMo + // SortformerModules. Defaults are the Nemotron-3-Diarization values, + // for GGUFs converted before these keys were written. + d.chunk_len = (int32_t)kv_u32(gguf_, "parakeet.diar.chunk_len", 264); + d.spkcache_len = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_len", 264); + d.fifo_len = (int32_t)kv_u32(gguf_, "parakeet.diar.fifo_len", 0); + d.spkcache_update_period = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_update_period", 264); + d.spkcache_sil_frames_per_spk = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_sil_frames_per_spk", 1); + d.sil_threshold = kv_f32(gguf_, "parakeet.diar.sil_threshold", 0.2f); + d.pred_score_threshold = kv_f32(gguf_, "parakeet.diar.pred_score_threshold", 0.25f); + d.scores_boost_latest = kv_f32(gguf_, "parakeet.diar.scores_boost_latest", 0.05f); + d.strong_boost_rate = kv_f32(gguf_, "parakeet.diar.strong_boost_rate", 0.75f); + d.weak_boost_rate = kv_f32(gguf_, "parakeet.diar.weak_boost_rate", 1.5f); + d.min_pos_scores_rate = kv_f32(gguf_, "parakeet.diar.min_pos_scores_rate", 0.5f); + d.use_learnable_sil_emb = kv_bool(gguf_, "parakeet.diar.use_learnable_sil_emb", + gguf_find_tensor(gguf_, "sortformer_modules.learnable_sil_emb") >= 0); } // durations array (stored as INT32 by the converter) { int64_t id = gguf_find_key(gguf_, "parakeet.tdt.durations"); diff --git a/src/model_loader.hpp b/src/model_loader.hpp index 417dde2..1beddf6 100644 --- a/src/model_loader.hpp +++ b/src/model_loader.hpp @@ -82,13 +82,12 @@ struct ParakeetConfig { float frame_resolution_sec=0.01f; // output frame duration float onset_threshold=0.5f; // hysteresis onset float offset_threshold=0.5f; // hysteresis offset - // --- AOSC streaming config (Phase 2) --- - bool streaming_capable=false; - int32_t chunk_len=0; // mel frames per chunk (264) - int32_t spkcache_len=0; // target spkcache size in mel frames (264) - int32_t fifo_len=0; // FIFO buffer length (0 for Nemotron-3) - int32_t spkcache_update_period=0; // frames before spkcache update - int32_t spkcache_sil_frames_per_spk=3; // silence placeholders per speaker + // --- streaming (speaker cache) config, in ENCODER frames (80 ms) --- + int32_t chunk_len=264; // encoder frames per chunk + int32_t spkcache_len=264; // speaker cache size + int32_t fifo_len=0; // FIFO size (0 for Nemotron-3) + int32_t spkcache_update_period=264; // frames popped FIFO -> cache per update + int32_t spkcache_sil_frames_per_spk=1; // reserved silence slots per speaker float sil_threshold=0.2f; // silence detection threshold float pred_score_threshold=0.25f; // log-score clamp floor float scores_boost_latest=0.05f; // boost for latest frames diff --git a/src/parakeet_capi.cpp b/src/parakeet_capi.cpp index 147dd5e..c3eba28 100644 --- a/src/parakeet_capi.cpp +++ b/src/parakeet_capi.cpp @@ -10,6 +10,9 @@ #include "transcription.hpp" // pk::Transcription, pk::Word #include "transcription_json.hpp" +#include +#include +#include #include #include #include @@ -38,11 +41,10 @@ // v6: transcribe_pcm_logits, exposing the CTC head's log-prob matrix (row-major // [T, vocab+1], already log-softmaxed) instead of decoded text, freed with // the new free_logits. Original entry points unchanged. -// v6.1: offline speaker diarization entry points (diarize_path / diarize_pcm). -// v7: speaker-attributed ASR (SAS) entry points — takes two contexts -// (ASR + diarization), runs both models, merges word timestamps with -// speaker segments. -#define PARAKEET_CAPI_ABI_VERSION 8 +// v7: speaker diarization (diarize_*), speaker-attributed ASR +// (transcribe_and_diarize*, sas_stream_*) and streaming diarization +// (diarize_stream_*). A context holds either an ASR or a diarization model. +#define PARAKEET_CAPI_ABI_VERSION 7 // The opaque context: a loaded model plus a buffer for the last error message. // Exactly one of `model` / `diar` is non-null: ASR models use `model`, @@ -858,451 +860,462 @@ extern "C" void parakeet_capi_free_string(char* s) { std::free(s); } +extern "C" const char* parakeet_capi_last_error(parakeet_ctx* ctx) { + if (!ctx) return ""; + return ctx->last_error.c_str(); +} + // --------------------------------------------------------------------------- -// Offline speaker diarization +// Speaker diarization + speaker-attributed ASR (ABI v7) // --------------------------------------------------------------------------- -// Serialize a DiarizationResult to the JSON shape documented in the header. -static char* diar_result_to_json(const pk::DiarizationResult& r) { - // {"speakers":N,"segments":[{"speaker":S,"start":X.XX,"end":Y.YY}, ...]} - std::string json; - json.reserve(128 + r.segments.size() * 40); - json += "{\"speakers\":"; - json += std::to_string(r.n_speakers); - json += ",\"segments\":["; - for (size_t i = 0; i < r.segments.size(); ++i) { - if (i) json += ','; - char buf[80]; - std::snprintf(buf, sizeof(buf), - "{\"speaker\":%d,\"start\":%.2f,\"end\":%.2f}", - r.segments[i].speaker, r.segments[i].start, r.segments[i].end); - json += buf; - } - json += "]}"; - return dup_to_c(json); -} +namespace { -extern "C" char* parakeet_capi_diarize_path(parakeet_ctx* ctx, - const char* wav_path) { - if (!ctx) return nullptr; +bool require_diar(parakeet_ctx* ctx) { + if (!ctx) return false; if (!ctx->diar) { - ctx->last_error = "context has no loaded diarization model"; - return nullptr; - } - if (!wav_path) { ctx->last_error = "wav_path is NULL"; return nullptr; } - try { - pk::DiarizationResult r = ctx->diar->diarize_path(wav_path); - ctx->last_error.clear(); - return diar_result_to_json(r); - } catch (const std::exception& e) { - ctx->last_error = e.what(); - return nullptr; - } catch (...) { - ctx->last_error = "unknown error"; - return nullptr; + ctx->last_error = ctx->model + ? "context holds an ASR model; diarize_* needs a diarization model" + : "context has no loaded model"; + return false; } + return true; } -extern "C" char* parakeet_capi_diarize_pcm(parakeet_ctx* ctx, - const float* samples, int n_samples, - int sample_rate) { - if (!ctx) return nullptr; - if (!ctx->diar) { - ctx->last_error = "context has no loaded diarization model"; - return nullptr; - } - if (!samples || n_samples < 0) { - ctx->last_error = "invalid samples buffer"; - return nullptr; - } - try { - std::vector pcm(samples, samples + n_samples); - pk::DiarizationResult r = ctx->diar->diarize_pcm(pcm, sample_rate); - ctx->last_error.clear(); - return diar_result_to_json(r); - } catch (const std::exception& e) { - ctx->last_error = e.what(); - return nullptr; - } catch (...) { - ctx->last_error = "unknown error"; - return nullptr; +bool require_asr(parakeet_ctx* ctx) { + if (!ctx) return false; + if (!ctx->model) { + ctx->last_error = ctx->diar + ? "context holds a diarization model; an ASR model is needed here" + : "context has no loaded model"; + return false; } + return true; } -extern "C" const char* parakeet_capi_last_error(parakeet_ctx* ctx) { - if (!ctx) return ""; - return ctx->last_error.c_str(); +char* diar_result_to_json(const pk::DiarizationResult& r) { + std::string json = "{\"speakers\":"; + pk::append_json_int(json, r.n_speakers); + json += ",\"segments\":["; + for (size_t i = 0; i < r.segments.size(); ++i) { + if (i) json += ','; + json += "{\"speaker\":"; + pk::append_json_int(json, r.segments[i].speaker); + json += ",\"start\":"; + pk::append_json_float(json, "%.2f", r.segments[i].start); + json += ",\"end\":"; + pk::append_json_float(json, "%.2f", r.segments[i].end); + json += '}'; + } + json += "]}"; + return dup_to_c(json); } -// --------------------------------------------------------------------------- -// Speaker-attributed ASR (SAS) -// --------------------------------------------------------------------------- - -static char* sas_results_to_json(const std::vector& utts, - int n_speakers) { - // Build JSON: {"speakers":N, "utterances":[...], "words":[...]} - // For the non-words variant, just utterances. - std::string s; - s.reserve(4096); - s += "{\"speakers\":"; - s += std::to_string(n_speakers); - s += ",\"utterances\":["; - for (size_t i = 0; i < utts.size(); ++i) { - if (i) s += ','; - char buf[512]; - snprintf(buf, sizeof(buf), - "{\"speaker\":%d,\"text\":\"%s\",\"start\":%.2f,\"end\":%.2f,\"conf\":%.3f}", - utts[i].speaker, - utts[i].text.c_str(), - utts[i].start, - utts[i].end, - utts[i].conf); - s += buf; - } - s += "]}"; - return dup_to_c(s); +template +void append_speaker_item(std::string& s, const T& x, const char* time_fmt) { + s += "{\"speaker\":"; + pk::append_json_int(s, x.speaker); + s += ",\"text\":"; + pk::append_json_string(s, x.text); + s += ",\"start\":"; + pk::append_json_float(s, time_fmt, x.start); + s += ",\"end\":"; + pk::append_json_float(s, time_fmt, x.end); + s += ",\"conf\":"; + pk::append_json_float(s, "%.3f", x.conf); + s += '}'; } -static char* sas_results_to_json_full(const std::vector& utts, - const std::vector& swords, - int n_speakers) { - std::string s; - s.reserve(8192); - s += "{\"speakers\":"; - s += std::to_string(n_speakers); - s += ",\"utterances\":["; +// Copy utterances into a malloc'd C array. Returns false on allocation failure. +bool to_c_results(const std::vector& utts, + parakeet_sas_result** out, int* n_out) { + *out = nullptr; + *n_out = 0; + if (utts.empty()) return true; + auto* r = static_cast(std::calloc(utts.size(), sizeof(parakeet_sas_result))); + if (!r) return false; for (size_t i = 0; i < utts.size(); ++i) { - if (i) s += ','; - char buf[512]; - snprintf(buf, sizeof(buf), - "{\"speaker\":%d,\"text\":\"%s\",\"start\":%.2f,\"end\":%.2f,\"conf\":%.3f}", - utts[i].speaker, - utts[i].text.c_str(), - utts[i].start, - utts[i].end, - utts[i].conf); - s += buf; - } - s += "],\"words\":["; - for (size_t i = 0; i < swords.size(); ++i) { - if (i) s += ','; - char buf[512]; - snprintf(buf, sizeof(buf), - "{\"speaker\":%d,\"text\":\"%s\",\"start\":%.3f,\"end\":%.3f,\"conf\":%.3f}", - swords[i].speaker, - swords[i].text.c_str(), - swords[i].start, - swords[i].end, - swords[i].conf); - s += buf; + r[i].speaker = utts[i].speaker; + r[i].text = dup_to_c(utts[i].text); + r[i].start = utts[i].start; + r[i].end = utts[i].end; + r[i].conf = utts[i].conf; + if (!r[i].text) { parakeet_capi_free_sas_results(r, (int)i); return false; } } - s += "]}"; - return dup_to_c(s); + *out = r; + *n_out = (int)utts.size(); + return true; } -// Internal: run ASR + diarization on the same audio, merge, return both -// utterances and per-word results. -static bool run_sas(parakeet_ctx* asr_ctx, - parakeet_ctx* diar_ctx, - const float* samples, int n_samples, int sample_rate, - std::vector& swords, - std::vector& utts, - int& n_speakers) { - if (!asr_ctx || !asr_ctx->model) { - if (asr_ctx) asr_ctx->last_error = "asr_ctx does not hold an ASR model"; - return false; - } - if (!diar_ctx || !diar_ctx->diar) { - if (diar_ctx) diar_ctx->last_error = "diar_ctx does not hold a diarization model"; +// ASR + diarization on the same audio, merged per word. +bool run_sas(parakeet_ctx* asr_ctx, parakeet_ctx* diar_ctx, + const float* samples, int n_samples, int sample_rate, + std::vector& words, int& n_speakers) { + if (!require_asr(asr_ctx) || !require_diar(diar_ctx)) return false; + if (!samples || n_samples < 0) { + asr_ctx->last_error = "invalid samples buffer"; return false; } - - // Run ASR (with timestamps so we get per-word [text, start, end, conf]) + const std::vector pcm(samples, samples + n_samples); pk::Transcription tr; try { - std::vector pcm(samples, samples + n_samples); tr = asr_ctx->model->transcribe_with_timestamps(pcm, sample_rate); } catch (const std::exception& e) { - asr_ctx->last_error = std::string("ASR failed: ") + e.what(); + asr_ctx->last_error = e.what(); return false; } - - // Run diarization pk::DiarizationResult dr; try { - std::vector pcm(samples, samples + n_samples); dr = diar_ctx->diar->diarize_pcm(pcm, sample_rate); } catch (const std::exception& e) { - diar_ctx->last_error = std::string("diarization failed: ") + e.what(); + diar_ctx->last_error = e.what(); return false; } - n_speakers = dr.n_speakers; - - // Merge: assign speaker to each word - swords = pk::merge_asr_diarization(tr.words, dr.segments); - - // Group into utterances - utts = pk::group_speaker_words(swords); - + words = pk::merge_asr_diarization(tr.words, dr.segments); + asr_ctx->last_error.clear(); + diar_ctx->last_error.clear(); return true; } -extern "C" parakeet_sas_result* parakeet_capi_transcribe_and_diarize( - parakeet_ctx* asr_ctx, - parakeet_ctx* diar_ctx, - const float* samples, int n_samples, int sample_rate, - int* n_results) { - if (n_results) *n_results = 0; - if (!asr_ctx || !diar_ctx) return nullptr; - - std::vector swords; - std::vector utts; - int n_speakers = 0; +} // namespace - if (!run_sas(asr_ctx, diar_ctx, samples, n_samples, sample_rate, - swords, utts, n_speakers)) { - return nullptr; +extern "C" char* parakeet_capi_diarize_path(parakeet_ctx* ctx, const char* wav_path) { + if (!require_diar(ctx)) return nullptr; + if (!wav_path) { ctx->last_error = "wav_path is NULL"; return nullptr; } + try { + char* out = diar_result_to_json(ctx->diar->diarize_path(wav_path)); + ctx->last_error.clear(); + return out; + } catch (const std::exception& e) { + ctx->last_error = e.what(); + } catch (...) { + ctx->last_error = "unknown error"; } + return nullptr; +} - // Allocate result array - parakeet_sas_result* results = (parakeet_sas_result*) - std::malloc(sizeof(parakeet_sas_result) * utts.size()); - if (!results) return nullptr; - - for (size_t i = 0; i < utts.size(); ++i) { - results[i].speaker = utts[i].speaker; - results[i].text = dup_to_c(utts[i].text); - results[i].start = utts[i].start; - results[i].end = utts[i].end; - results[i].conf = utts[i].conf; +extern "C" char* parakeet_capi_diarize_pcm(parakeet_ctx* ctx, const float* samples, + int n_samples, int sample_rate) { + if (!require_diar(ctx)) return nullptr; + if (!samples || n_samples < 0) { ctx->last_error = "invalid samples buffer"; return nullptr; } + try { + const std::vector pcm(samples, samples + n_samples); + char* out = diar_result_to_json(ctx->diar->diarize_pcm(pcm, sample_rate)); + ctx->last_error.clear(); + return out; + } catch (const std::exception& e) { + ctx->last_error = e.what(); + } catch (...) { + ctx->last_error = "unknown error"; } - - if (n_results) *n_results = (int)utts.size(); - return results; + return nullptr; } -extern "C" void parakeet_capi_free_sas_results(parakeet_sas_result* results) { - // We cannot free the .text strings because the caller doesn't pass the - // count to this function. The caller must free each .text with - // parakeet_capi_free_string and then call this function to free the array. - if (results) std::free(results); +extern "C" int parakeet_capi_transcribe_and_diarize(parakeet_ctx* asr_ctx, parakeet_ctx* diar_ctx, + const float* samples, int n_samples, + int sample_rate, + parakeet_sas_result** out, int* n_out) { + if (!out || !n_out) return 1; + *out = nullptr; + *n_out = 0; + try { + std::vector words; + int n_speakers = 0; + if (!run_sas(asr_ctx, diar_ctx, samples, n_samples, sample_rate, words, n_speakers)) + return 1; + if (!to_c_results(pk::group_speaker_words(words), out, n_out)) { + asr_ctx->last_error = "out of memory"; + return 1; + } + return 0; + } catch (...) { + if (asr_ctx) asr_ctx->last_error = "unknown error"; + return 1; + } } -extern "C" char* parakeet_capi_transcribe_and_diarize_json( - parakeet_ctx* asr_ctx, - parakeet_ctx* diar_ctx, - const float* samples, int n_samples, int sample_rate) { - if (!asr_ctx || !diar_ctx) return nullptr; - - std::vector swords; - std::vector utts; - int n_speakers = 0; +extern "C" void parakeet_capi_free_sas_results(parakeet_sas_result* results, int n) { + if (!results) return; + for (int i = 0; i < n; ++i) std::free(results[i].text); + std::free(results); +} - if (!run_sas(asr_ctx, diar_ctx, samples, n_samples, sample_rate, - swords, utts, n_speakers)) { +extern "C" char* parakeet_capi_transcribe_and_diarize_json(parakeet_ctx* asr_ctx, + parakeet_ctx* diar_ctx, + const float* samples, int n_samples, + int sample_rate) { + try { + std::vector words; + int n_speakers = 0; + if (!run_sas(asr_ctx, diar_ctx, samples, n_samples, sample_rate, words, n_speakers)) + return nullptr; + const std::vector utts = pk::group_speaker_words(words); + std::string s = "{\"speakers\":"; + pk::append_json_int(s, n_speakers); + s += ",\"utterances\":["; + for (size_t i = 0; i < utts.size(); ++i) { + if (i) s += ','; + append_speaker_item(s, utts[i], "%.2f"); + } + s += "],\"words\":["; + for (size_t i = 0; i < words.size(); ++i) { + if (i) s += ','; + append_speaker_item(s, words[i], "%.3f"); + } + s += "]}"; + return dup_to_c(s); + } catch (...) { + if (asr_ctx) asr_ctx->last_error = "unknown error"; return nullptr; } - - return sas_results_to_json_full(utts, swords, n_speakers); } -// =========================================================================== -// v8: Streaming diarization C-API -// =========================================================================== +// --- Streaming diarization ------------------------------------------------- struct parakeet_diar_stream { - std::unique_ptr diar; - parakeet_ctx* diar_ctx = nullptr; - std::string error; + parakeet_ctx* ctx = nullptr; + std::unique_ptr sd; + std::unique_ptr mel; + std::vector pending; // mel frames not yet diarized, frame-major [t][n_mels] + long long samples_in = 0; // PCM samples fed so far + bool finished = false; }; -extern "C" parakeet_diar_stream* parakeet_capi_diarize_stream_begin( - parakeet_ctx* diar_ctx) { - if (!diar_ctx || !diar_ctx->diar) { - return nullptr; - } - auto* stream = new (std::nothrow) parakeet_diar_stream(); - if (!stream) return nullptr; - stream->diar_ctx = diar_ctx; - stream->diar = std::make_unique(diar_ctx->diar->loader()); - stream->diar->reset(); - return stream; +namespace { + +// Append feat-major [n_mels, n] mel frames to a frame-major buffer. +void push_frames(std::vector& dst, const std::vector& fm, int n_mels, int n) { + const size_t base = dst.size(); + dst.resize(base + (size_t)n * n_mels); + for (int m = 0; m < n_mels; ++m) + for (int t = 0; t < n; ++t) dst[base + (size_t)t * n_mels + m] = fm[(size_t)m * n + t]; } -extern "C" parakeet_diar_segment* parakeet_capi_diarize_stream_feed( - parakeet_diar_stream* stream, - const float* mel, int n_mels, int n_frames, - int is_last, int* out_count) { - if (!stream || !mel || n_mels <= 0 || n_frames <= 0) { - if (out_count) *out_count = 0; - return nullptr; +// Feed PCM to the stream's mel front end and run every full diarization chunk +// (and, with is_last, the tail). Closed segments are appended to `segs`. +// Returns the number of chunks run. +int diar_stream_advance(parakeet_diar_stream* s, const float* pcm, int n, bool is_last, + std::vector& segs) { + const int n_mels = s->sd->n_mels(); + int nf = 0; + if (n > 0) { + std::vector fm = s->mel->feed(pcm, n, nf); + push_frames(s->pending, fm, n_mels, nf); + s->samples_in += n; + } + if (is_last) { + std::vector fm = s->mel->finalize(nf); + push_frames(s->pending, fm, n_mels, nf); + // NeMo keeps floor(S / hop) frames; the centered STFT emits one more. + const long long valid = s->samples_in / (long long)s->ctx->diar->config().hop_length; + const long long have = s->sd->frames_done() + (long long)(s->pending.size() / n_mels); + if (have > valid) s->pending.resize(s->pending.size() - (size_t)(have - valid) * n_mels); + } + const int cm = s->sd->chunk_mel_frames(); + int chunks = 0; + for (;;) { + const int avail = (int)(s->pending.size() / n_mels); + const bool last = is_last && avail <= cm; + if (avail < cm && !(last && avail > 0)) break; + const int take = std::min(avail, cm); + std::vector chunk((size_t)n_mels * take); + for (int t = 0; t < take; ++t) + for (int m = 0; m < n_mels; ++m) + chunk[(size_t)m * take + t] = s->pending[(size_t)t * n_mels + m]; + s->pending.erase(s->pending.begin(), s->pending.begin() + (size_t)take * n_mels); + auto closed = s->sd->feed_mel_chunk(chunk, n_mels, take, last); + segs.insert(segs.end(), closed.begin(), closed.end()); + ++chunks; + if (last) break; + } + if (is_last && chunks == 0 && s->sd->frames_done() > 0) { + // Stream length was an exact multiple of the chunk: close open segments. + auto open = s->sd->open_segments(); + segs.insert(segs.end(), open.begin(), open.end()); } + if (is_last) s->finished = true; + return chunks; +} + +} // namespace + +extern "C" parakeet_diar_stream* parakeet_capi_diarize_stream_begin(parakeet_ctx* diar_ctx) { + if (!require_diar(diar_ctx)) return nullptr; try { - std::vector mel_vec(mel, mel + (size_t)n_mels * n_frames); - auto segs = stream->diar->feed_mel_chunk(mel_vec, n_mels, n_frames, is_last != 0); - if (segs.empty()) { - if (out_count) *out_count = 0; - return nullptr; - } - auto* out = (parakeet_diar_segment*)std::malloc(segs.size() * sizeof(parakeet_diar_segment)); - if (!out) { - if (out_count) *out_count = 0; - return nullptr; - } - for (size_t i = 0; i < segs.size(); ++i) { - out[i].speaker = segs[i].speaker; - out[i].start = segs[i].start; - out[i].end = segs[i].end; - } - if (out_count) *out_count = (int)segs.size(); - return out; + auto* s = new parakeet_diar_stream(); + s->ctx = diar_ctx; + s->sd = std::make_unique(diar_ctx->diar->loader()); + s->mel = std::make_unique(diar_ctx->diar->loader()); + diar_ctx->last_error.clear(); + return s; } catch (const std::exception& e) { - stream->error = e.what(); - if (out_count) *out_count = 0; - return nullptr; + diar_ctx->last_error = e.what(); + } catch (...) { + diar_ctx->last_error = "unknown error"; } + return nullptr; } -extern "C" int parakeet_capi_diar_stream_chunk_len(parakeet_diar_stream* stream) { - if (!stream || !stream->diar) return 0; - return stream->diar->chunk_len(); +extern "C" int parakeet_capi_diarize_stream_chunk_samples(parakeet_diar_stream* s) { + if (!s) return 0; + return s->sd->chunk_mel_frames() * (int)s->ctx->diar->config().hop_length; } -extern "C" int parakeet_capi_diar_stream_n_mels(parakeet_diar_stream* stream) { - if (!stream || !stream->diar) return 0; - return stream->diar->n_mels(); +extern "C" int parakeet_capi_diarize_stream_feed(parakeet_diar_stream* s, const float* pcm, + int n_samples, int is_last, + parakeet_diar_segment** out, int* n_out) { + if (!s || !out || !n_out) return 1; + *out = nullptr; + *n_out = 0; + if ((!pcm && n_samples > 0) || n_samples < 0) { s->ctx->last_error = "invalid samples buffer"; return 1; } + if (s->finished) { s->ctx->last_error = "stream already finished"; return 1; } + try { + std::vector segs; + diar_stream_advance(s, pcm, n_samples, is_last != 0, segs); + if (!segs.empty()) { + auto* r = static_cast(std::malloc(segs.size() * sizeof(parakeet_diar_segment))); + if (!r) { s->ctx->last_error = "out of memory"; return 1; } + for (size_t i = 0; i < segs.size(); ++i) r[i] = {segs[i].speaker, segs[i].start, segs[i].end}; + *out = r; + *n_out = (int)segs.size(); + } + s->ctx->last_error.clear(); + return 0; + } catch (const std::exception& e) { + s->ctx->last_error = e.what(); + } catch (...) { + s->ctx->last_error = "unknown error"; + } + return 1; } extern "C" void parakeet_capi_free_diar_segments(parakeet_diar_segment* segs) { std::free(segs); } -extern "C" void parakeet_capi_diarize_stream_free(parakeet_diar_stream* stream) { - delete stream; +extern "C" void parakeet_capi_diarize_stream_free(parakeet_diar_stream* s) { + delete s; } -// =========================================================================== -// v8: Streaming speaker-attributed ASR (Phase 3.4) -// =========================================================================== +// --- Streaming speaker-attributed ASR --------------------------------------- struct parakeet_sas_stream { - parakeet_ctx* asr_ctx = nullptr; - parakeet_diar_stream* diar_stream = nullptr; - std::vector pcm_buffer; - int asr_chunk_samples = 0; - int total_samples = 0; - std::string error; + parakeet_ctx* asr = nullptr; + parakeet_diar_stream* diar = nullptr; + std::vector audio; // uncommitted PCM, starting at commit_sec + double commit_sec = 0.0; // stream time of audio[0] + std::vector segs; // closed diarization segments + pk::Word last_word; // last committed word (absolute times) + bool have_last_word = false; }; -extern "C" parakeet_sas_stream* parakeet_capi_sas_stream_begin( - parakeet_ctx* asr_ctx, parakeet_ctx* diar_ctx) { - if (!asr_ctx || !asr_ctx->model || !diar_ctx || !diar_ctx->diar) { - return nullptr; - } - auto* stream = new (std::nothrow) parakeet_sas_stream(); - if (!stream) return nullptr; - stream->asr_ctx = asr_ctx; - stream->diar_stream = parakeet_capi_diarize_stream_begin(diar_ctx); - if (!stream->diar_stream) { - delete stream; - return nullptr; - } - int chunk_mel = stream->diar_stream->diar->chunk_len(); - stream->asr_chunk_samples = chunk_mel * 160; // hop_length = 160 - stream->total_samples = 0; - return stream; -} - -extern "C" parakeet_sas_result* parakeet_capi_sas_stream_feed( - parakeet_sas_stream* stream, - const float* pcm, int n_samples, - int is_last, int* out_count) { - if (!stream || !pcm || n_samples <= 0) { - if (out_count) *out_count = 0; - return nullptr; - } - try { - stream->pcm_buffer.insert(stream->pcm_buffer.end(), pcm, pcm + n_samples); - - std::vector all_words; - std::vector all_segs; - - while ((int)stream->pcm_buffer.size() >= stream->asr_chunk_samples || - (is_last && !stream->pcm_buffer.empty())) { - int chunk_samples = std::min(stream->asr_chunk_samples, - (int)stream->pcm_buffer.size()); - bool chunk_is_last = is_last && - (chunk_samples == (int)stream->pcm_buffer.size()); - - std::vector chunk_pcm(stream->pcm_buffer.begin(), - stream->pcm_buffer.begin() + chunk_samples); - - // 1. Run ASR on the chunk - pk::Transcription trans = - stream->asr_ctx->model->transcribe_with_timestamps(chunk_pcm, 16000); - float chunk_offset = (float)stream->total_samples / 16000.0f; - for (auto& w : trans.words) { - w.start += chunk_offset; - w.end += chunk_offset; - all_words.push_back(w); - } - - // 2. Run diarization on the chunk - std::vector mel; - int n_mels = 0, n_frames = 0; - stream->diar_stream->diar_ctx->diar->mel().compute( - chunk_pcm, mel, n_mels, n_frames); +namespace { - auto segs = stream->diar_stream->diar->feed_mel_chunk( - mel, n_mels, n_frames, chunk_is_last); - for (auto& s : segs) - all_segs.push_back(s); +// Lowercase letters and digits only, for comparing a word heard twice. +std::string word_key(const std::string& w) { + std::string k; + for (unsigned char c : w) + if (std::isalnum(c) || c >= 0x80) k += (char)std::tolower(c); + return k; +} - stream->pcm_buffer.erase(stream->pcm_buffer.begin(), - stream->pcm_buffer.begin() + chunk_samples); - stream->total_samples += chunk_samples; +} // namespace + +extern "C" parakeet_sas_stream* parakeet_capi_sas_stream_begin(parakeet_ctx* asr_ctx, + parakeet_ctx* diar_ctx) { + if (!require_asr(asr_ctx) || !require_diar(diar_ctx)) return nullptr; + parakeet_diar_stream* d = parakeet_capi_diarize_stream_begin(diar_ctx); + if (!d) return nullptr; + auto* s = new (std::nothrow) parakeet_sas_stream(); + if (!s) { parakeet_capi_diarize_stream_free(d); return nullptr; } + s->asr = asr_ctx; + s->diar = d; + return s; +} - if (chunk_is_last) break; +extern "C" int parakeet_capi_sas_stream_feed(parakeet_sas_stream* s, const float* pcm, + int n_samples, int is_last, + parakeet_sas_result** out, int* n_out) { + if (!s || !out || !n_out) return 1; + *out = nullptr; + *n_out = 0; + if ((!pcm && n_samples > 0) || n_samples < 0) { s->asr->last_error = "invalid samples buffer"; return 1; } + if (s->diar->finished) { s->asr->last_error = "stream already finished"; return 1; } + parakeet_ctx* failed = s->diar->ctx; + try { + std::vector closed; + const int chunks = diar_stream_advance(s->diar, pcm, n_samples, is_last != 0, closed); + for (const auto& c : closed) s->segs.push_back({c.speaker, c.start, c.end}); + if (n_samples > 0) s->audio.insert(s->audio.end(), pcm, pcm + n_samples); + if (chunks == 0 && !is_last) return 0; + + // Diarized audio ends at frames_done; transcribe the uncommitted span. + const double hop_sec = (double)s->diar->ctx->diar->config().hop_length / 16000.0; + const double diar_end = s->diar->sd->frames_done() * hop_sec; + size_t span = is_last ? s->audio.size() + : std::min(s->audio.size(), + (size_t)std::max(0.0, (diar_end - s->commit_sec) * 16000.0)); + failed = s->asr; + std::vector words; + if (span > 0) { + const std::vector seg(s->audio.begin(), s->audio.begin() + span); + words = s->asr->model->transcribe_with_timestamps(seg, 16000).words; } - - // 3. Merge ASR words with diarization segments - std::vector diar_segs; - for (auto& s : all_segs) - diar_segs.push_back({s.speaker, s.start, s.end}); - - auto swords = pk::merge_asr_diarization(all_words, diar_segs); - auto utts = pk::group_speaker_words(swords, 0.5f); - - if (utts.empty()) { - if (out_count) *out_count = 0; - return nullptr; + // Commit only words that end kSasRightContextSec before the cut: the + // ASR needs right context, and a word at the edge may be cut in half. + // The rest is transcribed again with the next chunk. + constexpr double kSasRightContextSec = 1.0; + size_t keep = words.size(); + double next_commit = s->commit_sec + (double)span / 16000.0; + if (!is_last) { + const double limit = (double)span / 16000.0 - kSasRightContextSec; + keep = 0; + while (keep < words.size() && words[keep].end <= limit) ++keep; + next_commit = s->commit_sec + (keep < words.size() ? words[keep].start + : std::max(0.0, limit)); } - - auto* results = (parakeet_sas_result*)std::calloc(utts.size(), - sizeof(parakeet_sas_result)); - if (!results) { - if (out_count) *out_count = 0; - return nullptr; + std::vector committed(words.begin(), words.begin() + keep); + for (auto& w : committed) { w.start += (float)s->commit_sec; w.end += (float)s->commit_sec; } + // ASR timestamps are only accurate to a frame or two, so the tail of the + // previously committed word can be heard again at the new start. + if (s->have_last_word && !committed.empty() && + word_key(committed.front().text) == word_key(s->last_word.text) && + committed.front().start - s->last_word.start < 0.5f) { + committed.erase(committed.begin()); } - for (size_t i = 0; i < utts.size(); ++i) { - results[i].speaker = utts[i].speaker; - results[i].text = dup_to_c(utts[i].text); - results[i].start = utts[i].start; - results[i].end = utts[i].end; - results[i].conf = utts[i].conf; + if (!committed.empty()) { + s->last_word = committed.back(); + s->have_last_word = true; } - if (out_count) *out_count = (int)utts.size(); - return results; + + // Speaker segments known so far: closed ones plus those still open. + std::vector segs = s->segs; + for (const auto& o : s->diar->sd->open_segments()) segs.push_back({o.speaker, o.start, o.end}); + const auto utts = pk::group_speaker_words(pk::merge_asr_diarization(committed, segs)); + + const size_t drop = std::min(s->audio.size(), + (size_t)std::llround((next_commit - s->commit_sec) * 16000.0)); + s->audio.erase(s->audio.begin(), s->audio.begin() + drop); + s->commit_sec += (double)drop / 16000.0; + // Segments that ended before the commit point can no longer match a word. + s->segs.erase(std::remove_if(s->segs.begin(), s->segs.end(), + [&](const pk::SpeakerSegment& g) { return g.end < s->commit_sec; }), + s->segs.end()); + + if (!to_c_results(utts, out, n_out)) { s->asr->last_error = "out of memory"; return 1; } + s->asr->last_error.clear(); + return 0; } catch (const std::exception& e) { - stream->error = e.what(); - if (out_count) *out_count = 0; - return nullptr; + failed->last_error = e.what(); + } catch (...) { + failed->last_error = "unknown error"; } + return 1; } -extern "C" void parakeet_capi_sas_stream_free(parakeet_sas_stream* stream) { - if (!stream) return; - parakeet_capi_diarize_stream_free(stream->diar_stream); - delete stream; +extern "C" void parakeet_capi_sas_stream_free(parakeet_sas_stream* s) { + if (!s) return; + parakeet_capi_diarize_stream_free(s->diar); + delete s; } diff --git a/tests/test_combined_offline.cpp b/tests/test_combined_offline.cpp index a3dbc20..0e7fd54 100644 --- a/tests/test_combined_offline.cpp +++ b/tests/test_combined_offline.cpp @@ -1,20 +1,24 @@ -// End-to-end test for Phase 3: speaker-attributed ASR (SAS). +// Speaker-attributed ASR (SAS) and streaming diarization through the C-API. // -// Loads an ASR model and a diarization model, runs both on the same audio file -// via parakeet_capi_transcribe_and_diarize_json, and validates: -// - the JSON has "speakers", "utterances", and "words" arrays -// - every word has a valid speaker (-1 or 0..n_speakers-1) -// - utterances have text, start, end, speaker fields -// - the JSON is parseable +// Runs on tests/fixtures/two_speakers.wav (LibriSpeech speakers 1272 and 2086 +// alternating A-B-A-B) and checks: +// 1. transcribe_and_diarize_json: valid document, every word attributed to a +// speaker, speaker turns follow A-B-A-B +// 2. transcribe_and_diarize (struct): same utterances as the JSON variant +// 3. diarize_stream_*: fed live in 0.5 s pieces, the segments match the +// offline diarize_pcm segments (speaker, boundaries within 0.1 s) +// 4. sas_stream_*: fed live in 0.5 s pieces, same turn pattern and about the +// same words as the offline SAS // -// Env: -// PARAKEET_TEST_GGUF ASR model (skip 77 if unset) -// PARAKEET_TEST_DIAR_GGUF diarization model (skip 77 if unset) -// PARAKEET_TEST_COMBINED_WAV audio file (default: tests/fixtures/speech.wav) +// Env: PARAKEET_TEST_GGUF (ASR model) + PARAKEET_TEST_DIAR_GGUF; skips (77) +// when either is unset. WORKING_DIRECTORY is the repo root. #include "parakeet_capi.h" #include "audio_io.hpp" +#include +#include +#include #include #include #include @@ -127,201 +131,201 @@ bool parse_word_speakers(const std::string& s, std::vector& speakers) { return sc.eat(']'); } -} // namespace +// Collapse consecutive repeats: [0,0,1,0,0,1] -> [0,1,0,1]. +std::vector turns(const std::vector& spk) { + std::vector t; + for (int s : spk) if (t.empty() || t.back() != s) t.push_back(s); + return t; +} -int main() { - // ABI version sanity. - int abi = parakeet_capi_abi_version(); - if (abi < 7) { - std::fprintf(stderr, "test_combined_offline: abi version %d < 7 (need SAS)\n", abi); - return 1; +std::string show(const std::vector& v) { + std::string s; + for (int x : v) s += (s.empty() ? "" : ",") + std::to_string(x); + return "[" + s + "]"; +} + +int word_count(const char* text) { + int n = 0; + bool in = false; + for (const char* p = text; *p; ++p) { + const bool sp = *p == ' '; + if (!sp && !in) ++n; + in = !sp; } + return n; +} + +// Offline diarize_pcm segments as (speaker, start, end) triples. +bool parse_segments(const std::string& doc, std::vector>& out) { + Scan sc(doc); + if (!sc.seek_key("segments") || !sc.eat('[')) return false; + if (sc.eat(']')) return true; + do { + std::array seg{}; + if (!sc.eat('{')) return false; + for (int k = 0; k < 3; ++k) { + std::string key; + if (!sc.str(key) || !sc.eat(':') || !sc.num(seg[k])) return false; + if (k < 2 && !sc.eat(',')) return false; + } + if (!sc.eat('}')) return false; + out.push_back(seg); + } while (sc.eat(',')); + return sc.eat(']'); +} + +} // namespace + +#define CHECK(cond, ...) \ + do { \ + if (!(cond)) { \ + std::fprintf(stderr, "FAIL: " __VA_ARGS__); \ + std::fprintf(stderr, "\n"); \ + ok = false; \ + } \ + } while (0) +int main() { const char* asr_gguf = std::getenv("PARAKEET_TEST_GGUF"); - if (!asr_gguf) { - std::fprintf(stderr, "test_combined_offline: PARAKEET_TEST_GGUF not set; skip\n"); - return 77; - } const char* diar_gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); - if (!diar_gguf) { - std::fprintf(stderr, "test_combined_offline: PARAKEET_TEST_DIAR_GGUF not set; skip\n"); + if (!asr_gguf || !diar_gguf) { + std::fprintf(stderr, "test_combined_offline: PARAKEET_TEST_GGUF and/or " + "PARAKEET_TEST_DIAR_GGUF not set; skip\n"); return 77; } - - const char* wav = std::getenv("PARAKEET_TEST_COMBINED_WAV"); - if (!wav) wav = "tests/fixtures/speech.wav"; - - // Load both models. - parakeet_ctx* asr_ctx = parakeet_capi_load(asr_gguf); - if (!asr_ctx) { - std::fprintf(stderr, "test_combined_offline: ASR load failed: %s\n", - asr_ctx ? parakeet_capi_last_error(asr_ctx) : "(null)"); + if (parakeet_capi_abi_version() < 7) { + std::fprintf(stderr, "test_combined_offline: ABI < 7\n"); return 1; } - parakeet_ctx* diar_ctx = parakeet_capi_load(diar_gguf); - if (!diar_ctx) { - std::fprintf(stderr, "test_combined_offline: diar load failed: %s\n", - diar_ctx ? parakeet_capi_last_error(diar_ctx) : "(null)"); - parakeet_capi_free(asr_ctx); + parakeet_ctx* asr = parakeet_capi_load(asr_gguf); + parakeet_ctx* diar = parakeet_capi_load(diar_gguf); + if (!asr || !diar) { + std::fprintf(stderr, "test_combined_offline: load failed\n"); + parakeet_capi_free(asr); + parakeet_capi_free(diar); return 1; } - - // Load audio from file — we need raw PCM, so we use the diarize_path JSON - // variant as a smoke test... no, we need PCM for transcribe_and_diarize. - // Load the WAV using the ASR model's path transcribe (which loads the wav) - // — actually, we need to load the WAV ourselves. - // The C-API has no "load WAV to PCM" function, so we use a simple approach: - // call parakeet_capi_transcribe_and_diarize_json with a file path... no. - // Actually, the SAS API takes PCM samples. We need to read the WAV file - // ourselves. Let's use the existing test audio loading approach. - - // Read WAV using the shared audio_io loader. pk::Audio audio; - if (!pk::load_audio_16k_mono(wav, audio) || audio.samples.empty()) { - std::fprintf(stderr, "test_combined_offline: cannot read %s\n", wav); - parakeet_capi_free(asr_ctx); - parakeet_capi_free(diar_ctx); - return 77; - } - - std::vector& pcm = audio.samples; - int sr = 16000; - - std::fprintf(stderr, "test_combined_offline: loaded %s (%d samples, %d Hz)\n", - wav, (int)pcm.size(), sr); - - // --- Test 1: JSON variant --- - char* json = parakeet_capi_transcribe_and_diarize_json( - asr_ctx, diar_ctx, pcm.data(), (int)pcm.size(), sr); - if (!json) { - std::fprintf(stderr, "test_combined_offline: transcribe_and_diarize_json NULL: %s\n", - parakeet_capi_last_error(asr_ctx)); - parakeet_capi_free(asr_ctx); - parakeet_capi_free(diar_ctx); + if (!pk::load_audio_16k_mono("tests/fixtures/two_speakers.wav", audio)) { + std::fprintf(stderr, "test_combined_offline: cannot read the fixture\n"); return 1; } - - const std::string doc(json); - parakeet_capi_free_string(json); - - std::fprintf(stderr, "test_combined_offline: json head = %.200s ...\n", doc.c_str()); - + const std::vector& pcm = audio.samples; + const int n = (int)pcm.size(); + const std::vector expected_turns = {0, 1, 0, 1}; bool ok = true; - // Validate JSON structure: must have speakers, utterances, words. - if (!has_array(doc, "utterances")) { - std::fprintf(stderr, "test_combined_offline: missing \"utterances\" array\n"); - ok = false; - } - if (!has_array(doc, "words")) { - std::fprintf(stderr, "test_combined_offline: missing \"words\" array\n"); - ok = false; - } + // Wrong-model guards. + CHECK(parakeet_capi_diarize_pcm(asr, pcm.data(), n, 16000) == nullptr, + "diarize_pcm accepted an ASR context"); + CHECK(parakeet_capi_transcribe_pcm(diar, pcm.data(), n, 16000, 0) == nullptr, + "transcribe_pcm accepted a diarization context"); - // Check "speakers" field exists and is positive. + // 1. JSON variant. + int n_words_offline = 0, n_utts_json = -1; { - Scan sc(doc); - if (!sc.seek_key("speakers")) { - std::fprintf(stderr, "test_combined_offline: missing \"speakers\" field\n"); - ok = false; - } else { - double spk; - if (!sc.num(spk) || spk <= 0) { - std::fprintf(stderr, "test_combined_offline: invalid speakers value\n"); - ok = false; - } else { - std::fprintf(stderr, "test_combined_offline: speakers = %.0f\n", spk); - } + char* json = parakeet_capi_transcribe_and_diarize_json(asr, diar, pcm.data(), n, 16000); + CHECK(json != nullptr, "transcribe_and_diarize_json: %s", parakeet_capi_last_error(asr)); + if (json) { + const std::string doc(json); + parakeet_capi_free_string(json); + std::vector spk; + CHECK(parse_word_speakers(doc, spk), "cannot parse the words array"); + n_words_offline = (int)spk.size(); + n_utts_json = count_array_elements(doc, "utterances"); + int unassigned = 0; + for (int s : spk) unassigned += s < 0; + std::printf("offline SAS: %d words, %d utterances, turns %s, %d unassigned\n", + n_words_offline, n_utts_json, show(turns(spk)).c_str(), unassigned); + CHECK(n_words_offline > 40, "too few words (%d)", n_words_offline); + CHECK(unassigned == 0, "%d words without a speaker", unassigned); + CHECK(turns(spk) == expected_turns, "turns %s, expected [0,1,0,1]", + show(turns(spk)).c_str()); } } - // Parse word speakers and validate range. + // 2. Struct variant. { - std::vector speakers; - if (!parse_word_speakers(doc, speakers)) { - std::fprintf(stderr, "test_combined_offline: failed to parse word speakers\n"); - ok = false; - } else { - std::fprintf(stderr, "test_combined_offline: %zu words parsed\n", - speakers.size()); - // Check that all speaker indices are valid (-1 or 0..7) - for (size_t i = 0; i < speakers.size(); ++i) { - if (speakers[i] < -1 || speakers[i] > 7) { - std::fprintf(stderr, - "test_combined_offline: word[%zu] speaker=%d out of range\n", - i, speakers[i]); - ok = false; - break; - } - } - } + parakeet_sas_result* r = nullptr; + int nr = 0; + const int rc = parakeet_capi_transcribe_and_diarize(asr, diar, pcm.data(), n, 16000, &r, &nr); + CHECK(rc == 0, "transcribe_and_diarize: %s", parakeet_capi_last_error(asr)); + CHECK(nr == n_utts_json, "struct count %d != JSON count %d", nr, n_utts_json); + for (int i = 0; i < nr; ++i) + CHECK(r[i].text && r[i].start <= r[i].end && r[i].speaker >= 0, + "bad result %d", i); + parakeet_capi_free_sas_results(r, nr); } - // Count utterances and words. - int n_utts = count_array_elements(doc, "utterances"); - int n_words = count_array_elements(doc, "words"); - std::fprintf(stderr, "test_combined_offline: %d utterances, %d words\n", - n_utts, n_words); + // 3. Streaming diarization vs offline diarization. + { + char* json = parakeet_capi_diarize_pcm(diar, pcm.data(), n, 16000); + std::vector> offline; + CHECK(json && parse_segments(json, offline), "diarize_pcm"); + parakeet_capi_free_string(json); - if (n_utts < 0 || n_words < 0) { - std::fprintf(stderr, "test_combined_offline: failed to count arrays\n"); - ok = false; - } - if (n_words == 0) { - std::fprintf(stderr, "test_combined_offline: no words transcribed\n"); - ok = false; + parakeet_diar_stream* ds = parakeet_capi_diarize_stream_begin(diar); + CHECK(ds != nullptr, "diarize_stream_begin: %s", parakeet_capi_last_error(diar)); + std::vector streamed; + for (int lo = 0; ds && lo < n; lo += 8000) { + const int len = std::min(8000, n - lo); + parakeet_diar_segment* segs = nullptr; + int ns = 0; + const int rc = parakeet_capi_diarize_stream_feed(ds, pcm.data() + lo, len, + lo + len >= n, &segs, &ns); + CHECK(rc == 0, "diarize_stream_feed: %s", parakeet_capi_last_error(diar)); + streamed.insert(streamed.end(), segs, segs + ns); + parakeet_capi_free_diar_segments(segs); + } + parakeet_capi_diarize_stream_free(ds); + std::sort(streamed.begin(), streamed.end(), [](const auto& a, const auto& b) { + return a.start != b.start ? a.start < b.start : a.speaker < b.speaker; + }); + std::printf("streaming diarization: %zu segments (offline %zu)\n", + streamed.size(), offline.size()); + CHECK(streamed.size() == offline.size(), "segment count differs"); + for (size_t i = 0; i < std::min(streamed.size(), offline.size()); ++i) { + std::printf(" spk%d %6.2f-%6.2f offline spk%d %6.2f-%6.2f\n", + streamed[i].speaker, streamed[i].start, streamed[i].end, + (int)offline[i][0], offline[i][1], offline[i][2]); + CHECK(streamed[i].speaker == (int)offline[i][0] && + std::fabs(streamed[i].start - offline[i][1]) <= 0.1 && + std::fabs(streamed[i].end - offline[i][2]) <= 0.1, + "segment %zu differs", i); + } } - // --- Test 2: struct variant --- - int n_results = 0; - parakeet_sas_result* results = parakeet_capi_transcribe_and_diarize( - asr_ctx, diar_ctx, pcm.data(), (int)pcm.size(), sr, &n_results); - if (!results) { - std::fprintf(stderr, "test_combined_offline: transcribe_and_diarize NULL: %s\n", - parakeet_capi_last_error(asr_ctx)); - ok = false; - } else { - std::fprintf(stderr, "test_combined_offline: struct variant returned %d results\n", - n_results); - if (n_results != n_utts) { - std::fprintf(stderr, - "test_combined_offline: struct count %d != JSON count %d\n", - n_results, n_utts); - ok = false; - } - // Validate each result: speaker in range, text non-null, start < end. - for (int i = 0; i < n_results && i < 20; ++i) { - if (results[i].speaker < -1 || results[i].speaker > 7) { - std::fprintf(stderr, - "test_combined_offline: result[%d] speaker=%d out of range\n", - i, results[i].speaker); - ok = false; - } - if (!results[i].text) { - std::fprintf(stderr, - "test_combined_offline: result[%d] text is null\n", i); - ok = false; - } - if (results[i].end < results[i].start) { - std::fprintf(stderr, - "test_combined_offline: result[%d] end < start\n", i); - ok = false; + // 4. Streaming SAS. + { + parakeet_sas_stream* ss = parakeet_capi_sas_stream_begin(asr, diar); + CHECK(ss != nullptr, "sas_stream_begin"); + std::vector spk; + int words = 0; + std::string text; + for (int lo = 0; ss && lo < n; lo += 8000) { + const int len = std::min(8000, n - lo); + parakeet_sas_result* r = nullptr; + int nr = 0; + const int rc = parakeet_capi_sas_stream_feed(ss, pcm.data() + lo, len, + lo + len >= n, &r, &nr); + CHECK(rc == 0, "sas_stream_feed: %s", parakeet_capi_last_error(asr)); + for (int i = 0; i < nr; ++i) { + spk.push_back(r[i].speaker); + words += word_count(r[i].text); + text += std::string(text.empty() ? "" : " ") + r[i].text; } + parakeet_capi_free_sas_results(r, nr); } - // Free text strings and the array. - for (int i = 0; i < n_results; ++i) { - if (results[i].text) parakeet_capi_free_string(results[i].text); - } - parakeet_capi_free_sas_results(results); + parakeet_capi_sas_stream_free(ss); + std::printf("streaming SAS: %d words, turns %s\n %s\n", words, + show(turns(spk)).c_str(), text.c_str()); + CHECK(turns(spk) == expected_turns, "streaming turns %s", show(turns(spk)).c_str()); + CHECK(std::abs(words - n_words_offline) <= 3, "streaming words %d vs offline %d", + words, n_words_offline); } - parakeet_capi_free(asr_ctx); - parakeet_capi_free(diar_ctx); - - if (!ok) { - std::fprintf(stderr, "test_combined_offline: FAIL\n"); - return 1; - } - std::fprintf(stderr, "test_combined_offline: PASS\n"); - return 0; + parakeet_capi_free(asr); + parakeet_capi_free(diar); + std::printf(ok ? "test_combined_offline: PASS\n" : "test_combined_offline: FAIL\n"); + return ok ? 0 : 1; } diff --git a/tests/test_streaming_diarization.cpp b/tests/test_streaming_diarization.cpp index 3281a14..bff39c9 100644 --- a/tests/test_streaming_diarization.cpp +++ b/tests/test_streaming_diarization.cpp @@ -1,130 +1,175 @@ -// test_streaming_diarization.cpp +// Streaming diarization accuracy vs NeMo cache-aware streaming +// (SortformerEncLabelModel with streaming_mode=True, the model's own chunk and +// speaker-cache config). // -// Tests the streaming diarization path (Phase 2) against the offline path. -// Loads the diarization GGUF, processes a WAV file in chunks, and compares -// the total number of segments and speaker coverage with the offline path. +// Feeds the baseline clip through pk::StreamingDiarization two ways and checks +// both against NeMo's streaming output: +// A. mel chunks cut from the whole-clip mel (exactly what NeMo does) +// B. pk::StreamingMel fed with small PCM pieces (the live-audio path the +// C-API uses), re-chunked to the model's chunk size +// Checks: per-frame probabilities (max/mean abs diff) and the segments +// (count, speaker, boundaries within 20 ms). // -// Environment: -// PARAKEET_TEST_DIAR_GGUF — path to the diarization GGUF -// PARAKEET_TEST_AUDIO — path to a test WAV file - +// Env: PARAKEET_TEST_DIAR_GGUF + PARAKEET_TEST_BASELINE_DIAR +// (scripts/gen_diar_baseline.py); PARAKEET_TEST_DIAR_PROB_TOL as in +// test_diarization_accuracy. Skips (77) when unset. #include "diarization.hpp" #include "diarization_streaming.hpp" #include "mel.hpp" -#include "audio_io.hpp" +#include "parity.hpp" -#include +#include +#include #include #include -#include #include -int main() { - const char* gguf_path = std::getenv("PARAKEET_TEST_DIAR_GGUF"); - const char* audio_path = std::getenv("PARAKEET_TEST_AUDIO"); - if (!gguf_path || !audio_path) { - std::printf("test_streaming_diarization: PARAKEET_TEST_DIAR_GGUF or PARAKEET_TEST_AUDIO not set; skip\n"); - return 77; +namespace { + +struct Run { + std::vector probs; // [n_spk, T] + std::vector segs; +}; + +// Feed [n_mels, T] mel through the streaming diarizer in model-sized chunks. +Run stream_mel(pk::StreamingDiarization& sd, const std::vector& mel, int n_mels, int T) { + Run r; + const int ns = sd.n_speakers(), cm = sd.chunk_mel_frames(); + r.probs.assign((size_t)ns * T, 0.0f); + sd.reset(); + for (int lo = 0; lo < T; lo += cm) { + const int n = std::min(cm, T - lo); + std::vector chunk((size_t)n_mels * n); + for (int m = 0; m < n_mels; ++m) + std::copy_n(mel.begin() + (size_t)m * T + lo, n, chunk.begin() + (size_t)m * n); + auto segs = sd.feed_mel_chunk(chunk, n_mels, n, lo + n >= T); + r.segs.insert(r.segs.end(), segs.begin(), segs.end()); + for (int s = 0; s < ns; ++s) + std::copy_n(sd.last_chunk_probs().begin() + (size_t)s * n, n, + r.probs.begin() + (size_t)s * T + lo); } + std::sort(r.segs.begin(), r.segs.end(), [](const auto& a, const auto& b) { + return a.start != b.start ? a.start < b.start : a.speaker < b.speaker; + }); + return r; +} - // Load the diarization model - auto model = pk::DiarizationModel::load(gguf_path); - if (!model) { - std::printf("test_streaming_diarization: failed to load model: %s\n", gguf_path); - return 1; +int check(const char* label, const Run& r, const std::vector& ref_probs, + const std::vector& ref_segs, float tol) { + int fails = 0; + double max_d = 0.0, sum_d = 0.0; + for (size_t i = 0; i < ref_probs.size(); ++i) { + const double d = std::fabs((double)r.probs[i] - ref_probs[i]); + max_d = std::max(max_d, d); + sum_d += d; } - - // Load audio - pk::Audio audio; - if (!pk::load_audio_16k_mono(audio_path, audio)) { - std::printf("test_streaming_diarization: failed to load audio: %s\n", audio_path); - return 1; + const double mean_d = sum_d / ref_probs.size(); + if (std::getenv("PARAKEET_TEST_DIAR_VERBOSE")) { + // Per-10 s max diff, to localize divergence (e.g. after cache compression). + const size_t T = ref_probs.size() / 8; + for (size_t lo = 0; lo < T; lo += 1000) { + double m = 0.0; + for (size_t sp = 0; sp < 8; ++sp) + for (size_t t = lo; t < std::min(T, lo + 1000); ++t) + m = std::max(m, std::fabs((double)r.probs[sp * T + t] - ref_probs[sp * T + t])); + std::printf(" frames %zu..%zu max_diff %.4f\n", lo, std::min(T, lo + 1000), m); + } } - std::printf("test_streaming_diarization: loaded %s (%zu samples, %d Hz)\n", - audio_path, audio.samples.size(), audio.sample_rate); - - // 1. Run offline diarization for reference - auto offline_result = model->diarize_pcm(audio.samples, audio.sample_rate); - std::printf("test_streaming_diarization: offline segments = %zu\n", - offline_result.segments.size()); - - // 2. Run streaming diarization - pk::StreamingDiarization stream(model->loader()); - stream.reset(); - - // Compute mel features for the full audio - std::vector mel; - int n_mels = 0, T = 0; - model->mel().compute(audio.samples, mel, n_mels, T); - std::printf("test_streaming_diarization: mel = %d x %d\n", n_mels, T); - - int chunk_len = stream.chunk_len(); - int n_mels_expected = stream.n_mels(); - assert(n_mels == n_mels_expected); - - std::vector stream_segs; - int offset = 0; - while (offset < T) { - int n_frames = std::min(chunk_len, T - offset); - bool is_last = (offset + n_frames >= T); - - // Extract chunk: mel[m*n_frames + t] - std::vector chunk((size_t)n_mels * n_frames); - for (int m = 0; m < n_mels; ++m) - for (int t = 0; t < n_frames; ++t) - chunk[(size_t)m * n_frames + t] = mel[(size_t)m * T + (offset + t)]; - - auto segs = stream.feed_mel_chunk(chunk, n_mels, n_frames, is_last); - for (auto& s : segs) - stream_segs.push_back(s); - - offset += n_frames; + std::printf("[%s] probs: max_diff=%.5f mean_diff=%.6f\n", label, max_d, mean_d); + if (max_d > tol || mean_d > 2e-3) { std::fprintf(stderr, "[%s] FAIL: probabilities\n", label); ++fails; } + + const size_t n_ref = ref_segs.size() / 3; + std::printf("[%s] segments: ours %zu, NeMo %zu\n", label, r.segs.size(), n_ref); + if (r.segs.size() != n_ref) { + std::fprintf(stderr, "[%s] FAIL: segment count\n", label); + return fails + 1; } - - std::printf("test_streaming_diarization: streaming segments = %zu\n", - stream_segs.size()); - - // 3. Verify: streaming should produce a reasonable number of segments. - // The exact count won't match offline (streaming uses spkcache context), - // but it should be in the same ballpark. - if (stream_segs.empty()) { - std::printf("test_streaming_diarization: FAIL — no streaming segments produced\n"); - return 1; + // NeMo's rows are sorted by (start, speaker) the same way. + std::vector order(n_ref); + for (size_t i = 0; i < n_ref; ++i) order[i] = i; + std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { + return ref_segs[a * 3 + 1] != ref_segs[b * 3 + 1] ? ref_segs[a * 3 + 1] < ref_segs[b * 3 + 1] + : ref_segs[a * 3] < ref_segs[b * 3]; + }); + for (size_t i = 0; i < n_ref; ++i) { + const float* g = &ref_segs[order[i] * 3]; + const auto& o = r.segs[i]; + const bool ok = o.speaker == (int)g[0] && std::fabs(o.start - g[1]) <= 0.02f && + std::fabs(o.end - g[2]) <= 0.02f; + std::printf(" %s spk%d %6.2f-%6.2f NeMo spk%d %6.2f-%6.2f\n", ok ? "ok " : "DIFF", + o.speaker, o.start, o.end, (int)g[0], g[1], g[2]); + if (!ok) ++fails; } + return fails; +} - // Count unique speakers in both - std::set offline_spk, stream_spk; - for (auto& s : offline_result.segments) offline_spk.insert(s.speaker); - for (auto& s : stream_segs) stream_spk.insert(s.speaker); - - std::printf("test_streaming_diarization: offline speakers = %zu, streaming speakers = %zu\n", - offline_spk.size(), stream_spk.size()); +} // namespace - // Streaming should detect at least 1 speaker - if (stream_spk.empty()) { - std::printf("test_streaming_diarization: FAIL — no speakers detected in streaming\n"); - return 1; +int main() { + const char* gguf = std::getenv("PARAKEET_TEST_DIAR_GGUF"); + const char* base = std::getenv("PARAKEET_TEST_BASELINE_DIAR"); + if (!gguf || !base) { + std::fprintf(stderr, "test_streaming_diarization: PARAKEET_TEST_DIAR_GGUF and/or " + "PARAKEET_TEST_BASELINE_DIAR not set; skip\n"); + return 77; } - - // Segments should be in chronological order - for (size_t i = 1; i < stream_segs.size(); ++i) { - if (stream_segs[i].start < stream_segs[i-1].start) { - std::printf("test_streaming_diarization: FAIL — segments not in order at %zu\n", i); - return 1; - } + const char* tol_env = std::getenv("PARAKEET_TEST_DIAR_PROB_TOL"); + const float tol = tol_env ? (float)std::atof(tol_env) : 0.02f; + + auto m = pk::DiarizationModel::load(gguf); + if (!m) { std::fprintf(stderr, "load failed: %s\n", gguf); return 1; } + + std::vector audio, ref_probs, ref_segs; + std::vector shape; + if (!pktest::load_baseline(base, "audio", audio, shape)) return 1; + if (!pktest::load_baseline(base, "stream_probs", ref_probs, shape)) return 1; + const int T = (int)shape[1]; + if (!pktest::load_baseline(base, "stream_segs", ref_segs, shape)) return 1; + + pk::StreamingDiarization sd(m->loader()); + const int n_mels = sd.n_mels(); + int fails = 0; + + // A. Whole-clip mel (no peak normalization in streaming mode), trimmed to + // floor(S / hop) frames like NeMo. + { + std::vector mel; + int nm = 0, Tm = 0; + m->mel().compute(audio, mel, nm, Tm); + if (Tm < T) { std::fprintf(stderr, "mel too short: %d < %d\n", Tm, T); return 1; } + std::vector trimmed((size_t)nm * T); + for (int i = 0; i < nm; ++i) + std::copy_n(mel.begin() + (size_t)i * Tm, T, trimmed.begin() + (size_t)i * T); + fails += check("whole-clip mel", stream_mel(sd, trimmed, nm, T), ref_probs, ref_segs, tol); } - // Segment timestamps should be within the audio duration - float audio_dur = (float)audio.samples.size() / 16000.0f; - for (auto& s : stream_segs) { - if (s.start < 0.0f || s.end > audio_dur + 1.0f) { - std::printf("test_streaming_diarization: FAIL — segment out of range: %.2f-%.2f (dur=%.2f)\n", - s.start, s.end, audio_dur); - return 1; + // B. Incremental mel from 100 ms PCM pieces. + { + pk::StreamingMel sm(m->loader()); + std::vector> cols; // per-frame mel columns + auto take = [&](const std::vector& fm, int n) { + for (int t = 0; t < n; ++t) { + std::vector col(n_mels); + for (int i = 0; i < n_mels; ++i) col[i] = fm[(size_t)i * n + t]; + cols.push_back(std::move(col)); + } + }; + for (size_t lo = 0; lo < audio.size(); lo += 1600) { + const int n = (int)std::min(1600, audio.size() - lo); + int nf = 0; + auto fm = sm.feed(audio.data() + lo, n, nf); + take(fm, nf); } + int nf = 0; + auto tail = sm.finalize(nf); + take(tail, nf); + if ((int)cols.size() < T) { std::fprintf(stderr, "stream mel too short\n"); return 1; } + std::vector mel((size_t)n_mels * T); + for (int t = 0; t < T; ++t) + for (int i = 0; i < n_mels; ++i) mel[(size_t)i * T + t] = cols[t][i]; + fails += check("StreamingMel", stream_mel(sd, mel, n_mels, T), ref_probs, ref_segs, tol); } - std::printf("test_streaming_diarization: PASS (%zu streaming segments, %zu offline)\n", - stream_segs.size(), offline_result.segments.size()); - return 0; + std::printf(fails ? "test_streaming_diarization: FAIL\n" : "test_streaming_diarization: PASS\n"); + return fails ? 1 : 0; } From 25f13e50c6246f4fbdedb7cac0c96cbc2555b48d Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:43:52 +0000 Subject: [PATCH 13/17] fix(sas): snap words that just miss a speaker segment ASR word and diarization segment boundaries can disagree by a frame or two, and a word that overlaps no segment got speaker -1. It now takes the nearest segment's speaker when that segment is within 0.5 s. Words farther from any segment are still unassigned. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- src/sas_merge.cpp | 15 ++++++++++++++- src/sas_merge.hpp | 12 +++++------- tests/test_sas_merge.cpp | 21 +++++++++++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/sas_merge.cpp b/src/sas_merge.cpp index bc05a0a..82c13b2 100644 --- a/src/sas_merge.cpp +++ b/src/sas_merge.cpp @@ -7,7 +7,8 @@ namespace pk { std::vector merge_asr_diarization( const std::vector& words, - const std::vector& segs) + const std::vector& segs, + float max_snap_sec) { std::vector result; result.reserve(words.size()); @@ -38,6 +39,18 @@ std::vector merge_asr_diarization( } } + if (best_speaker < 0) { + // No overlap: snap to the nearest segment within max_snap_sec. + float best_dist = max_snap_sec; + for (const auto& seg : sorted_segs) { + const float dist = seg.end <= w.start ? w.start - seg.end : seg.start - w.end; + if (dist <= best_dist) { + best_dist = dist; + best_speaker = seg.speaker; + } + } + } + SpeakerWord sw; sw.speaker = best_speaker; sw.text = w.text; diff --git a/src/sas_merge.hpp b/src/sas_merge.hpp index 22eb165..2030c86 100644 --- a/src/sas_merge.hpp +++ b/src/sas_merge.hpp @@ -32,15 +32,13 @@ struct SpeakerUtterance { // // For each word, the dominant active speaker is the one whose diarization // segment overlaps the word's [start, end] interval by the largest amount. -// Words with no overlapping segment get speaker = -1. -// -// `word_frame_sec` and `diar_frame_sec` are the ASR and diarization encoder -// frame strides (seconds per encoder frame). They are not used for the merge -// itself (timestamps are already in seconds) but are exposed in the signature -// for future streaming use where frame-level alignment is needed. +// A word that overlaps no segment (ASR and diarization boundaries can disagree +// by a frame or two) takes the nearest segment's speaker when that segment is +// within `max_snap_sec`; otherwise speaker = -1. std::vector merge_asr_diarization( const std::vector& words, - const std::vector& segs); + const std::vector& segs, + float max_snap_sec = 0.5f); // Group speaker-attributed words into utterances. // Consecutive words with the same speaker and gap <= max_gap_sec are joined. diff --git a/tests/test_sas_merge.cpp b/tests/test_sas_merge.cpp index fa250b3..e409593 100644 --- a/tests/test_sas_merge.cpp +++ b/tests/test_sas_merge.cpp @@ -90,6 +90,26 @@ static void test_no_speaker() { CHECK(swords[0].speaker == -1); } +// ── Test 3b: no overlap, but a segment within the snap distance ───────── +// ASR and diarization boundaries can disagree slightly: a word that just +// misses a segment takes the nearest segment's speaker. +static void test_snap_to_nearest() { + std::vector words = { + {"well", 19.92f, 20.00f, 0.7f}, // 0.10 s before spk 1 starts + {"far", 25.00f, 25.20f, 0.7f}, // 1.4 s after spk 1 ends + }; + std::vector segs = { + {0, 14.78f, 18.75f}, // 1.17 s away from "well" + {1, 20.10f, 23.60f}, + }; + auto swords = merge_asr_diarization(words, segs); + CHECK(swords[0].speaker == 1); + CHECK(swords[1].speaker == -1); + // Snapping can be disabled. + swords = merge_asr_diarization(words, segs, 0.0f); + CHECK(swords[0].speaker == -1); +} + // ── Test 4: utterance grouping — same speaker, small gap ────────────────── static void test_grouping_same_speaker() { std::vector swords = { @@ -210,6 +230,7 @@ int main() { test_basic_assignment(); test_dominant_speaker(); test_no_speaker(); + test_snap_to_nearest(); test_grouping_same_speaker(); test_grouping_speaker_change(); test_grouping_large_gap(); From 5633c78ab34835483cc5082743499d8082f10708 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:43:59 +0000 Subject: [PATCH 14/17] chore: stop tracking the ROCm design spec docs/superpowers/ holds local planning notes and stays out of the tree. The file is kept locally. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- ...26-08-28-rocm-release-and-docker-design.md | 273 ------------------ 1 file changed, 273 deletions(-) delete mode 100644 docs/superpowers/specs/2026-08-28-rocm-release-and-docker-design.md diff --git a/docs/superpowers/specs/2026-08-28-rocm-release-and-docker-design.md b/docs/superpowers/specs/2026-08-28-rocm-release-and-docker-design.md deleted file mode 100644 index 2f203e7..0000000 --- a/docs/superpowers/specs/2026-08-28-rocm-release-and-docker-design.md +++ /dev/null @@ -1,273 +0,0 @@ -# ROCm release binaries and Docker images - -Date: 2026-08-28 - -## Goal - -Make ROCm a first-class published backend alongside CPU, Vulkan, CUDA, and -Metal. Each tagged release will provide Linux x64 ROCm bundles for the CLI and -server and for the shared C API. The container workflow will publish matching -ROCm CLI and server images. Documentation will describe the supported AMD GPU -targets, host requirements, build flags, artifact names, container tags, and -device passthrough. - -The initial runtime and validation target is ROCm 7.2.4 on Ubuntu 24.04. The -hardware gate is the Ryzen AI Max+ 395 / Radeon 8060S (`gfx1151`) available as -`strix:gpu0` through `rc`. - -## Current evidence - -Clean commit `f469a57` builds with the pinned ggml v0.13.0 HIP backend when -configured for `gfx1151`. On Strix Halo with ROCm 7.2.4, the 110M F16 TDT model -produced the exact reference transcript. - -The warmed transcription measurements for `tests/fixtures/speech.wav` were: - -| Backend | Processing time | Transcript | -| --- | ---: | --- | -| ROCm 7.2.4 | 34.505 ms | Exact reference | -| Vulkan / RADV | 53.914 ms | Exact reference | -| CPU, 8 threads | 68.015 ms | Exact reference | - -These measurements establish viability and guide documentation. They are not a -performance threshold in CI because runner load and driver versions vary. - -## Approaches considered - -### One fat ROCm bundle that uses the host ROCm runtime - -Build one Linux x64 HIP backend containing code objects for a curated set of -AMD architectures. Package parakeet and ggml, but require a compatible ROCm -userspace installation on the host. - -This is the selected approach. It gives users one clearly named artifact and -keeps the release download reasonably sized. It follows the shape of upstream -ggml/llama.cpp ROCm releases. - -### Fully self-contained ROCm release bundle - -Bundle the HIP runtime, hipBLAS, rocBLAS, rocSOLVER, hipBLASLt, and all -architecture databases. This would make the tarball several gigabytes and -couple it tightly to a driver/runtime combination. The installed ROCm 7.2.4 -development stack used for the probe occupied more than 8 GB; hipBLASLt alone -occupied about 4.5 GB. This option is rejected for release tarballs. - -### Separate Radeon and Instinct bundles - -Split code objects and runtime guidance into consumer/APU and datacenter -artifacts. This reduces each individual HIP library but multiplies assets, -documentation paths, and support ambiguity. It remains a fallback only if the -fat HIP artifact exceeds GitHub artifact or release limits. - -## Supported GPU targets - -The Linux x64 release and Docker images will build the following HIP targets: - -```text -gfx908;gfx90a;gfx942;gfx1030;gfx1100;gfx1101;gfx1102;gfx1150;gfx1151;gfx1200;gfx1201 -``` - -This covers the currently relevant ROCm-supported AMD Instinct generations, -RDNA2/RDNA3/RDNA4 Radeon GPUs, and Ryzen AI APUs including Strix Halo. The list -matches the broad target set used by upstream llama.cpp ROCm containers. It is -passed explicitly through `GPU_TARGETS`; builds must not depend on compiler -auto-detection from the GPU-less GitHub runner. - -ROCm images and release artifacts are Linux x86-64 only. CPU and Vulkan remain -the portable choices for AMD hardware outside this target list or on other -operating systems. - -## Build configuration - -The release and Docker builds use: - -```text --DPARAKEET_GGML_HIP=ON --DGPU_TARGETS= --DGGML_HIP_NO_VMM=ON --DGGML_NATIVE=OFF -``` - -`GGML_HIP_NO_VMM=ON` is required for predictable behavior on Strix Halo, whose -HIP device reports no VMM support. The existing persistent `ggml_gallocr` fast -path and zero-copy device weight realization remain unchanged. - -The HIP compiler and ROCm root are set explicitly when CMake cannot infer them: - -```text --DCMAKE_HIP_COMPILER=/opt/rocm/lib/llvm/bin/clang++ --DCMAKE_HIP_COMPILER_ROCM_ROOT=/opt/rocm-7.2.4 -``` - -The pinned ggml HIP backend does not support a fully static ggml build. ROCm -artifacts therefore ship the required ggml shared libraries next to the -executables or `libparakeet.so`, with an `$ORIGIN` runtime search path. This is -an implementation detail of the bundle; the public CLI, server, and C API stay -unchanged. - -## Release assets - -Add one `rocm` / `x64` entry to the Linux release matrix. Use an Ubuntu 24.04 -runner and install the ROCm 7.2.4 HIP compiler, device libraries, hipBLAS, and -rocBLAS development packages from AMD's official repository. - -Tagged releases and manual workflow runs produce: - -```text -parakeet--bin-linux-rocm-x64.tar.gz -parakeet--lib-linux-rocm-x64.tar.gz -``` - -The binary bundle contains: - -- `parakeet-cli` -- `parakeet-server` -- the ggml base, CPU, and HIP shared libraries required by the executables -- `LICENSE` -- `README.md` - -The library bundle contains: - -- `libparakeet.so` -- the ggml base, CPU, and HIP shared libraries required by `libparakeet.so` -- `include/parakeet_capi.h` -- `LICENSE` -- `README.md` - -Both bundles require a compatible ROCm 7.2 userspace installation on the host. -Packaging verifies this boundary with `ldd` from outside the build tree. Every -non-system dependency must resolve either from the bundle or from the -documented ROCm runtime. - -The existing release upload job needs no new publication mechanism: the ROCm -matrix entry emits the same binary and library artifact outputs as the other -Linux backends. - -## Docker images - -Extend the existing Docker matrix with a `rocm` variant for Linux `amd64`. -ROCm is not added to the arm64 matrix. - -The existing image names gain the following tags: - -```text -ghcr.io/mudler/parakeet.cpp-cli:latest-rocm -ghcr.io/mudler/parakeet.cpp-server:latest-rocm -ghcr.io/mudler/parakeet.cpp-cli:-rocm -ghcr.io/mudler/parakeet.cpp-server:-rocm -ghcr.io/mudler/parakeet.cpp-cli:sha--rocm -ghcr.io/mudler/parakeet.cpp-server:sha--rocm -``` - -CPU retains the unsuffixed `latest` tag. CUDA retains `latest-cuda`. - -The Docker build keeps `ubuntu:24.04` as its build and runtime base and -registers AMD's official ROCm 7.2.4 package repository. The build stage installs -the same minimal development set validated on Strix Halo: `hipcc`, `hip-dev`, -`rocm-device-libs`, `hipblas-dev`, and `rocblas-dev`. The ROCm runtime stage -installs `rocm-hip-runtime` and `rocm-hip-libraries`, then receives the staged -ggml shared libraries. Unlike the thin release tarball, the ROCm container is -turnkey once the host kernel driver exposes the GPU devices. - -Users run the image with at least: - -```text ---device=/dev/kfd --device=/dev/dri --group-add video -``` - -Models and audio remain external. The CLI image keeps the `parakeet-cli` -entrypoint, and the server image keeps `parakeet-server --host 0.0.0.0`, exactly -as the CPU and CUDA variants do. - -Pull requests continue building only the CPU Docker variants. ROCm joins CUDA -on pushes to `master`, version tags, and manual dispatch because its multi-GPU -code generation and base image are expensive. The merge job creates a -single-platform `linux/amd64` manifest for each ROCm CLI/server tag, using the -same digest and metadata flow as the other variants. - -## Validation - -### Build-time validation - -The release and container workflows must: - -1. Configure HIP with the explicit target list and `GGML_HIP_NO_VMM=ON`. -2. Build the CLI and server. -3. Build the shared C API library in a separate tree. -4. Run the existing usage-banner smoke checks. -5. Inspect staged binaries and libraries with `ldd` after moving them outside - the build tree. -6. Fail if a required ggml library is missing from the package. - -GitHub-hosted runners do not need an AMD GPU. They compile code objects for the -explicit architecture list and perform non-device packaging checks. - -### Strix Halo hardware gate - -Before merging, copy or check out the implementation branch into the shared -`rc` workspace and run all GPU work through `rc run -d strix:gpu0`. Never run -directly on the GPU host without a lease. - -Validate the release-style build, extracted bundles, and Docker images: - -1. `rocminfo` reports `gfx1151`. -2. parakeet selects `ROCm0`, not CPU or Vulkan. -3. Model-independent tests pass. -4. The extracted CLI bundle transcribes the 110M F16 anchor model and produces - the exact reference transcript. -5. The extracted server bundle serves an OpenAI-compatible transcription - request with the same transcript. -6. The extracted shared-library bundle passes a C-API load, transcribe, and - free smoke test. -7. The CLI ROCm image produces the reference transcript with `/dev/kfd` and - `/dev/dri` passed through. -8. The server ROCm image returns the reference transcript over HTTP. -9. Warmed ROCm, Vulkan, and CPU timings are recorded for documentation and - regression context. - -The transcript is a correctness gate. Timing is informational unless a later -performance specification introduces a stable threshold. - -## Documentation - -Update `README.md` to include: - -- `rocm` in the Linux x64 release matrix -- the two ROCm release asset names -- the supported HIP target list and representative GPU families -- ROCm 7.2 host runtime requirements and an official installation link -- a source-build example with the HIP CMake flags -- automatic `ROCm0` selection and `PARAKEET_DEVICE=ROCm0` override -- the `latest-rocm`, versioned, and commit Docker tags -- Docker device-passthrough examples for both CLI and server -- the Strix Halo correctness and indicative performance results - -Update `examples/server/README.md` with the ROCm server image tag, device -passthrough, and model mounting/fetching examples. - -Comments in `.github/workflows/release.yml`, `.github/workflows/docker.yml`, -and `Dockerfile` must describe CPU, CUDA, and ROCm behavior accurately after -the matrix expansion. - -## Error handling and compatibility - -- If no compiled HIP code object matches the user's GPU, ggml will fail at - device execution. Documentation directs unsupported GPUs to Vulkan or CPU. -- If ROCm userspace is absent for a release tarball, the dynamic loader error - is expected; documentation lists the runtime prerequisite. -- Docker users must expose `/dev/kfd` and `/dev/dri`. Documentation calls this - out next to every ROCm run example. -- `PARAKEET_DEVICE=cpu` continues to force CPU even in a ROCm build. -- The public C and C++ APIs, model format, decoder behavior, and ABI version do - not change. -- No changes may replace the persistent allocator, introduce per-call weight - copies, or route supported HIP graphs through the scheduler fast path. - -## Out of scope - -- Windows HIP/ROCm release artifacts -- ROCm on arm64 -- Installing or replacing the host kernel driver -- Bundling a multi-gigabyte ROCm userspace into release tarballs -- Backend-specific kernel optimization or rocWMMA tuning -- Changing decoding, model conversion, quantization, or the public API From 17c762e9ee03719fa62ee5f4c85da20fcf8e8d92 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:59:19 +0000 Subject: [PATCH 15/17] feat(diarization): follow the model's streaming_mode in diarize_* NeMo's diarize() for Nemotron-3-Diarization runs cache-aware streaming inference (streaming_mode: true in the model config). diarize_path and diarize_pcm always ran the offline path, which attends over the whole clip: on a 12.3 min, 3 speaker recording it agreed with NeMo on 22% of speech frames and put almost everything on one speaker. The streaming path agrees on 100% and runs in 6.8 s instead of 11.8 s, since each chunk attends over at most 528 steps. The converter writes parakeet.diar.streaming_mode; GGUFs without it default to streaming. run_offline, run_streaming and segments_from_probs are public so tests can check both modes. test_diarization_accuracy now also checks the default diarize_pcm against NeMo's diarize(). PARAKEET_TEST_DIAR_VERBOSE prints the streaming probability diff per 10 s window. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- scripts/convert_parakeet_to_gguf.py | 4 ++ src/diarization.cpp | 61 ++++++++++++++--- src/diarization.hpp | 13 +++- src/model_loader.cpp | 1 + src/model_loader.hpp | 1 + tests/test_diarization_accuracy.cpp | 102 +++++++++++++++------------- 6 files changed, 122 insertions(+), 60 deletions(-) diff --git a/scripts/convert_parakeet_to_gguf.py b/scripts/convert_parakeet_to_gguf.py index f2ebfc1..a43bd59 100644 --- a/scripts/convert_parakeet_to_gguf.py +++ b/scripts/convert_parakeet_to_gguf.py @@ -353,6 +353,10 @@ def main(): w.add_float32("parakeet.diar.onset_threshold", 0.5) w.add_float32("parakeet.diar.offset_threshold", 0.5) + # NeMo diarize() runs streaming inference when streaming_mode is set. + w.add_bool("parakeet.diar.streaming_mode", + bool(_get_cfg_value(model_cfg, "streaming_mode", False))) + # Streaming speaker-cache config (SortformerModules), in encoder frames. # Defaults are the SortformerModules constructor defaults. def sf(key, default): diff --git a/src/diarization.cpp b/src/diarization.cpp index b4bf7e5..17d8915 100644 --- a/src/diarization.cpp +++ b/src/diarization.cpp @@ -1,4 +1,5 @@ #include "diarization.hpp" +#include "diarization_streaming.hpp" #include "audio_io.hpp" #include "backend.hpp" @@ -118,23 +119,63 @@ void DiarizationModel::speaker_probs(const std::vector& samples, } DiarizationResult DiarizationModel::run(const std::vector& samples) { - const ParakeetConfig& cfg = loader_.config(); + // NeMo's diarize() follows the checkpoint's streaming_mode (true for + // Nemotron-3-Diarization). The streaming path is also the one that holds + // up on long audio: offline attends over the whole clip, far beyond the + // training sessions (and quadratic in length). + return loader_.config().diarization.streaming_mode ? run_streaming(samples) + : run_offline(samples); +} +DiarizationResult DiarizationModel::run_offline(const std::vector& samples) const { std::vector probs; - int n_spk = 0, T_out = 0; - speaker_probs(samples, probs, n_spk, T_out); + int n_spk = 0, T = 0; + speaker_probs(samples, probs, n_spk, T); + DiarizationResult result; + result.segments = segments_from_probs(probs, n_spk, T); + result.n_speakers = (int)loader_.config().diarization.n_speakers; + return result; +} - // 4. Post-process -> speaker segments +DiarizationResult DiarizationModel::run_streaming(const std::vector& samples) const { + const ParakeetConfig& cfg = loader_.config(); DiarizationResult result; - result.segments = postprocess( - probs, n_spk, T_out, - cfg.diarization.frame_resolution_sec, - cfg.diarization.onset_threshold, - cfg.diarization.offset_threshold); - result.n_speakers = n_spk; + result.n_speakers = (int)cfg.diarization.n_speakers; + + // Whole-clip log-mel, not peak-normalized in streaming mode, trimmed to + // floor(S / hop) frames like NeMo. + std::vector feats; + int n_mels = 0, T = 0; + mel_->compute(samples, feats, n_mels, T); + if (cfg.hop_length > 0) T = std::min(T, (int)(samples.size() / cfg.hop_length)); + if (T <= 0) return result; + const int T_full = (int)(feats.size() / n_mels); + + StreamingDiarization sd(loader_); + const int cm = sd.chunk_mel_frames(); + std::vector chunk; + for (int lo = 0; lo < T; lo += cm) { + const int n = std::min(cm, T - lo); + chunk.resize((size_t)n_mels * n); + for (int m = 0; m < n_mels; ++m) + std::copy_n(feats.begin() + (size_t)m * T_full + lo, n, chunk.begin() + (size_t)m * n); + for (const auto& g : sd.feed_mel_chunk(chunk, n_mels, n, lo + n >= T)) + result.segments.push_back({g.speaker, g.start, g.end}); + } + std::sort(result.segments.begin(), result.segments.end(), + [](const SpeakerSegment& a, const SpeakerSegment& b) { + return a.start != b.start ? a.start < b.start : a.speaker < b.speaker; + }); return result; } +std::vector DiarizationModel::segments_from_probs( + const std::vector& probs, int n_spk, int T) const { + const auto& d = loader_.config().diarization; + return postprocess(probs, n_spk, T, d.frame_resolution_sec, d.onset_threshold, + d.offset_threshold); +} + std::vector DiarizationModel::postprocess( const std::vector& probs, int n_spk, int T_out, float frame_sec, float onset, float offset) const { diff --git a/src/diarization.hpp b/src/diarization.hpp index 5ce1af6..4d39b75 100644 --- a/src/diarization.hpp +++ b/src/diarization.hpp @@ -50,6 +50,16 @@ class DiarizationModel { void speaker_probs(const std::vector& pcm16k, std::vector& probs, int& n_spk, int& T) const; + // Offline segments for probabilities from speaker_probs (hysteresis at the + // model's onset/offset, 10 ms frames, rounded to 10 ms). + std::vector segments_from_probs(const std::vector& probs, + int n_spk, int T) const; + + // The two pipelines diarize_* chooses between (config().diarization. + // streaming_mode, as NeMo's diarize() does). Input is 16 kHz PCM. + DiarizationResult run_offline(const std::vector& pcm16k) const; + DiarizationResult run_streaming(const std::vector& pcm16k) const; + const ParakeetConfig& config() const { return loader_.config(); } const ModelLoader& loader() const { return loader_; } @@ -59,8 +69,7 @@ class DiarizationModel { private: DiarizationModel() = default; - // Internal: run the full pipeline (mel → encoder → head → postprocess) - // on already-16kHz PCM. + // Dispatch to run_offline / run_streaming on already-16 kHz PCM. DiarizationResult run(const std::vector& samples); // Post-process per-frame speaker probabilities into speaker segments. diff --git a/src/model_loader.cpp b/src/model_loader.cpp index 2de057c..ae33991 100644 --- a/src/model_loader.cpp +++ b/src/model_loader.cpp @@ -210,6 +210,7 @@ bool ModelLoader::load(const std::string& path){ // Streaming (speaker cache) config, in ENCODER frames as in NeMo // SortformerModules. Defaults are the Nemotron-3-Diarization values, // for GGUFs converted before these keys were written. + d.streaming_mode = kv_bool(gguf_, "parakeet.diar.streaming_mode", true); d.chunk_len = (int32_t)kv_u32(gguf_, "parakeet.diar.chunk_len", 264); d.spkcache_len = (int32_t)kv_u32(gguf_, "parakeet.diar.spkcache_len", 264); d.fifo_len = (int32_t)kv_u32(gguf_, "parakeet.diar.fifo_len", 0); diff --git a/src/model_loader.hpp b/src/model_loader.hpp index 1beddf6..7cfedae 100644 --- a/src/model_loader.hpp +++ b/src/model_loader.hpp @@ -82,6 +82,7 @@ struct ParakeetConfig { float frame_resolution_sec=0.01f; // output frame duration float onset_threshold=0.5f; // hysteresis onset float offset_threshold=0.5f; // hysteresis offset + bool streaming_mode=true; // NeMo diarize() default: streaming // --- streaming (speaker cache) config, in ENCODER frames (80 ms) --- int32_t chunk_len=264; // encoder frames per chunk int32_t spkcache_len=264; // speaker cache size diff --git a/tests/test_diarization_accuracy.cpp b/tests/test_diarization_accuracy.cpp index 2a26a3c..e5879c2 100644 --- a/tests/test_diarization_accuracy.cpp +++ b/tests/test_diarization_accuracy.cpp @@ -4,8 +4,10 @@ // (scripts/gen_diar_baseline.py) and checks it against NeMo's own output: // // 1. offline speaker probabilities: same shape, max/mean abs diff in bounds -// 2. offline segments: same count, same speakers, boundaries within 20 ms -// 3. frame-level speaker activity agreement (10 ms grid) >= 99.5% +// 2. offline segments: same count, same speakers, boundaries within 20 ms, +// and frame-level speaker activity agreement (10 ms grid) >= 99.5% +// 3. the same for diarize_pcm, which follows the model's streaming_mode +// like NeMo's diarize() (streaming for Nemotron-3-Diarization) // // The default fixture is tests/fixtures/two_speakers.wav (LibriSpeech 1272 and // 2086 alternating, A-B-A-B), where NeMo finds 5 segments across 2 speakers. @@ -51,6 +53,47 @@ std::vector to_grid(const std::vector& segs, int n_spk, int T) { return g; } +int check_segments(const char* label, const std::vector& got, + const std::vector& ref, int n_spk, int T) { + int fails = 0; + std::vector ours; + for (const auto& g : got) ours.push_back({g.speaker, g.start, g.end}); + auto same = [](const Seg& a, const Seg& b) { + return a.spk == b.spk && std::fabs(a.start - b.start) <= 0.02f && + std::fabs(a.end - b.end) <= 0.02f; + }; + std::printf("[%s] segments: ours %zu, NeMo %zu\n", label, ours.size(), ref.size()); + for (size_t i = 0; i < std::max(ours.size(), ref.size()); ++i) { + const bool ho = i < ours.size(), hr = i < ref.size(); + std::printf(" %s spk%d %6.2f-%6.2f NeMo spk%d %6.2f-%6.2f\n", + ho && hr && same(ours[i], ref[i]) ? "ok " : "DIFF", + ho ? ours[i].spk : -1, ho ? ours[i].start : 0.f, ho ? ours[i].end : 0.f, + hr ? ref[i].spk : -1, hr ? ref[i].start : 0.f, hr ? ref[i].end : 0.f); + if (!(ho && hr && same(ours[i], ref[i]))) ++fails; + } + + // Frame-level agreement over frames where either side has speech. + const std::vector go = to_grid(ours, n_spk, T), gr = to_grid(ref, n_spk, T); + int active = 0, agree = 0; + for (int t = 0; t < T; ++t) { + bool any = false, eq = true; + for (int s = 0; s < n_spk; ++s) { + const char a = go[(size_t)s * T + t], b = gr[(size_t)s * T + t]; + any = any || a || b; + eq = eq && a == b; + } + if (any) { ++active; agree += eq ? 1 : 0; } + } + const double agreement = active ? (double)agree / active : 1.0; + std::printf("[%s] frame agreement: %.2f%% of %d active frames\n", label, 100.0 * agreement, active); + if (agreement < 0.995) { + std::fprintf(stderr, "[%s] FAIL: frame agreement below 99.5%%\n", label); + ++fails; + } + if (fails) std::fprintf(stderr, "[%s] FAIL: segments differ from NeMo\n", label); + return fails ? 1 : 0; +} + } // namespace int main() { @@ -99,53 +142,16 @@ int main() { ++fails; } - // 2. Segments. - const std::vector ref = to_segs(ref_segs_flat); - pk::DiarizationResult r = m->diarize_pcm(audio, 16000); - std::vector ours; - for (const auto& s : r.segments) ours.push_back({s.speaker, s.start, s.end}); - std::printf("segments: ours %zu, NeMo %zu\n", ours.size(), ref.size()); - for (size_t i = 0; i < std::max(ours.size(), ref.size()); ++i) { - const bool has_o = i < ours.size(), has_r = i < ref.size(); - std::printf(" %s spk%d %6.2f-%6.2f NeMo spk%d %6.2f-%6.2f\n", - (has_o && has_r && ours[i].spk == ref[i].spk && - std::fabs(ours[i].start - ref[i].start) <= 0.02f && - std::fabs(ours[i].end - ref[i].end) <= 0.02f) ? "ok " : "DIFF", - has_o ? ours[i].spk : -1, has_o ? ours[i].start : 0.f, has_o ? ours[i].end : 0.f, - has_r ? ref[i].spk : -1, has_r ? ref[i].start : 0.f, has_r ? ref[i].end : 0.f); - } - if (ours.size() != ref.size()) { - std::fprintf(stderr, "FAIL: segment count differs\n"); - ++fails; - } else { - for (size_t i = 0; i < ref.size(); ++i) { - if (ours[i].spk != ref[i].spk || - std::fabs(ours[i].start - ref[i].start) > 0.02f || - std::fabs(ours[i].end - ref[i].end) > 0.02f) { - std::fprintf(stderr, "FAIL: segment %zu differs\n", i); - ++fails; - } - } - } + // 2. Offline segments. + fails += check_segments("offline", m->segments_from_probs(probs, n_spk, T), + to_segs(ref_segs_flat), n_spk, T); - // 3. Frame-level agreement over frames where either side has speech. - const std::vector go = to_grid(ours, n_spk, T), gr = to_grid(ref, n_spk, T); - int active = 0, agree = 0; - for (int t = 0; t < T; ++t) { - bool any = false, same = true; - for (int s = 0; s < n_spk; ++s) { - const char a = go[(size_t)s * T + t], b = gr[(size_t)s * T + t]; - any = any || a || b; - same = same && a == b; - } - if (any) { ++active; agree += same ? 1 : 0; } - } - const double agreement = active ? (double)agree / active : 1.0; - std::printf("frame agreement: %.2f%% of %d active frames\n", 100.0 * agreement, active); - if (agreement < 0.995) { - std::fprintf(stderr, "FAIL: frame agreement below 99.5%%\n"); - ++fails; - } + // 3. Default diarize_pcm (the model's streaming_mode, as NeMo diarize()). + const bool streaming = m->config().diarization.streaming_mode; + std::vector ref_default = ref_segs_flat; + if (streaming && !pktest::load_baseline(base, "stream_segs", ref_default, shape)) return 1; + fails += check_segments(streaming ? "diarize_pcm (streaming)" : "diarize_pcm (offline)", + m->diarize_pcm(audio, 16000).segments, to_segs(ref_default), n_spk, T); std::printf(fails ? "test_diarization_accuracy: FAIL\n" : "test_diarization_accuracy: PASS\n"); return fails ? 1 : 0; From ace15e7a1c997fe89091b7a2621139e1cf3c42bd Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:59:19 +0000 Subject: [PATCH 16/17] feat(cli): add --stream to the diarize example Feeds the audio through parakeet_capi_diarize_stream_* in 1 s pieces, the live path, and prints the same JSON as the offline mode. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- examples/cli/diarize.cpp | 79 +++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/examples/cli/diarize.cpp b/examples/cli/diarize.cpp index 389de1a..1e7233d 100644 --- a/examples/cli/diarize.cpp +++ b/examples/cli/diarize.cpp @@ -1,28 +1,79 @@ -// Standalone diarize tool — loads a diarization GGUF and diarizes a WAV. -// Usage: diarize -// Prints JSON segments to stdout. +// Standalone diarize tool: loads a diarization GGUF and diarizes a WAV. +// Usage: diarize [--stream] +// Prints {"speakers":N,"segments":[{"speaker","start","end"}, ...]}. +// --stream feeds the audio through the streaming C-API in 1 s pieces (NeMo +// cache-aware streaming) instead of the offline path. #include "parakeet_capi.h" +#include "audio_io.hpp" + +#include #include -#include +#include +#include + +static int diarize_stream(parakeet_ctx* ctx, const char* wav) { + pk::Audio audio; + if (!pk::load_audio_16k_mono(wav, audio)) { + std::fprintf(stderr, "cannot read %s\n", wav); + return 1; + } + parakeet_diar_stream* s = parakeet_capi_diarize_stream_begin(ctx); + if (!s) { + std::fprintf(stderr, "stream_begin failed: %s\n", parakeet_capi_last_error(ctx)); + return 1; + } + std::vector all; + const int n = (int)audio.samples.size(); + for (int lo = 0; lo < n || lo == 0; lo += 16000) { + const int len = std::min(16000, n - lo); + parakeet_diar_segment* segs = nullptr; + int ns = 0; + if (parakeet_capi_diarize_stream_feed(s, audio.samples.data() + lo, len, + lo + len >= n, &segs, &ns) != 0) { + std::fprintf(stderr, "stream_feed failed: %s\n", parakeet_capi_last_error(ctx)); + parakeet_capi_diarize_stream_free(s); + return 1; + } + all.insert(all.end(), segs, segs + ns); + parakeet_capi_free_diar_segments(segs); + if (lo + len >= n) break; + } + parakeet_capi_diarize_stream_free(s); + std::sort(all.begin(), all.end(), [](const auto& a, const auto& b) { + return a.start != b.start ? a.start < b.start : a.speaker < b.speaker; + }); + std::printf("{\"speakers\":8,\"segments\":["); + for (size_t i = 0; i < all.size(); ++i) + std::printf("%s{\"speaker\":%d,\"start\":%.2f,\"end\":%.2f}", i ? "," : "", + all[i].speaker, all[i].start, all[i].end); + std::printf("]}\n"); + return 0; +} int main(int argc, char** argv) { if (argc < 3) { - fprintf(stderr, "usage: %s \n", argv[0]); + std::fprintf(stderr, "usage: %s [--stream]\n", argv[0]); return 1; } + const bool stream = argc > 3 && std::strcmp(argv[3], "--stream") == 0; parakeet_ctx* ctx = parakeet_capi_load(argv[1]); if (!ctx) { - fprintf(stderr, "failed to load %s\n", argv[1]); + std::fprintf(stderr, "failed to load %s\n", argv[1]); return 1; } - char* json = parakeet_capi_diarize_path(ctx, argv[2]); - if (!json) { - fprintf(stderr, "diarize failed: %s\n", parakeet_capi_last_error(ctx)); - parakeet_capi_free(ctx); - return 1; + int rc = 0; + if (stream) { + rc = diarize_stream(ctx, argv[2]); + } else { + char* json = parakeet_capi_diarize_path(ctx, argv[2]); + if (!json) { + std::fprintf(stderr, "diarize failed: %s\n", parakeet_capi_last_error(ctx)); + rc = 1; + } else { + std::printf("%s\n", json); + parakeet_capi_free_string(json); + } } - printf("%s\n", json); - parakeet_capi_free_string(json); parakeet_capi_free(ctx); - return 0; + return rc; } From 78dbb4c9fb149551d275bc2e5a250fa17743a169 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 19:59:19 +0000 Subject: [PATCH 17/17] docs: add a diarization reference, drop the plan docs/diarization.md covers the model, conversion, offline and streaming inference, parity with NeMo, speaker-attributed ASR, speed and tests. AGENTS.md lists the new sources, tests, fixture, baseline script and C-API symbols. The implementation plan was a working note and now lives with the other plans outside the tree. Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- AGENTS.md | 42 ++++ docs/diarization-plan.md | 460 --------------------------------------- docs/diarization.md | 113 ++++++++++ 3 files changed, 155 insertions(+), 460 deletions(-) delete mode 100644 docs/diarization-plan.md create mode 100644 docs/diarization.md diff --git a/AGENTS.md b/AGENTS.md index fa3334b..4e3ab2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,12 +76,18 @@ src/ libparakeet implementation tdt.cpp / rnnt.cpp , TDT / RNNT greedy loops streaming_encoder.hpp/cpp, cache-aware streaming FastConformer encoder streaming.hpp/cpp , pk::StreamingSession (carried RNN-T + EOU events) + run_stream_over_pcm + diarization.hpp/cpp, pk::DiarizationModel: offline speaker diarization (Sortformer) + diarization_encoder/head.*, RoPE Transformer encoder + speaker head + diarization_streaming.*, NeMo cache-aware streaming diarization (speaker cache + FIFO) + sas_merge.hpp/cpp , ASR words x speaker segments -> speaker-attributed utterances examples/cli/ parakeet-cli binary subcommands: info, transcribe (+ --stream), quantize + diarize binary: diarize [--stream] scripts/ Python tooling convert_parakeet_to_gguf.py, .nemo/.hf -> GGUF (--dtype f32|f16|q8_0) gen_nemo_baseline.py , NeMo intermediates -> baseline.gguf gen_stream_baseline.py , NeMo cache-aware streaming encode+decode -> stream baseline.gguf + gen_diar_baseline.py , NeMo offline + streaming diarization -> diar baseline.gguf validate_vs_nemo.py , WER parity gate vs NeMo publish_hf.py , convert+quantize -> HF upload (dry-run default) requirements.txt , nemo_toolkit[asr] + gguf @@ -101,10 +107,15 @@ tests/ ctest targets test_streaming_decode.cpp , streaming RNN-T tokens == NeMo cache-aware streaming test_streaming_eou_reset.cpp, multi-utterance streaming: decoder resets on , transcript == NeMo reset-on-EOU (issue #13; PARAKEET_TEST_BASELINE_EOU_RESET) test_capi_stream.cpp , streaming C-API transcript == NeMo streaming (PARAKEET_TEST_BASELINE_EOU_STREAM) + test_diarization_accuracy.cpp, offline diarization == NeMo (PARAKEET_TEST_BASELINE_DIAR) + test_streaming_diarization.cpp, streaming diarization == NeMo streaming (same baseline) + test_combined_offline.cpp, SAS + streaming diarization/SAS through the C-API + test_sas_merge.cpp , SAS merge/grouping (model-independent) python/check_convert.py , converter round-trip (model-dependent) python/check_baseline.py, baseline dumper (model-dependent) fixtures/clip.wav , 2 s 16 kHz mono WAV for stage parity tests fixtures/speech.wav , LibriSpeech 2086-149220-0033, ~7.4 s + fixtures/two_speakers.wav, LibriSpeech 1272 + 2086 alternating A-B-A-B, 23.6 s third_party/ vendored deps ggml/ , submodule pinned at v0.13.0 dr_wav.h , vendored single header @@ -114,6 +125,7 @@ docs/ conversion.md , GGUF schema reference quantization.md , quantization allowlist, policy, measured size + WER per type parity.md , full model coverage matrix + per-stage tensor parity + diarization.md , speaker diarization + speaker-attributed ASR: parity, C-API, speed .github/workflows/ ci.yml , build job (per-push) + closed-loop job (pull_request + dispatch) ``` @@ -258,6 +270,18 @@ parakeet_capi_stream_finalize # flush the end-of-stream tail parakeet_capi_stream_free ``` +Speaker diarization (ABI v7, additive; not used by LocalAI yet). A +diarization GGUF loads into its own `parakeet_ctx`; see `docs/diarization.md`: + +``` +parakeet_capi_diarize_path / _pcm # offline, JSON segments +parakeet_capi_transcribe_and_diarize(_json) # speaker-attributed ASR (two contexts) +parakeet_capi_free_sas_results # frees the array and every .text +parakeet_capi_diarize_stream_begin / _feed / _free / _chunk_samples +parakeet_capi_free_diar_segments +parakeet_capi_sas_stream_begin / _feed / _free +``` + `parakeet_capi_transcribe_path_json(ctx, wav, decoder)` returns malloc'd UTF-8 JSON `{"text":..,"words":[{"w","start","end","conf"}],"tokens":[{"id","t","conf"}]}` (times in seconds, conf in `(0,1]`), built from @@ -300,6 +324,24 @@ drain, a word finalizes when the next `▁`-token arrives, the last word on ## Dumping NeMo baselines +Diarization (needs NeMo main / >= 3.1: NeMo 3.0 cannot load the RoPE +encoder of nvidia/Nemotron-3-Diarization): + +``` +.venv/bin/python scripts/convert_parakeet_to_gguf.py \ + --model nvidia/Nemotron-3-Diarization --output /tmp/diar.gguf +.venv/bin/python scripts/gen_diar_baseline.py \ + --model nvidia/Nemotron-3-Diarization \ + --audio tests/fixtures/two_speakers.wav --output /tmp/diar_baseline.gguf +PARAKEET_TEST_DIAR_GGUF=/tmp/diar.gguf PARAKEET_TEST_BASELINE_DIAR=/tmp/diar_baseline.gguf \ + ctest --test-dir build -R diar --output-on-failure +``` + +Quantized diarization GGUFs keep the same segments but move probabilities +more; set `PARAKEET_TEST_DIAR_PROB_TOL=0.05` for Q8_0. + +ASR: + Used by Phase 1 parity tests. Requires the venv and a 16 kHz mono WAV. ``` diff --git a/docs/diarization-plan.md b/docs/diarization-plan.md deleted file mode 100644 index 6104743..0000000 --- a/docs/diarization-plan.md +++ /dev/null @@ -1,460 +0,0 @@ -# Plan: Nemotron-3-Diarization support in parakeet.cpp - -## What the model is - -Nemotron-3-Diarization is a Sortformer speaker diarization model -(`SortformerEncLabelModel` in NeMo). It determines "who spoke when" in -audio with up to 8 speakers. Two stages: - -1. **NEST FastConformer encoder** — 16kHz audio → log-mel → `[d_model, T]`. - Architecturally the same FastConformer parakeet.cpp already runs. -2. **Transformer encoder sorting head** — takes encoder output → per-frame, - per-speaker sigmoid logits. No text, no tokenizer, no CTC/RNNT/TDT decoder. - -Streaming uses **AOSC (Arrival-Order Speaker Cache) + FIFO queue** — a -different streaming mechanism from NeMo's cache-aware streaming that -parakeet.cpp currently implements. Input buffer latency from 80ms -(ultra-low-latency) to 30.4s (offline-style); output frame resolution -configurable in multiples of 10ms. - -## What's reusable as-is (~60-70% of the engine) - -The FastConformer encoder, mel frontend, subsampling, positional encoding, -relpos attention, conformer layers, ggml graph infrastructure, audio I/O, -and FFT are all cleanly separable from the ASR-specific heads. The encoder -emits a `[d_model, Tout]` channels-first tensor — a diarization head plugs -in at the same boundary as `CTCDecoder` (`src/ctc_decoder.cpp`). - -| Component | File(s) | Reuse | -|---|---|---| -| Mel frontend (offline) | `src/mel.cpp` (`MelFrontend`, `MelKernel`) | As-is | -| Mel frontend (GPU) | `src/mel_gpu.cpp` (`GpuMel`) | As-is | -| Mel frontend (streaming) | `src/mel.cpp` (`StreamingMel`) | As-is (if `normalize=NA`) | -| Subsampling | `src/subsampling.cpp` | As-is | -| Positional encoding | `src/pos_enc.cpp` | As-is | -| RelPos attention | `src/relpos_attention.cpp` | As-is | -| Conformer layer | `src/conformer.cpp` | As-is | -| FastConformer encoder | `src/encoder.cpp` | As-is | -| GGML graph infra | `src/ggml_graph.cpp`, `src/graph_builder.hpp` | As-is | -| Audio I/O | `src/audio_io.cpp` | As-is | -| FFT | `src/fft.cpp` | As-is | - -## Where ASR-specific assumptions are baked in - -| Location | Assumption | Fix | -|---|---|---| -| `src/model_loader.cpp:203` | `return cfg_.d_model>0 && cfg_.vocab_size>0` | Relax to `cfg_.d_model>0` or branch on arch | -| `scripts/convert_parakeet_to_gguf.py:38` | `from nemo.collections.asr.models import ASRModel` | Also accept `SortformerEncLabelModel` | -| `scripts/convert_parakeet_to_gguf.py:297-302` | Vocab/tokenizer emitted unconditionally | Make conditional on arch | -| `src/model.cpp` (entire `Model` class) | All methods return text | New `DiarizationModel` class | -| `include/parakeet_capi.h` | All entry points return transcripts | New `diarize_*` surface | -| `src/streaming_encoder.cpp:40,56-58` | Hard-asserts NeMo cache-aware streaming | New AOSC+FIFO streaming path | -| `tests/test_model_loader.cpp:21-22` | Asserts `vocab_size > 0` | Conditional on arch | - -## Architectural decisions (decide upfront, before Phase 1) - -### Decision 1: Separate `DiarizationModel` class, not bolt-on to `Model` - -The existing `Model` class (`src/model.hpp:22-119`) is entirely ASR-shaped — -every public method returns `std::string` or `Transcription`. Bolting -diarization onto it would pollute the class. A separate `DiarizationModel` -class composes the same reusable pieces (`MelFrontend`, `Encoder`, new -`DiarizationHead`) — mirroring how `StreamingSession` is already a separate -class from `Model`. - -### Decision 2: Shared segment/word timestamp types - -Both ASR and diarization produce timestamped outputs. Design the types so -they compose: - -```cpp -// Existing (src/transcription.hpp): -struct Word { std::string text; float start; float end; float conf; }; - -// New (src/diarization.hpp): -struct SpeakerSegment { - int speaker; // 0-indexed speaker label - float start; // seconds - float end; // seconds - float conf; // aggregate per-frame confidence -}; -``` - -Both use `float start/end` in seconds. The merge for combined ASR+diarization -is then: for each word's `[start, end]`, find the dominant speaker in the -overlapping segments. This is a timestamp intersection, not a deep -architecture coupling. - -### Decision 3: Mel computation stays composable - -Don't bake mel computation into `DiarizationModel`. Keep `MelFrontend` as a -standalone composable step (as it already is) so both models can share it -when running combined ASR+diarization on the same audio. - -### Decision 4: C-API designed for composition from the start - -The diarization C-API uses a separate `parakeet_diar_ctx` opaque type, not -overloading `parakeet_ctx`. This lets a caller hold both contexts and call -both APIs, and later call a combined `parakeet_capi_transcribe_and_diarize` -that takes both. - -### Decision 5: New arch string `"diarization"` - -Add `arch = "diarization"` to the arch vocabulary. The model loader dispatches -on this to know it's not an ASR model (skip vocab/decoder/joint loading, load -diarization head config instead). - ---- - -## Phase 1: Offline diarization (standalone) - -**Goal**: Load Nemotron-3-Diarization GGUF, run offline diarization on a WAV -file, produce speaker segments. No streaming. - -### 1.1 Converter changes (`scripts/convert_parakeet_to_gguf.py`) - -- Import `SortformerEncLabelModel` alongside `ASRModel`. -- Add `"diarization"` branch to `detect_arch()`. -- Make vocab/tokenizer emission conditional — skip for diarization (no - tokenizer in a diarization checkpoint; `m.tokenizer` would `AttributeError`). -- Emit new GGUF KV: - - `parakeet.diarization.num_speakers` (max 8) - - `parakeet.diarization.threshold` (sigmoid threshold, default 0.5) - - `parakeet.diarization.head_layers` (transformer encoder layer count) - - `parakeet.diarization.head_d_model` - - `parakeet.diarization.head_n_heads` - - `parakeet.diarization.head_ff_dim` -- The generic tensor loop (line 329-357) writes NeMo state_dict keys verbatim - — encoder tensors (`encoder.layers.N.*`, `encoder.pre_encode.*`) convert - with zero changes. Add Sortformer-head linear patterns to the quantization - allowlist (`_QUANTIZABLE_PATTERNS`). -- Featurizer buffer lift (`preprocessor.featurizer.fb`, `.window`) is already - generic — works as-is. - -### 1.2 Model loader changes - -- `src/model_loader.cpp:203`: relax `vocab_size>0` check to - `cfg_.d_model>0` (or branch: `arch == "diarization"` → skip vocab check). -- Add `DiarizationCfg` sub-struct to `ParakeetConfig` in - `src/model_loader.hpp`: - ```cpp - struct DiarizationCfg { - uint32_t num_speakers = 0; - float threshold = 0.5f; - uint32_t head_layers = 0; - uint32_t head_d_model = 0; - uint32_t head_n_heads = 0; - uint32_t head_ff_dim = 0; - bool present = false; - }; - ``` -- Read `parakeet.diarization.*` KV in `ModelLoader::load`. -- The `parakeet.decoder.*` / `parakeet.joint.*` / `parakeet.tdt.*` fields - already default to 0 when absent — safe for diarization GGUFs that omit - them. - -### 1.3 Diarization head (`src/diarization_head.hpp` / `.cpp`) - -New file, mirrors `src/ctc_decoder.hpp`/`.cpp` as a template: - -- `class DiarizationHead`: - - `DiarizationHead(const ModelLoader& ml)` — reads transformer encoder - config + weights. - - `void forward(const std::vector& enc, int d_model, int T, - std::vector& probs, int& num_speakers)` — takes `[d_model, T]`, - runs transformer encoder layers (standard MHSA, not relpos) + sigmoid - output layer, returns `[T, num_speakers]` per-frame speaker probabilities. -- The transformer encoder uses standard multi-head self-attention (not - relpos). parakeet.cpp currently only has `RelPosAttention` — need a plain - `MultiHeadAttention` or verify if the Sortformer head uses a different - attention variant. Check NeMo source for the exact attention type. -- Weight names: NeMo keys like - `sortformer_modules.transformer_encoder.layers.N.*`, - `sortformer_modules.encoder2unfold.*`, - `sortformer_modules.linear_layer.*`. - -### 1.4 DiarizationModel class (`src/diarization.hpp` / `.cpp`) - -New class, composes `MelFrontend` + `Encoder` + `DiarizationHead`: - -```cpp -class DiarizationModel { -public: - static std::unique_ptr load(const std::string& gguf_path); - std::vector diarize_pcm( - const std::vector& pcm, int sample_rate) const; - std::vector diarize_path(const std::string& wav_path) const; -private: - ModelLoader loader_; -}; -``` - -Orchestration: `pcm → resample to 16k → MelFrontend → Encoder → DiarizationHead -→ threshold per-frame sigmoid → merge consecutive frames with same active -speaker → segments`. - -The merge logic: threshold the per-frame per-speaker probabilities at -`cfg.diarization.threshold`, group consecutive frames where the same speaker -is active into segments, convert frame indices to seconds using `frame_sec` -(`hop_length * subsampling_factor / sample_rate`). - -### 1.5 C-API surface (`include/parakeet_capi.h`, `src/parakeet_capi.cpp`) - -New entry points (bump ABI version v5 → v6): - -```c -typedef struct parakeet_diar_ctx parakeet_diar_ctx; - -typedef struct parakeet_segment { - int speaker; - float start; - float end; - float conf; -} parakeet_segment; - -parakeet_diar_ctx* parakeet_capi_diar_load(const char* gguf_path); -void parakeet_capi_diar_free(parakeet_diar_ctx* ctx); - -parakeet_segment* parakeet_capi_diarize_path( - parakeet_diar_ctx* ctx, const char* wav_path, int* n_segments); -parakeet_segment* parakeet_capi_diarize_pcm( - parakeet_diar_ctx* ctx, const float* samples, int n_samples, - int sample_rate, int* n_segments); -void parakeet_capi_free_segments(parakeet_segment* segs); - -// JSON variant: -// {"segments":[{"speaker":0,"start":0.48,"end":2.16,"conf":0.91},...], -// "frame_sec":0.080000} -char* parakeet_capi_diarize_path_json(parakeet_diar_ctx* ctx, const char* wav_path); -char* parakeet_capi_diarize_pcm_json(parakeet_diar_ctx* ctx, - const float* samples, int n_samples, int sample_rate); -``` - -### 1.6 Tests - -- Relax `tests/test_model_loader.cpp:21-22` to not assert `vocab_size > 0` - when `arch == "diarization"`. -- `tests/test_diarization_head.cpp` — unit test: encoder output → head → - per-frame speaker probs, compare vs NeMo baseline `.npz`. -- `tests/test_diarization.cpp` — end-to-end: PCM → segments, compare vs NeMo - `diar_model.diarize()` baseline. -- Existing encoder/mel/conformer/subsampling tests carry over unchanged - (they're arch-agnostic, test components in isolation). - -### 1.7 Parity validation - -- Set up NeMo baseline: load `SortformerEncLabelModel.from_pretrained(...)`, - run `diar_model.diarize(audio=[...])`, dump segments as `.json` baseline. -- Dump intermediate tensors (mel, encoder_out, head_probs) as `.npz` for - per-component parity testing. -- Match NeMo's segment merging logic (consecutive frames, same speaker, - threshold). - ---- - -## Phase 2: Streaming diarization (AOSC + FIFO) - -**Goal**: Stream audio in chunks, get incremental speaker segments with low -latency (80ms minimum, 0.32s recommended). - -### 2.1 Sortformer streaming encoder (`src/sortformer_streaming.hpp` / `.cpp`) - -The existing `StreamingEncoder` (`src/streaming_encoder.cpp`) is NOT reusable -— it hard-asserts NeMo cache-aware streaming at the ctor (lines 40, 56-58): -`c.streaming.present`, `c.causal_downsampling`, `c.conv_causal`, -`att_context_style == "chunked_limited"`. Sortformer uses a fundamentally -different streaming mechanism (AOSC + FIFO). - -**Reusable from existing streaming code:** -- Graph-input/cache-capture pattern (`graph_input_tensor`, `capture_graph_output`) -- `run_graph` + `GraphInputPool` machinery -- Subsampling `in_valid_frames` override path - -**New (not reusable):** -- AOSC cache: retains speaker summary representations (not raw K/V columns) -- FIFO queue: manages chunk overlap/drop -- Attention cache structure (different from NeMo's conv-left-context + K/V cache) -- The conformer layer's `build_stream_layer` cache threading is the wrong - mechanism for AOSC - -### 2.2 Streaming C-API - -```c -typedef struct parakeet_diar_stream parakeet_diar_stream; - -parakeet_diar_stream* parakeet_capi_diar_stream_begin(parakeet_diar_ctx* ctx); - -// Feed PCM, get newly-finalized segments -parakeet_segment* parakeet_capi_diar_stream_feed( - parakeet_diar_stream* s, const float* pcm, int n_samples, - int* n_new_segments); - -// Flush remaining audio, get tail segments -parakeet_segment* parakeet_capi_diar_stream_finalize( - parakeet_diar_stream* s, int* n_tail_segments); - -void parakeet_capi_diar_stream_free(parakeet_diar_stream* s); -``` - -### 2.3 Streaming config in GGUF - -New GGUF KV for Sortformer streaming: -- `parakeet.sortformer.chunk_len` (frames, e.g. 340 = 27.2s at 80ms/frame) -- `parakeet.sortformer.chunk_right_context` (frames, e.g. 40) -- `parakeet.sortformer.fifo_len` (frames, e.g. 40) -- `parakeet.sortformer.spkcache_len` (AOSC cache size in frames) -- `parakeet.sortformer.spkcache_update_period` (frames, e.g. 300) - -### 2.4 Tests - -- `tests/test_sortformer_streaming.cpp` — streaming parity vs NeMo streaming - config baseline. -- Test chunk boundary correctness (no speaker label jumps at chunk edges). -- Test AOSC persistence (speaker identity maintained across chunks). - ---- - -## Phase 3: Speaker-attributed ASR (combined parakeet + diarization) - -**Goal**: Run both ASR and diarization, merge into speaker-attributed -transcription ("who said what"). - -### 3.1 Merge layer (`src/sas_merge.hpp` / `.cpp`) - -Simple timestamp intersection: -```cpp -struct SpeakerWord { - int speaker; // from diarization - std::string text; // from ASR - float start; // from ASR word - float end; // from ASR word - float conf; // from ASR word -}; - -std::vector merge_asr_diarization( - const std::vector& words, // ASR (timestamped) - const std::vector& segs, // diarization (timestamped) - float frame_sec); -``` - -For each word's `[start, end]`, find the dominant active speaker in the -overlapping diarization segments. This is a linear scan, not a deep -architecture coupling. - -### 3.2 Combined C-API - -```c -// Load both models, get speaker-attributed transcription -typedef struct parakeet_sas_result { - int speaker; - char* text; - float start; - float end; - float conf; -} parakeet_sas_result; - -parakeet_sas_result* parakeet_capi_transcribe_and_diarize( - parakeet_ctx* asr_ctx, - parakeet_diar_ctx* diar_ctx, - const float* samples, int n_samples, int sample_rate, - int* n_results); -void parakeet_capi_free_sas_results(parakeet_sas_result* results); - -// JSON variant with full per-word + per-segment detail -char* parakeet_capi_transcribe_and_diarize_json( - parakeet_ctx* asr_ctx, - parakeet_diar_ctx* diar_ctx, - const float* samples, int n_samples, int sample_rate); -``` - -### 3.3 Mel sharing optimization - -Both models compute log-mel on the same 16kHz audio. If mel configs match -(`n_mels`, `hop_length`, `n_fft`, `preemph`, `mag_power` all identical), compute -mel once and feed both encoders. If configs differ, compute mel twice (cost is -negligible vs two encoder forward passes). - -Check at load time whether the two configs are compatible for mel sharing. - -### 3.4 Streaming combined ASR + diarization - -The two models have different streaming mechanisms and latencies: -- ASR: NeMo cache-aware streaming (chunked-limited attention, conv left-context) -- Diarization: AOSC + FIFO (speaker cache, different chunk structure) - -The ASR model might emit a word at time T, but the diarization model's speaker -decision for frame T might not be finalized yet. Need a **merge buffer** that: -1. Holds ASR word hypotheses with timestamps -2. Holds diarization frame labels -3. Emits combined `(speaker, text, start, end)` only when both models have - committed to a time range - -This is a bounded-delay merge problem — doable but requires careful design. - -### 3.5 LocalAI integration - -Wire the combined ASR+diarization as a LocalAI backend endpoint: -- `/v1/audio/transcriptions` with `diarize=true` → speaker-attributed text -- Streaming variant for real-time use - -### 3.6 Tests - -- `tests/test_sas_merge.cpp` — unit test the merge logic with known - word/segment inputs. -- `tests/test_combined_offline.cpp` — end-to-end: audio → both models → - speaker-attributed transcription, compare vs NeMo SAS baseline. -- `tests/test_combined_streaming.cpp` — streaming combined with merge buffer. - -### 3.7 Publish - -- Quantize + publish diarization GGUF to HuggingFace. -- Publish combined ASR+diarization documentation. - ---- - -## Dependency graph - -``` -Phase 1 (offline diarization) - ├── 1.1 Converter ──────┐ - ├── 1.2 Loader ─────────┤ - ├── 1.3 DiarizationHead ┼── 1.4 DiarizationModel ── 1.5 C-API ── 1.6 Tests ── 1.7 Parity - └───────────────────────┘ - -Phase 2 (streaming diarization) - ├── 2.1 SortformerStreamingEncoder ── 2.2 C-API ── 2.3 GGUF KV ── 2.4 Tests - └── depends on Phase 1 (head + model + converter) - -Phase 3 (combined ASR + diarization) - ├── 3.1 Merge layer ── 3.2 C-API ── 3.3 Mel sharing ── 3.6 Tests ── 3.7 Publish - ├── 3.4 Streaming combined (depends on Phase 2) - ├── 3.5 LocalAI integration - └── depends on Phase 1 (offline diarization works) - Phase 2 (for streaming combined) -``` - -## Risk areas / unknowns - -1. **Sortformer transformer head attention type**: Need to verify whether the - Sortformer sorting head uses standard MHSA, relpos, or a variant. parakeet.cpp - currently only has `RelPosAttention`. If standard MHSA is needed, it's a new - ggml graph (not hard, but needs implementing). Check NeMo source for the exact - attention type. - -2. **AOSC cache mechanism**: The AOSC is the novel streaming component. Its exact - implementation (what is cached, how it's updated, how it interacts with the - transformer attention) needs to be understood from the NeMo source before - implementing. This is the highest-risk piece of Phase 2. - -3. **Mel config compatibility**: Whether the diarization model's mel config - matches any existing parakeet model's config (for mel sharing in Phase 3). - If `normalize="per_feature"`, streaming mel can't be used incrementally — - `MelFrontend::compute` on the full clip works fine (offline path). - -4. **Frame alignment**: ASR and diarization may have different subsampling - factors, producing different `frame_sec` values. The merge must handle this - by working in seconds (both produce timestamps in seconds), not frame indices. - -5. **Segment merging conventions**: NeMo's segment merging logic (consecutive - frames, same speaker, threshold, minimum segment duration) needs to be - matched exactly for parity. Check NeMo `diarize()` output format. diff --git a/docs/diarization.md b/docs/diarization.md new file mode 100644 index 0000000..6ddefaf --- /dev/null +++ b/docs/diarization.md @@ -0,0 +1,113 @@ +# Speaker diarization + +parakeet.cpp runs [nvidia/Nemotron-3-Diarization](https://huggingface.co/nvidia/Nemotron-3-Diarization), +a Sortformer model that answers "who spoke when" for up to 8 speakers, and +combines it with any Parakeet ASR model for speaker-attributed transcripts +("who said what"). + +## Model + +- Encoder: FeatureStacking (8 mel frames stacked, 80 ms per step) and a + 31-layer pre-norm Transformer with RoPE attention (d_model 512, 8 heads). +- Head: projection to 192, a subpixel Conv1d that upsamples 8x back to 10 ms + frames, two linear layers and a sigmoid per speaker. +- Output: per-speaker activity probabilities every 10 ms. Segments come from + thresholding at 0.5. +- Speakers are numbered in order of first appearance. + +## Converting + +``` +.venv/bin/python scripts/convert_parakeet_to_gguf.py \ + --model nvidia/Nemotron-3-Diarization --dtype q8_0 \ + --output models/nemotron-3-diarization.q8_0.gguf +``` + +`--model` also takes a local `.nemo`. The converter reads the checkpoint +directly, so it works with a NeMo that cannot instantiate the model (NeMo 3.0 +has no RoPE Transformer encoder). Sizes: F32 397 MB, F16 201 MB, Q8_0 109 MB. + +## Offline and streaming inference + +NeMo's `diarize()` for this checkpoint runs cache-aware streaming inference +(`streaming_mode: true` in the model config), and `parakeet_capi_diarize_*` +does the same: audio is processed in 264-step chunks (21.12 s) with a +speaker cache of 264 steps that keeps speaker identities stable across chunks. +This is also what keeps long recordings correct: the model is trained on +sessions of about 105 s, and attending over a whole long clip at once is +outside that range. On a 12 minute, 3 speaker recording the offline path +(which matches NeMo offline on short clips) agrees with NeMo's `diarize()` on +22% of speech frames and puts almost everything on one speaker; the streaming +path agrees on 100%. + +`DiarizationModel::run_offline` / `speaker_probs` still implement the offline +path (NeMo peak-normalizes the waveform there) for short clips and for parity +checks. + +The live streaming API (`parakeet_capi_diarize_stream_*`) takes 16 kHz PCM in +pieces of any size, computes the log-mel incrementally (bit-identical to the +whole-clip mel) and returns segments once per 21.12 s chunk. Segments that +continue past a chunk boundary are not split. + +## Parity with NeMo + +Measured against NeMo main (the reference must support the RoPE encoder), with +`scripts/gen_diar_baseline.py`: + +| Clip | Speakers | Offline prob max diff | Offline segments | Streaming segments | +|---|---|---|---|---| +| `tests/fixtures/two_speakers.wav`, 23.6 s | 2 | 0.004 | 5 / 5 identical | 5 / 5 identical | +| synthetic two-voice dialogue (VibeVoice sample), 68.5 s | 2 | 0.004 | 26 / 26 identical | 26 / 26 identical | +| synthetic three-voice dialogue (VibeVoice sample), 12.3 min | 3 | | | 100% frame agreement | + +Segment boundaries match to the 10 ms frame. F16 gives the same results; Q8_0 +keeps the same segments with a probability max diff of about 0.03. + +After the speaker cache compresses, NeMo picks cache frames with +`torch.topk`, whose order for tied scores is arbitrary. parakeet.cpp breaks ties +toward the earlier frame, so streaming probabilities can drift by up to about +0.02 on long clips while the segments stay the same. + +## Speaker-attributed ASR + +`parakeet_capi_transcribe_and_diarize(_json)` runs an ASR context and a +diarization context on the same PCM and assigns every ASR word to the speaker +whose segments overlap it most. A word that overlaps no segment takes the +nearest segment's speaker if that segment is within 0.5 s, otherwise -1. +Consecutive words from one speaker (gaps up to 0.5 s) form an utterance. + +`parakeet_capi_sas_stream_*` does the same live: when a diarization chunk +completes, the audio not yet committed is transcribed, and words that end at +least 1 s before the chunk edge are committed with their speakers. The rest is +transcribed again with the next chunk, so no word is cut at the edge. + +## Speed + +End to end with the `diarize` example (model load included), AMD Ryzen 9 +9950X3D, CPU: + +| Audio | F32 | Q8_0 | +|---|---|---| +| 23.6 s | 0.43 s | 0.25 s | +| 68.5 s | 0.80 s | 0.60 s | +| 12.3 min | 6.8 s | 6.0 s | + +Streaming cost grows linearly with length (each chunk attends over at most +528 steps), so long recordings run at about 110x real time. + +## Tests + +``` +PARAKEET_TEST_DIAR_GGUF=diar.gguf PARAKEET_TEST_BASELINE_DIAR=diar_baseline.gguf \ +PARAKEET_TEST_GGUF=asr.gguf ctest --test-dir build -R "diar|sas|combined" +``` + +- `test_diarization_accuracy`: offline probabilities and segments, and the + default `diarize_pcm` segments, against NeMo. +- `test_streaming_diarization`: streaming probabilities and segments against + NeMo streaming, from a whole-clip mel and from live 100 ms PCM pieces. +- `test_combined_offline`: speaker-attributed ASR and the streaming C-API on + the two-speaker fixture. +- `test_sas_merge`: word to speaker assignment (no model needed). + +Set `PARAKEET_TEST_DIAR_PROB_TOL=0.05` for Q8_0.