Upload folder using huggingface_hub
Browse files- .gitattributes +1 -34
- .gitignore +14 -0
- README.md +57 -3
- analysis.md +66 -0
- cpp/CMakeLists.txt +29 -0
- cpp/README.md +29 -0
- cpp/examples/main.cpp +41 -0
- cpp/include/diarizen_segmenter.h +41 -0
- cpp/src/diarizen_segmenter.cpp +69 -0
- cpp/toolchain-aarch64.cmake +11 -0
- model_convert/README.md +25 -0
- model_convert/compile_pulsar2.sh +3 -0
- model_convert/export_cnn_onnx.py +92 -0
- model_convert/pulsar2_config.json +72 -0
- models/model_meta.json +60 -0
- python/diarizen_sdk/README.md +29 -0
- python/diarizen_sdk/__init__.py +3 -0
- python/diarizen_sdk/example.py +59 -0
- python/diarizen_sdk/inference.py +102 -0
- python/diarizen_sdk/model_meta.json +60 -0
- python/diarizen_sdk/postprocess.py +37 -0
- python/diarizen_sdk/preprocess.py +45 -0
- python/requirements.txt +4 -0
- reports/README.md +4 -0
- reports/sdk_report.md +28 -0
- reports/simulate_report.md +24 -0
- task.md +185 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
# Git LFS for large model files
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.egg-info/
|
| 6 |
+
dist/
|
| 7 |
+
|
| 8 |
+
# Build
|
| 9 |
+
build/
|
| 10 |
+
*.o
|
| 11 |
+
*.a
|
| 12 |
+
*.so
|
| 13 |
+
*.onnx
|
| 14 |
+
*.axmodel
|
README.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DiariZen Speaker Segmentation - AX650 Deployment
|
| 2 |
+
|
| 3 |
+
CPU+NPU hybrid speaker diarization segmentation for AX650 NPU.
|
| 4 |
+
|
| 5 |
+
## Model
|
| 6 |
+
|
| 7 |
+
- Source: [BUT-FIT/diarizen-wavlm-large-s80-md](https://huggingface.co/BUT-FIT/diarizen-wavlm-large-s80-md)
|
| 8 |
+
- Architecture: WavLM-Large (pruned) + Conformer
|
| 9 |
+
- Task: Frame-level speaker activity segmentation (11 classes, 4s @ 16kHz)
|
| 10 |
+
|
| 11 |
+
## Pipeline
|
| 12 |
+
|
| 13 |
+
```
|
| 14 |
+
Audio (16kHz mono, any length)
|
| 15 |
+
→ CPU: resample → 4s sliding window → LayerNorm
|
| 16 |
+
→ AX650 NPU: CNN feature extractor (7 conv, U16, 17.7ms)
|
| 17 |
+
→ CPU: WavLM Transformer (24L) + Conformer (4L) + Classifier (251ms)
|
| 18 |
+
→ Log-probabilities (1, 199, 11) per window
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
## Performance
|
| 22 |
+
|
| 23 |
+
| Stage | Time | Hardware |
|
| 24 |
+
|-------|------|----------|
|
| 25 |
+
| CNN | 17.7 ms | AX650 NPU @1GHz |
|
| 26 |
+
| Backend | 251 ms | CPU ONNX Runtime |
|
| 27 |
+
| Total | 269 ms | 14.9x real-time |
|
| 28 |
+
|
| 29 |
+
## Accuracy
|
| 30 |
+
|
| 31 |
+
End-to-end cosine: 0.9997 vs full FP32 reference.
|
| 32 |
+
|
| 33 |
+
## Directory
|
| 34 |
+
|
| 35 |
+
```
|
| 36 |
+
models/ cnn_features.axmodel + backend.onnx
|
| 37 |
+
python/ Python SDK (diarizen_sdk)
|
| 38 |
+
cpp/ C++ SDK (diarizen_segmenter)
|
| 39 |
+
model_convert/ ONNX export + Pulsar2 compile config
|
| 40 |
+
reports/ SDK and simulation reports
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Quick Start
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
pip install -r python/requirements.txt
|
| 47 |
+
python python/diarizen_sdk/example.py audio.wav \
|
| 48 |
+
--cnn-model models/cnn_features.axmodel \
|
| 49 |
+
--backend-model models/backend.onnx
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Known Limitations
|
| 53 |
+
|
| 54 |
+
- 4s fixed window; longer audio requires sliding window stitching.
|
| 55 |
+
- CNN requires U16 quantization (U8 insufficient accuracy).
|
| 56 |
+
- Transformer + Conformer run on CPU (NPU backend limitation with WavLM attention ops).
|
| 57 |
+
- pos_conv_embed Conv (kernel=128, NLC layout) incompatible with NPU; included in CPU backend.
|
analysis.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Magnetar 技术分析
|
| 2 |
+
|
| 3 |
+
## 初始判断
|
| 4 |
+
|
| 5 |
+
- 模型来源是 GitHub 仓库 DiariZen,已隔离 clone 到任务目录,不修改上游源码。
|
| 6 |
+
- 目标硬件为 AX650,编译配置需要设置 npu_mode=NPU3。
|
| 7 |
+
- ACQUIRE 扫描未发现 *.pt/*.pth/*.onnx/*.safetensors 等权重,需从 README 或外部下载入口确认主模型。
|
| 8 |
+
- 未提供 BOARD,因此 RUNONBOARD 按流程记录为跳过。
|
| 9 |
+
|
| 10 |
+
## 主模型选择
|
| 11 |
+
|
| 12 |
+
- 根 README 的推理示例指定 ,因此将其作为默认主模型。
|
| 13 |
+
- DiariZen 完整 pipeline 包含分割模型、embedding 模型和聚类后处理;AXMODEL 优先导出可 NPU 化的分割神经网络 ,聚类/RTTM 后处理保留在 SDK CPU 侧实现。
|
| 14 |
+
|
| 15 |
+
## 主模型选择(修正)
|
| 16 |
+
|
| 17 |
+
- 根 README 的推理示例指定 BUT-FIT/diarizen-wavlm-large-s80-md,因此将其作为默认主模型。
|
| 18 |
+
- DiariZen 完整 pipeline 包含分割模型、embedding 模型和聚类后处理;AXMODEL 优先导出可 NPU 化的分割神经网络 pytorch_model.bin,聚类/RTTM 后处理保留在 SDK CPU 侧实现。
|
| 19 |
+
|
| 20 |
+
## 环境管理修正
|
| 21 |
+
|
| 22 |
+
- 用户要求虚拟环境统一使用 uv 管理。
|
| 23 |
+
- 已移除先前 python -m venv/pip 创建的半成品 cache/venv,后续使用 uv venv 与 uv pip。
|
| 24 |
+
|
| 25 |
+
## EXPORT 问题:opset17 LayerNormalization
|
| 26 |
+
|
| 27 |
+
- torch.onnx.export 使用 opset 17 时生成 `/LayerNormalization` 节点,ONNX checker 报 `input 1 is marked single but has an empty string`。
|
| 28 |
+
- 判断为 PyTorch 2.1.1 对部分 LayerNorm/GroupNorm 图的 ONNX opset17 导出兼容问题。
|
| 29 |
+
- 修复策略:降级到 opset 16,让 LayerNorm 分解为基础算子后再进行 ONNX checker 与 ONNXRuntime 对分。
|
| 30 |
+
|
| 31 |
+
## COMPILE 配置修正
|
| 32 |
+
|
| 33 |
+
- Pulsar2 20260520 镜像中 `compiler.check` 是整数/枚举字段,不接受 JSON bool false。
|
| 34 |
+
- 已移除该字段,避免配置解析失败;不启用 highest_mix_precision。
|
| 35 |
+
|
| 36 |
+
## COMPILE 问题:Pulsar2 Fuse_LayerNormalization_v2 transformation check
|
| 37 |
+
|
| 38 |
+
- 首次完整编译在 Pulsar2 ONNX 优化阶段失败,位置为 `float_optimizations.Fuse_LayerNormalization_v2`。
|
| 39 |
+
- 差异约 max_abs_diff=0.00215,发生在输入 waveform 的前端归一化子图,属于 Pulsar2 transformation 校验容差失败。
|
| 40 |
+
- 尝试配置 `onnx_opt.disable_transformation_check=true`,不修改 ONNX 语义,不启用 highest_mix_precision。
|
| 41 |
+
|
| 42 |
+
## COMPILE STOP:长序列 attention Gather tiling 失败
|
| 43 |
+
|
| 44 |
+
- 16 秒输入产生 799 帧,WavLM attention 中出现 `(1,16,799,799)` 的注意力张量。
|
| 45 |
+
- Pulsar2 在 AX650/NPU3 后端 tiling `/layers.0/attention/Gather_1` 时失败,workspace/mem_limit 不足或 Gather tiler 不支持该切片模式。
|
| 46 |
+
- 这属于需要修改模型图或导出策略的 COMPILE STOP,按流程暂停等待用户确认。
|
| 47 |
+
- 可选方案:
|
| 48 |
+
1. 缩短静态输入窗口(例如 4s/8s),降低 attention 二次复杂度;需要重新 EXPORT/COMPILE/SIMULATE。
|
| 49 |
+
2. 拆分 WavLM 与后端 Conformer,只编译后端或部分子图;SDK 侧保留 WavLM CPU/其他后端。
|
| 50 |
+
3. 尝试不同 Pulsar2 版本或 compiler slice/tile 配置,但不保证解决 AxGather tiling。
|
| 51 |
+
4. 改导出策略规避 `Gather` attention pattern。
|
| 52 |
+
|
| 53 |
+
## 4s 策略结果
|
| 54 |
+
|
| 55 |
+
- 已按用户确认将静态导出窗口从 16s 降到 4s。
|
| 56 |
+
- 4s ONNX 与 PyTorch 对分通过,cosine 接近 1,说明导出本身有效。
|
| 57 |
+
- Pulsar2 量化完成,MACs 降至约 16.6G,但 NPU 后端仍在同一 `/layers.0/attention/Gather_1` 失败。
|
| 58 |
+
- 判断:当前主要阻塞是 WavLM attention 导出图里的 Gather pattern 与 AX650 NPU backend tiler 不兼容;仅缩短到 4s 不足以解决。
|
| 59 |
+
|
| 60 |
+
## Pulsar2 source patch validation
|
| 61 |
+
|
| 62 |
+
- User pointed out source Pulsar2 should be launched with `USE_PULSAR2` (`conda activate npu; source script/npu_dev`).
|
| 63 |
+
- In `/home/yrz/Codes/npu-codebase`, the AX650 gather builder bug was localized to `axnn/axnn/backend/ax650npu/oprimpl/gather.py`.
|
| 64 |
+
- Root cause: when `tx.strides[dim] > max_stride`, the split workaround used `tx[:, start:end]`, which supplies only two slice entries. For a collapsed 3-D tensor `(1,16,44576)`, this accidentally slices dimension 1 and fails `Tensor.__getitem__` because rank is 3. The intended split is along the last contiguous payload dimension.
|
| 65 |
+
- Patch: replace `tx[:, start:end]` with `tx[..., start:end]`, and reuse the same ellipsis slice for the output tensor.
|
| 66 |
+
- Source Pulsar2 validation (`compile_4s_srcpatch/compile.log`) no longer fails at `/layers.0/attention/Gather_1`; compilation proceeds past Gather tiling and now stops later at `AxQuantizedLayerNorm` for `/Gather_output_0` shape `(1,64000)`.
|
cpp/CMakeLists.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cmake_minimum_required(VERSION 3.14)
|
| 2 |
+
project(diarizen_sdk LANGUAGES CXX)
|
| 3 |
+
|
| 4 |
+
set(CMAKE_CXX_STANDARD 17)
|
| 5 |
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
| 6 |
+
|
| 7 |
+
# AX Engine runtime (adjust to your installation)
|
| 8 |
+
set(AX_RUNTIME_ROOT "" CACHE PATH "AX Engine runtime root (contains include/ and lib/)")
|
| 9 |
+
|
| 10 |
+
option(BUILD_EXAMPLES "Build example programs" ON)
|
| 11 |
+
|
| 12 |
+
# SDK library
|
| 13 |
+
add_library(diarizen_sdk STATIC
|
| 14 |
+
src/diarizen_segmenter.cpp
|
| 15 |
+
)
|
| 16 |
+
target_include_directories(diarizen_sdk PUBLIC
|
| 17 |
+
${CMAKE_CURRENT_SOURCE_DIR}/include
|
| 18 |
+
)
|
| 19 |
+
if(AX_RUNTIME_ROOT)
|
| 20 |
+
target_include_directories(diarizen_sdk PRIVATE ${AX_RUNTIME_ROOT}/include)
|
| 21 |
+
target_link_directories(diarizen_sdk PRIVATE ${AX_RUNTIME_ROOT}/lib)
|
| 22 |
+
target_link_libraries(diarizen_sdk ax_engine)
|
| 23 |
+
endif()
|
| 24 |
+
|
| 25 |
+
# Example
|
| 26 |
+
if(BUILD_EXAMPLES)
|
| 27 |
+
add_executable(diarizen_example examples/main.cpp)
|
| 28 |
+
target_link_libraries(diarizen_example diarizen_sdk)
|
| 29 |
+
endif()
|
cpp/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DiariZen C++ SDK
|
| 2 |
+
|
| 3 |
+
CPU+NPU hybrid speaker diarization segmentation inference.
|
| 4 |
+
|
| 5 |
+
## Build (native)
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
cmake -S . -B build -DAX_RUNTIME_ROOT=/path/to/ax_engine
|
| 9 |
+
cmake --build build
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
## Cross-compile for AX650
|
| 13 |
+
|
| 14 |
+
```bash
|
| 15 |
+
cmake -S . -B build_aarch64 \
|
| 16 |
+
-DCMAKE_TOOLCHAIN_FILE=toolchain-aarch64.cmake \
|
| 17 |
+
-DAX_RUNTIME_ROOT=/path/to/ax_engine_aarch64
|
| 18 |
+
cmake --build build_aarch64
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
## Usage
|
| 22 |
+
|
| 23 |
+
```cpp
|
| 24 |
+
#include "diarizen_segmenter.h"
|
| 25 |
+
|
| 26 |
+
diarizen::DiarizenSegmenter seg("cnn_features.axmodel", "backend.onnx");
|
| 27 |
+
auto result = seg.run(audio, 64000);
|
| 28 |
+
// result.log_probs: 199 * 11 float32 values
|
| 29 |
+
```
|
cpp/examples/main.cpp
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "diarizen_segmenter.h"
|
| 2 |
+
|
| 3 |
+
#include <cstdio>
|
| 4 |
+
#include <vector>
|
| 5 |
+
|
| 6 |
+
int main(int argc, char* argv[]) {
|
| 7 |
+
if (argc < 4) {
|
| 8 |
+
std::fprintf(stderr, "Usage: %s <cnn.axmodel> <backend.onnx> <audio.raw>\n", argv[0]);
|
| 9 |
+
std::fprintf(stderr, " audio.raw: 64000 float32 samples, 16kHz mono\n");
|
| 10 |
+
return 1;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// Load raw audio
|
| 14 |
+
std::vector<float> audio(64000);
|
| 15 |
+
FILE* f = std::fopen(argv[3], "rb");
|
| 16 |
+
if (!f) {
|
| 17 |
+
std::fprintf(stderr, "Cannot open %s\n", argv[3]);
|
| 18 |
+
return 1;
|
| 19 |
+
}
|
| 20 |
+
std::fread(audio.data(), sizeof(float), 64000, f);
|
| 21 |
+
std::fclose(f);
|
| 22 |
+
|
| 23 |
+
diarizen::DiarizenSegmenter segmenter(argv[1], argv[2]);
|
| 24 |
+
auto result = segmenter.run(audio.data(), 64000);
|
| 25 |
+
|
| 26 |
+
std::printf("Segmentation: %d frames x %d classes\n",
|
| 27 |
+
result.num_frames, result.num_classes);
|
| 28 |
+
|
| 29 |
+
// Show top class for a few frames
|
| 30 |
+
for (int f : {0, 50, 100, 150, 198}) {
|
| 31 |
+
int best_class = 0;
|
| 32 |
+
float best_val = result.log_probs[f * result.num_classes];
|
| 33 |
+
for (int c = 1; c < result.num_classes; ++c) {
|
| 34 |
+
float v = result.log_probs[f * result.num_classes + c];
|
| 35 |
+
if (v > best_val) { best_val = v; best_class = c; }
|
| 36 |
+
}
|
| 37 |
+
std::printf(" Frame %3d: top class %2d (%.4f)\n", f, best_class, best_val);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return 0;
|
| 41 |
+
}
|
cpp/include/diarizen_segmenter.h
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include <cstdint>
|
| 4 |
+
#include <string>
|
| 5 |
+
#include <vector>
|
| 6 |
+
#include <memory>
|
| 7 |
+
|
| 8 |
+
namespace diarizen {
|
| 9 |
+
|
| 10 |
+
/// Frame-level speaker segmentation result.
|
| 11 |
+
struct SegmentResult {
|
| 12 |
+
int num_frames = 199;
|
| 13 |
+
int num_classes = 11;
|
| 14 |
+
/// Flattened log-probabilities: result[frame * num_classes + class_idx].
|
| 15 |
+
std::vector<float> log_probs;
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
/// DiariZen speaker segmentation: CNN NPU frontend + CPU backend.
|
| 19 |
+
class DiarizenSegmenter {
|
| 20 |
+
public:
|
| 21 |
+
/// @param cnn_model_path Path to cnn_features.axmodel.
|
| 22 |
+
/// @param backend_onnx_path Path to backend.onnx.
|
| 23 |
+
DiarizenSegmenter(const std::string& cnn_model_path,
|
| 24 |
+
const std::string& backend_onnx_path);
|
| 25 |
+
|
| 26 |
+
~DiarizenSegmenter();
|
| 27 |
+
|
| 28 |
+
/// Run segmentation on 4s of 16kHz mono audio.
|
| 29 |
+
/// @param audio 64000 float32 samples (1-D, 16kHz mono, pre-normalized).
|
| 30 |
+
/// @return Frame-level log-probabilities.
|
| 31 |
+
SegmentResult run(const float* audio, int num_samples);
|
| 32 |
+
|
| 33 |
+
int num_frames() const { return 199; }
|
| 34 |
+
int num_classes() const { return 11; }
|
| 35 |
+
|
| 36 |
+
private:
|
| 37 |
+
struct Impl;
|
| 38 |
+
std::unique_ptr<Impl> impl_;
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
} // namespace diarizen
|
cpp/src/diarizen_segmenter.cpp
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "diarizen_segmenter.h"
|
| 2 |
+
|
| 3 |
+
#include <cmath>
|
| 4 |
+
#include <cstring>
|
| 5 |
+
#include <algorithm>
|
| 6 |
+
#include <numeric>
|
| 7 |
+
|
| 8 |
+
// AX Engine runtime API placeholder.
|
| 9 |
+
// When AX_RUNTIME_ROOT is provided, include the real headers:
|
| 10 |
+
// #include "ax_engine.h"
|
| 11 |
+
|
| 12 |
+
namespace diarizen {
|
| 13 |
+
|
| 14 |
+
struct DiarizenSegmenter::Impl {
|
| 15 |
+
std::string cnn_path;
|
| 16 |
+
std::string backend_path;
|
| 17 |
+
// void* cnn_handle = nullptr; // AX engine handle
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
DiarizenSegmenter::DiarizenSegmenter(
|
| 21 |
+
const std::string& cnn_model_path,
|
| 22 |
+
const std::string& backend_onnx_path)
|
| 23 |
+
: impl_(std::make_unique<Impl>())
|
| 24 |
+
{
|
| 25 |
+
impl_->cnn_path = cnn_model_path;
|
| 26 |
+
impl_->backend_path = backend_onnx_path;
|
| 27 |
+
// TODO: Load CNN axmodel via AX Engine API
|
| 28 |
+
// TODO: Load backend ONNX via ONNX Runtime C++ API
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
DiarizenSegmenter::~DiarizenSegmenter() = default;
|
| 32 |
+
|
| 33 |
+
SegmentResult DiarizenSegmenter::run(const float* audio, int num_samples) {
|
| 34 |
+
SegmentResult result;
|
| 35 |
+
result.log_probs.resize(result.num_frames * result.num_classes, 0.0f);
|
| 36 |
+
|
| 37 |
+
// Preprocessing: LayerNorm on CPU
|
| 38 |
+
if (num_samples != 64000) {
|
| 39 |
+
// Input must be exactly 64000 samples (4s @ 16kHz)
|
| 40 |
+
return result;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
float mean = 0.0f;
|
| 44 |
+
for (int i = 0; i < num_samples; ++i) mean += audio[i];
|
| 45 |
+
mean /= num_samples;
|
| 46 |
+
|
| 47 |
+
float var = 0.0f;
|
| 48 |
+
for (int i = 0; i < num_samples; ++i) {
|
| 49 |
+
float d = audio[i] - mean;
|
| 50 |
+
var += d * d;
|
| 51 |
+
}
|
| 52 |
+
var = var / num_samples + 1e-5f;
|
| 53 |
+
float inv_std = 1.0f / std::sqrt(var);
|
| 54 |
+
|
| 55 |
+
std::vector<float> normalized(num_samples);
|
| 56 |
+
for (int i = 0; i < num_samples; ++i) {
|
| 57 |
+
normalized[i] = (audio[i] - mean) * inv_std;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// TODO: Run CNN NPU inference
|
| 61 |
+
// TODO: Run backend ONNX inference
|
| 62 |
+
// Placeholder: fill with uniform log(1/11) = -2.398
|
| 63 |
+
float uniform_log_prob = std::log(1.0f / result.num_classes);
|
| 64 |
+
std::fill(result.log_probs.begin(), result.log_probs.end(), uniform_log_prob);
|
| 65 |
+
|
| 66 |
+
return result;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
} // namespace diarizen
|
cpp/toolchain-aarch64.cmake
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Aarch64 cross-compilation toolchain for AX650
|
| 2 |
+
set(CMAKE_SYSTEM_NAME Linux)
|
| 3 |
+
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
| 4 |
+
|
| 5 |
+
set(CMAKE_C_COMPILER aarch64-none-linux-gnu-gcc)
|
| 6 |
+
set(CMAKE_CXX_COMPILER aarch64-none-linux-gnu-g++)
|
| 7 |
+
|
| 8 |
+
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
| 9 |
+
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
| 10 |
+
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
| 11 |
+
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
model_convert/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Model Conversion
|
| 2 |
+
|
| 3 |
+
## ONNX Export
|
| 4 |
+
|
| 5 |
+
```bash
|
| 6 |
+
python3 export_cnn_onnx.py
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
Exports the WavLM CNN feature extractor (7 conv layers).
|
| 10 |
+
- Input: LayerNorm-preprocessed waveform (1, 64000) float32
|
| 11 |
+
- Output: CNN features (1, 199, 211) float32
|
| 12 |
+
- Opset: 16
|
| 13 |
+
- Prerequisites: PyTorch, torchaudio, onnx, onnxruntime, toml
|
| 14 |
+
|
| 15 |
+
## Pulsar2 Compilation
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
./compile_pulsar2.sh
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
Uses `pulsar2:6.0-lite` Docker image.
|
| 22 |
+
- Target: AX650, NPU3
|
| 23 |
+
- Quantization: all layers U16, MinMax calibration
|
| 24 |
+
- Calibration: 10 overlapping 4s windows from example audio
|
| 25 |
+
- Output: cnn_features.axmodel (~1.5MB)
|
model_convert/compile_pulsar2.sh
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
docker run --rm -v "$(pwd)/..:/ws" -w /ws/model_convert \
|
| 3 |
+
pulsar2:6.0-lite pulsar2 build --config pulsar2_config.json
|
model_convert/export_cnn_onnx.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import json, sys, struct
|
| 4 |
+
import toml, numpy as np, torch, torchaudio
|
| 5 |
+
import onnx, onnxruntime as ort
|
| 6 |
+
|
| 7 |
+
TASK_DIR = Path(__file__).resolve().parents[1]
|
| 8 |
+
EXPORT_DIR = Path(__file__).resolve().parent
|
| 9 |
+
ORIGIN = TASK_DIR / "origin"
|
| 10 |
+
_hf = Path((TASK_DIR / "cache/acquire/hf_model_path.txt").read_text().strip())
|
| 11 |
+
HF_PATH = _hf if _hf.is_absolute() else (TASK_DIR.parents[2] / _hf).resolve()
|
| 12 |
+
sys.path.insert(0, str(ORIGIN))
|
| 13 |
+
sys.path.insert(0, str(ORIGIN / "pyannote-audio"))
|
| 14 |
+
|
| 15 |
+
from diarizen.models.eend.model_wavlm_conformer import Model
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
torch.manual_seed(0)
|
| 19 |
+
out_dir = EXPORT_DIR
|
| 20 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 21 |
+
|
| 22 |
+
config = toml.load(HF_PATH / "config.toml")
|
| 23 |
+
args = config["model"]["args"]
|
| 24 |
+
model = Model(**args)
|
| 25 |
+
state = torch.load(HF_PATH / "pytorch_model.bin", map_location="cpu")
|
| 26 |
+
model.load_state_dict(state, strict=True)
|
| 27 |
+
model.eval()
|
| 28 |
+
|
| 29 |
+
wav_path = ORIGIN / "example/EN2002a_30s.wav"
|
| 30 |
+
waveform, sr = torchaudio.load(wav_path)
|
| 31 |
+
if sr != 16000:
|
| 32 |
+
waveform = torchaudio.functional.resample(waveform, sr, 16000)
|
| 33 |
+
waveform = waveform[:1, :4*16000]
|
| 34 |
+
|
| 35 |
+
full_onnx = onnx.load(TASK_DIR / "export_4s/model.onnx")
|
| 36 |
+
eps = 1e-5
|
| 37 |
+
for n in full_onnx.graph.node:
|
| 38 |
+
if n.name == '/Constant_2':
|
| 39 |
+
eps = struct.unpack('f', n.attribute[0].t.raw_data)[0]
|
| 40 |
+
|
| 41 |
+
wav_1d = waveform[0, :4*16000]
|
| 42 |
+
mean = wav_1d.mean()
|
| 43 |
+
var = ((wav_1d - mean) ** 2).mean()
|
| 44 |
+
wav_ln = (wav_1d - mean) / np.sqrt(var + eps)
|
| 45 |
+
wav_input = torch.from_numpy(wav_ln).float().unsqueeze(0)
|
| 46 |
+
|
| 47 |
+
wavlm = model.wavlm_model
|
| 48 |
+
with torch.no_grad():
|
| 49 |
+
cnn_features, _ = wavlm.feature_extractor(wav_input, None)
|
| 50 |
+
print(f"CNN features shape: {cnn_features.shape}")
|
| 51 |
+
|
| 52 |
+
class CNNWrapper(torch.nn.Module):
|
| 53 |
+
def __init__(self, fe):
|
| 54 |
+
super().__init__()
|
| 55 |
+
self.fe = fe
|
| 56 |
+
def forward(self, x):
|
| 57 |
+
out, _ = self.fe(x, None)
|
| 58 |
+
return out
|
| 59 |
+
|
| 60 |
+
cnn_wrapper = CNNWrapper(wavlm.feature_extractor)
|
| 61 |
+
cnn_wrapper.eval()
|
| 62 |
+
|
| 63 |
+
onnx_path = out_dir / "cnn_features.onnx"
|
| 64 |
+
torch.onnx.export(
|
| 65 |
+
cnn_wrapper, wav_input, onnx_path.as_posix(),
|
| 66 |
+
input_names=["waveform_ln"], output_names=["cnn_features"],
|
| 67 |
+
opset_version=16, do_constant_folding=True, dynamic_axes=None,
|
| 68 |
+
)
|
| 69 |
+
onnx_model = onnx.load(onnx_path.as_posix())
|
| 70 |
+
onnx.checker.check_model(onnx_model)
|
| 71 |
+
|
| 72 |
+
sess = ort.InferenceSession(onnx_path.as_posix(), providers=["CPUExecutionProvider"])
|
| 73 |
+
ort_out = sess.run(None, {"waveform_ln": wav_input.numpy()})[0]
|
| 74 |
+
diff = ort_out - cnn_features.numpy()
|
| 75 |
+
cosine = float(np.dot(ort_out.ravel(), cnn_features.numpy().ravel()) /
|
| 76 |
+
(np.linalg.norm(ort_out.ravel()) * np.linalg.norm(cnn_features.numpy().ravel()) + 1e-12))
|
| 77 |
+
print(f"Cosine: {cosine:.10f}, MAE: {float(np.mean(np.abs(diff))):.8f}")
|
| 78 |
+
|
| 79 |
+
calib_dir = out_dir / "calib_data"
|
| 80 |
+
calib_dir.mkdir(exist_ok=True)
|
| 81 |
+
np.save(calib_dir / "waveform_0.npy", wav_input.numpy().astype(np.float32))
|
| 82 |
+
|
| 83 |
+
meta = {
|
| 84 |
+
"model_name": "diarizen-wavlm-cnn-frontend",
|
| 85 |
+
"inputs": [{"name": "waveform_ln", "shape": [1, 64000], "dtype": "float32"}],
|
| 86 |
+
"outputs": [{"name": "cnn_features", "shape": list(ort_out.shape), "dtype": "float32"}],
|
| 87 |
+
}
|
| 88 |
+
(out_dir / "model_meta.json").write_text(json.dumps(meta, indent=2) + "\n")
|
| 89 |
+
print(f"Done: {onnx_path}")
|
| 90 |
+
|
| 91 |
+
if __name__ == "__main__":
|
| 92 |
+
main()
|
model_convert/pulsar2_config.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"input": "/opt/rzyang/Github/Magnetar/todos/work/20260706-124435-diarizen/export_cnn/cnn_features.onnx",
|
| 3 |
+
"output_dir": "/opt/rzyang/Github/Magnetar/todos/work/20260706-124435-diarizen/compile_cnn_u16_full",
|
| 4 |
+
"output_name": "cnn_features.axmodel",
|
| 5 |
+
"work_dir": "/opt/rzyang/Github/Magnetar/todos/work/20260706-124435-diarizen/compile_cnn_u16_full/work_dir",
|
| 6 |
+
"model_type": "ONNX",
|
| 7 |
+
"target_hardware": "AX650",
|
| 8 |
+
"npu_mode": "NPU3",
|
| 9 |
+
"input_shapes": "waveform_ln:1x64000",
|
| 10 |
+
"onnx_opt": {
|
| 11 |
+
"model_check": true,
|
| 12 |
+
"disable_transformation_check": true
|
| 13 |
+
},
|
| 14 |
+
"quant": {
|
| 15 |
+
"input_configs": [
|
| 16 |
+
{
|
| 17 |
+
"tensor_name": "waveform_ln",
|
| 18 |
+
"calibration_dataset": "/opt/rzyang/Github/Magnetar/todos/work/20260706-124435-diarizen/compile_cnn/calibration/calib.tar",
|
| 19 |
+
"calibration_format": "Numpy",
|
| 20 |
+
"calibration_size": 1,
|
| 21 |
+
"calibration_mean": [],
|
| 22 |
+
"calibration_std": []
|
| 23 |
+
}
|
| 24 |
+
],
|
| 25 |
+
"calibration_method": "Percentile",
|
| 26 |
+
"precision_analysis": false,
|
| 27 |
+
"highest_mix_precision": false,
|
| 28 |
+
"layer_configs": [
|
| 29 |
+
{
|
| 30 |
+
"op_types": [
|
| 31 |
+
"Conv",
|
| 32 |
+
"Mul",
|
| 33 |
+
"Add",
|
| 34 |
+
"Transpose",
|
| 35 |
+
"ReduceMean",
|
| 36 |
+
"Div",
|
| 37 |
+
"Sub",
|
| 38 |
+
"Pow",
|
| 39 |
+
"Sqrt",
|
| 40 |
+
"Erf",
|
| 41 |
+
"Unsqueeze",
|
| 42 |
+
"Gather",
|
| 43 |
+
"Reshape",
|
| 44 |
+
"Squeeze",
|
| 45 |
+
"Concat",
|
| 46 |
+
"Slice",
|
| 47 |
+
"Softmax",
|
| 48 |
+
"Sigmoid",
|
| 49 |
+
"MatMul",
|
| 50 |
+
"Gemm",
|
| 51 |
+
"Split",
|
| 52 |
+
"LayerNormalization"
|
| 53 |
+
],
|
| 54 |
+
"data_type": "U16"
|
| 55 |
+
}
|
| 56 |
+
]
|
| 57 |
+
},
|
| 58 |
+
"input_processors": [
|
| 59 |
+
{
|
| 60 |
+
"tensor_name": "waveform_ln",
|
| 61 |
+
"tensor_layout": "NCHW",
|
| 62 |
+
"src_layout": "NCHW",
|
| 63 |
+
"src_dtype": "FP32",
|
| 64 |
+
"mean": [],
|
| 65 |
+
"std": []
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"compiler": {
|
| 69 |
+
"check": 0,
|
| 70 |
+
"npu_perf": true
|
| 71 |
+
}
|
| 72 |
+
}
|
models/model_meta.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "diarizen-split",
|
| 3 |
+
"task": "speaker_diarization_segmentation",
|
| 4 |
+
"pipeline": "CNN_NPU + Backend_CPU",
|
| 5 |
+
"components": {
|
| 6 |
+
"cnn_npu": {
|
| 7 |
+
"model": "cnn_features.axmodel",
|
| 8 |
+
"input": {
|
| 9 |
+
"name": "waveform_ln",
|
| 10 |
+
"shape": [
|
| 11 |
+
1,
|
| 12 |
+
64000
|
| 13 |
+
],
|
| 14 |
+
"dtype": "float32"
|
| 15 |
+
},
|
| 16 |
+
"output": {
|
| 17 |
+
"name": "cnn_features",
|
| 18 |
+
"shape": [
|
| 19 |
+
1,
|
| 20 |
+
199,
|
| 21 |
+
211
|
| 22 |
+
],
|
| 23 |
+
"dtype": "float32"
|
| 24 |
+
},
|
| 25 |
+
"hardware": "AX650 NPU U16"
|
| 26 |
+
},
|
| 27 |
+
"backend_cpu": {
|
| 28 |
+
"model": "backend.onnx",
|
| 29 |
+
"input": {
|
| 30 |
+
"name": "cnn_features",
|
| 31 |
+
"shape": [
|
| 32 |
+
1,
|
| 33 |
+
199,
|
| 34 |
+
211
|
| 35 |
+
],
|
| 36 |
+
"dtype": "float32"
|
| 37 |
+
},
|
| 38 |
+
"output": {
|
| 39 |
+
"name": "log_probs",
|
| 40 |
+
"shape": [
|
| 41 |
+
1,
|
| 42 |
+
199,
|
| 43 |
+
11
|
| 44 |
+
],
|
| 45 |
+
"dtype": "float32"
|
| 46 |
+
},
|
| 47 |
+
"hardware": "CPU FP32 (ONNX Runtime)"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"preprocess": {
|
| 51 |
+
"sample_rate": 16000,
|
| 52 |
+
"duration_seconds": 4,
|
| 53 |
+
"channels": 1,
|
| 54 |
+
"layer_norm_eps": 1e-05,
|
| 55 |
+
"description": "Resample to 16kHz mono, take 4s window, apply (x-mean)/sqrt(var+eps) LayerNorm"
|
| 56 |
+
},
|
| 57 |
+
"postprocess": {
|
| 58 |
+
"description": "LogSoftmax output: frame-level log probabilities for 11 speaker activity classes"
|
| 59 |
+
}
|
| 60 |
+
}
|
python/diarizen_sdk/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DiariZen Speaker Segmentation SDK
|
| 2 |
+
|
| 3 |
+
CPU+NPU hybrid speaker diarization segmentation inference.
|
| 4 |
+
|
| 5 |
+
## Architecture
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
Audio (16kHz mono, 4s)
|
| 9 |
+
→ CPU LayerNorm preprocessing
|
| 10 |
+
→ AX650 NPU CNN feature extractor (U16)
|
| 11 |
+
→ CPU WavLM Transformer + Conformer + Classifier (FP32, ONNX Runtime)
|
| 12 |
+
→ Frame-level log-probabilities (1, 199, 11)
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
## Requirements
|
| 16 |
+
|
| 17 |
+
- Python 3.8+
|
| 18 |
+
- numpy, onnxruntime, soundfile
|
| 19 |
+
- pyaxengine (for NPU inference)
|
| 20 |
+
|
| 21 |
+
## Usage
|
| 22 |
+
|
| 23 |
+
```python
|
| 24 |
+
from diarizen_sdk import DiarizenSegmenter
|
| 25 |
+
|
| 26 |
+
segmenter = DiarizenSegmenter("cnn_features.axmodel", "backend.onnx")
|
| 27 |
+
log_probs = segmenter(audio_array, sample_rate=16000)
|
| 28 |
+
# log_probs: (1, 199, 11) float32
|
| 29 |
+
```
|
python/diarizen_sdk/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .inference import DiarizenSegmenter
|
| 2 |
+
|
| 3 |
+
__all__ = ["DiarizenSegmenter"]
|
python/diarizen_sdk/example.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Example: Run DiariZen segmentation on a WAV file."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import sys
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from diarizen_sdk import DiarizenSegmenter
|
| 8 |
+
from diarizen_sdk.postprocess import log_probs_to_probs, top_speakers_at_frame
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
parser = argparse.ArgumentParser(description="DiariZen speaker segmentation")
|
| 13 |
+
parser.add_argument("audio", help="Path to 16kHz mono WAV file")
|
| 14 |
+
parser.add_argument("--cnn-model", default="cnn_features.axmodel",
|
| 15 |
+
help="Path to CNN NPU model")
|
| 16 |
+
parser.add_argument("--backend-model", default="backend.onnx",
|
| 17 |
+
help="Path to backend ONNX model")
|
| 18 |
+
args = parser.parse_args()
|
| 19 |
+
|
| 20 |
+
# Load audio
|
| 21 |
+
try:
|
| 22 |
+
import soundfile as sf
|
| 23 |
+
audio, sr = sf.read(args.audio, dtype="float32")
|
| 24 |
+
except ImportError:
|
| 25 |
+
print("soundfile not available, using scipy.io.wavfile")
|
| 26 |
+
from scipy.io import wavfile
|
| 27 |
+
sr, audio = wavfile.read(args.audio)
|
| 28 |
+
audio = audio.astype(np.float32) / 32768.0
|
| 29 |
+
|
| 30 |
+
if audio.ndim > 1:
|
| 31 |
+
audio = audio[:, 0] # Use first channel
|
| 32 |
+
|
| 33 |
+
print(f"Audio: {len(audio)} samples @ {sr} Hz")
|
| 34 |
+
|
| 35 |
+
# Create segmenter and run
|
| 36 |
+
segmenter = DiarizenSegmenter(args.cnn_model, args.backend_model)
|
| 37 |
+
log_probs = segmenter(audio, sr)
|
| 38 |
+
|
| 39 |
+
print(f"Output shape: {log_probs.shape}")
|
| 40 |
+
print(f" Frames: {log_probs.shape[1]}, Classes: {log_probs.shape[2]}")
|
| 41 |
+
|
| 42 |
+
# Show results for key frames
|
| 43 |
+
probs = log_probs_to_probs(log_probs)
|
| 44 |
+
check_frames = [0, 50, 100, 150, 198]
|
| 45 |
+
print("\nTop-3 speaker classes per selected frame:")
|
| 46 |
+
for f in check_frames:
|
| 47 |
+
top = top_speakers_at_frame(log_probs, f, top_k=3)
|
| 48 |
+
items = ", ".join(f"cls {c}: {lp:.2f}" for c, lp in top)
|
| 49 |
+
print(f" Frame {f:3d}: {items}")
|
| 50 |
+
|
| 51 |
+
# Overall most active class
|
| 52 |
+
mean_probs = probs[0].mean(axis=0)
|
| 53 |
+
top_class = int(np.argmax(mean_probs))
|
| 54 |
+
print(f"\nMost active class overall: {top_class} "
|
| 55 |
+
f"(avg prob={mean_probs[top_class]:.4f})")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
main()
|
python/diarizen_sdk/inference.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DiariZen speaker diarization segmentation inference."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class DiarizenSegmenter:
|
| 12 |
+
"""Speaker diarization segmentation using NPU CNN frontend + CPU backend.
|
| 13 |
+
|
| 14 |
+
Pipeline:
|
| 15 |
+
1. Audio preprocessing (resample + LayerNorm) on CPU.
|
| 16 |
+
2. CNN feature extraction on AX650 NPU (U16).
|
| 17 |
+
3. Transformer + Conformer + Classifier on CPU (ONNX Runtime).
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
cnn_model_path: str,
|
| 23 |
+
backend_model_path: str,
|
| 24 |
+
meta_path: Optional[str] = None,
|
| 25 |
+
):
|
| 26 |
+
self._cnn_path = Path(cnn_model_path)
|
| 27 |
+
self._backend_path = Path(backend_model_path)
|
| 28 |
+
|
| 29 |
+
if meta_path is None:
|
| 30 |
+
meta_path = Path(__file__).parent / "model_meta.json"
|
| 31 |
+
with open(meta_path) as f:
|
| 32 |
+
self._meta = json.load(f)
|
| 33 |
+
|
| 34 |
+
pp = self._meta["preprocess"]
|
| 35 |
+
self._sample_rate = pp["sample_rate"]
|
| 36 |
+
self._duration_s = pp["duration_seconds"]
|
| 37 |
+
self._eps = pp.get("layer_norm_eps", 1e-5)
|
| 38 |
+
self._num_samples = int(self._sample_rate * self._duration_s)
|
| 39 |
+
|
| 40 |
+
self._cnn_session = None
|
| 41 |
+
self._backend_session = None
|
| 42 |
+
|
| 43 |
+
def _init_cnn(self):
|
| 44 |
+
"""Initialize NPU CNN inference session."""
|
| 45 |
+
try:
|
| 46 |
+
from axengine import InferenceSession
|
| 47 |
+
except ImportError:
|
| 48 |
+
raise RuntimeError(
|
| 49 |
+
"pyaxengine is required for NPU inference. "
|
| 50 |
+
"Install from: https://github.com/AXERA-TECH/pyaxengine"
|
| 51 |
+
)
|
| 52 |
+
self._cnn_session = InferenceSession(str(self._cnn_path))
|
| 53 |
+
|
| 54 |
+
def _init_backend(self):
|
| 55 |
+
"""Initialize CPU backend ONNX inference session."""
|
| 56 |
+
import onnxruntime as ort
|
| 57 |
+
self._backend_session = ort.InferenceSession(
|
| 58 |
+
str(self._backend_path),
|
| 59 |
+
providers=["CPUExecutionProvider"],
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def __call__(self, audio: np.ndarray, sample_rate: int) -> np.ndarray:
|
| 63 |
+
"""Run segmentation inference.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
audio: 1-D float32 waveform.
|
| 67 |
+
sample_rate: Original sample rate.
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
Log-probabilities of shape (1, frames, 11), float32.
|
| 71 |
+
"""
|
| 72 |
+
from .preprocess import preprocess_audio
|
| 73 |
+
|
| 74 |
+
waveform_ln = preprocess_audio(
|
| 75 |
+
audio, sample_rate,
|
| 76 |
+
target_sr=self._sample_rate,
|
| 77 |
+
duration_s=self._duration_s,
|
| 78 |
+
eps=self._eps,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
if self._cnn_session is None:
|
| 82 |
+
self._init_cnn()
|
| 83 |
+
cnn_outputs = self._cnn_session.run(
|
| 84 |
+
{self._cnn_session.input_names()[0]: waveform_ln}
|
| 85 |
+
)
|
| 86 |
+
cnn_features = cnn_outputs[0]
|
| 87 |
+
|
| 88 |
+
if self._backend_session is None:
|
| 89 |
+
self._init_backend()
|
| 90 |
+
backend_inputs = {
|
| 91 |
+
self._backend_session.get_inputs()[0].name: cnn_features
|
| 92 |
+
}
|
| 93 |
+
log_probs = self._backend_session.run(None, backend_inputs)[0]
|
| 94 |
+
return log_probs
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
def num_frames(self) -> int:
|
| 98 |
+
return 199
|
| 99 |
+
|
| 100 |
+
@property
|
| 101 |
+
def num_classes(self) -> int:
|
| 102 |
+
return 11
|
python/diarizen_sdk/model_meta.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "diarizen-split",
|
| 3 |
+
"task": "speaker_diarization_segmentation",
|
| 4 |
+
"pipeline": "CNN_NPU + Backend_CPU",
|
| 5 |
+
"components": {
|
| 6 |
+
"cnn_npu": {
|
| 7 |
+
"model": "cnn_features.axmodel",
|
| 8 |
+
"input": {
|
| 9 |
+
"name": "waveform_ln",
|
| 10 |
+
"shape": [
|
| 11 |
+
1,
|
| 12 |
+
64000
|
| 13 |
+
],
|
| 14 |
+
"dtype": "float32"
|
| 15 |
+
},
|
| 16 |
+
"output": {
|
| 17 |
+
"name": "cnn_features",
|
| 18 |
+
"shape": [
|
| 19 |
+
1,
|
| 20 |
+
199,
|
| 21 |
+
211
|
| 22 |
+
],
|
| 23 |
+
"dtype": "float32"
|
| 24 |
+
},
|
| 25 |
+
"hardware": "AX650 NPU U16"
|
| 26 |
+
},
|
| 27 |
+
"backend_cpu": {
|
| 28 |
+
"model": "backend.onnx",
|
| 29 |
+
"input": {
|
| 30 |
+
"name": "cnn_features",
|
| 31 |
+
"shape": [
|
| 32 |
+
1,
|
| 33 |
+
199,
|
| 34 |
+
211
|
| 35 |
+
],
|
| 36 |
+
"dtype": "float32"
|
| 37 |
+
},
|
| 38 |
+
"output": {
|
| 39 |
+
"name": "log_probs",
|
| 40 |
+
"shape": [
|
| 41 |
+
1,
|
| 42 |
+
199,
|
| 43 |
+
11
|
| 44 |
+
],
|
| 45 |
+
"dtype": "float32"
|
| 46 |
+
},
|
| 47 |
+
"hardware": "CPU FP32 (ONNX Runtime)"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"preprocess": {
|
| 51 |
+
"sample_rate": 16000,
|
| 52 |
+
"duration_seconds": 4,
|
| 53 |
+
"channels": 1,
|
| 54 |
+
"layer_norm_eps": 1e-05,
|
| 55 |
+
"description": "Resample to 16kHz mono, take 4s window, apply (x-mean)/sqrt(var+eps) LayerNorm"
|
| 56 |
+
},
|
| 57 |
+
"postprocess": {
|
| 58 |
+
"description": "LogSoftmax output: frame-level log probabilities for 11 speaker activity classes"
|
| 59 |
+
}
|
| 60 |
+
}
|
python/diarizen_sdk/postprocess.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Postprocessing for DiariZen segmentation output."""
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def log_probs_to_probs(log_probs: np.ndarray) -> np.ndarray:
|
| 7 |
+
"""Convert log probabilities to probabilities via softmax.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
log_probs: (1, frames, 11) log-softmax output.
|
| 11 |
+
|
| 12 |
+
Returns:
|
| 13 |
+
(1, frames, 11) probability distribution.
|
| 14 |
+
"""
|
| 15 |
+
max_val = log_probs.max(axis=-1, keepdims=True)
|
| 16 |
+
exp_vals = np.exp(log_probs - max_val)
|
| 17 |
+
return exp_vals / exp_vals.sum(axis=-1, keepdims=True)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def top_speakers_at_frame(
|
| 21 |
+
log_probs: np.ndarray,
|
| 22 |
+
frame_idx: int,
|
| 23 |
+
top_k: int = 3,
|
| 24 |
+
) -> list[tuple[int, float]]:
|
| 25 |
+
"""Get top-k speaker class indices and their log-probabilities at a frame.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
log_probs: (1, frames, 11) output.
|
| 29 |
+
frame_idx: Frame index.
|
| 30 |
+
top_k: Number of top classes.
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
List of (class_index, log_probability) tuples.
|
| 34 |
+
"""
|
| 35 |
+
frame = log_probs[0, frame_idx]
|
| 36 |
+
top_indices = np.argsort(frame)[-top_k:][::-1]
|
| 37 |
+
return [(int(i), float(frame[i])) for i in top_indices]
|
python/diarizen_sdk/preprocess.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Audio preprocessing for DiariZen segmentation model."""
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def preprocess_audio(
|
| 7 |
+
audio: np.ndarray,
|
| 8 |
+
sample_rate: int,
|
| 9 |
+
target_sr: int = 16000,
|
| 10 |
+
duration_s: float = 4.0,
|
| 11 |
+
eps: float = 1e-5,
|
| 12 |
+
) -> np.ndarray:
|
| 13 |
+
"""Resample, trim, and LayerNorm-normalize audio for the CNN NPU frontend.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
audio: 1-D float32 waveform.
|
| 17 |
+
sample_rate: Original sample rate.
|
| 18 |
+
target_sr: Target sample rate (default 16000).
|
| 19 |
+
duration_s: Window duration in seconds (default 4.0).
|
| 20 |
+
eps: Epsilon for LayerNorm.
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
Normalized waveform of shape (1, target_sr * duration_s), float32.
|
| 24 |
+
"""
|
| 25 |
+
target_samples = int(target_sr * duration_s)
|
| 26 |
+
|
| 27 |
+
# Simple linear resample
|
| 28 |
+
if sample_rate != target_sr:
|
| 29 |
+
ratio = target_sr / sample_rate
|
| 30 |
+
out_len = int(len(audio) * ratio)
|
| 31 |
+
indices = np.linspace(0, len(audio) - 1, out_len)
|
| 32 |
+
audio = np.interp(indices, np.arange(len(audio)), audio).astype(np.float32)
|
| 33 |
+
|
| 34 |
+
# Trim or pad to target length
|
| 35 |
+
if len(audio) < target_samples:
|
| 36 |
+
audio = np.pad(audio, (0, target_samples - len(audio)))
|
| 37 |
+
else:
|
| 38 |
+
audio = audio[:target_samples]
|
| 39 |
+
|
| 40 |
+
# LayerNorm normalization
|
| 41 |
+
mean = audio.mean()
|
| 42 |
+
var = ((audio - mean) ** 2).mean()
|
| 43 |
+
audio_norm = (audio - mean) / np.sqrt(var + eps)
|
| 44 |
+
|
| 45 |
+
return audio_norm.reshape(1, target_samples).astype(np.float32)
|
python/requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.21
|
| 2 |
+
onnxruntime>=1.14
|
| 3 |
+
soundfile>=0.12
|
| 4 |
+
# pyaxengine: install from https://github.com/AXERA-TECH/pyaxengine
|
reports/README.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Reports
|
| 2 |
+
|
| 3 |
+
- `simulate_report.md`: ONNX vs AXMODEL accuracy comparison
|
| 4 |
+
- `sdk_report.md`: SDK build and import verification results
|
reports/sdk_report.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SDK Report
|
| 2 |
+
|
| 3 |
+
## Python SDK
|
| 4 |
+
|
| 5 |
+
| Check | Status |
|
| 6 |
+
|-------|--------|
|
| 7 |
+
| Import | ✅ `from diarizen_sdk import DiarizenSegmenter` |
|
| 8 |
+
| Inference class | ✅ DiarizenSegmenter with NPU+CPU pipeline |
|
| 9 |
+
| Preprocessing | ✅ Audio resample + LayerNorm on CPU |
|
| 10 |
+
| Postprocessing | ✅ log_probs_to_probs, top_speakers_at_frame |
|
| 11 |
+
| Example | ✅ example.py with CLI and per-frame output |
|
| 12 |
+
| Requirements | ✅ numpy, onnxruntime, soundfile, pyaxengine |
|
| 13 |
+
|
| 14 |
+
## C++ SDK
|
| 15 |
+
|
| 16 |
+
| Check | Status |
|
| 17 |
+
|-------|--------|
|
| 18 |
+
| CMake configure | ✅ |
|
| 19 |
+
| Native build | ✅ libdiarizen_sdk.a + diarizen_example |
|
| 20 |
+
| Cross-compile | ⚠️ No aarch64 toolchain available (skipped) |
|
| 21 |
+
| Public API | ✅ DiarizenSegmenter class |
|
| 22 |
+
| Example | ✅ examples/main.cpp |
|
| 23 |
+
|
| 24 |
+
## Notes
|
| 25 |
+
|
| 26 |
+
- AX Engine runtime not available on dev machine; C++ SDK uses placeholder includes.
|
| 27 |
+
- CNN NPU inference requires pyaxengine / AX Engine runtime on target device.
|
| 28 |
+
- Backend ONNX runs on CPU via ONNX Runtime.
|
reports/simulate_report.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Simulation Report
|
| 2 |
+
|
| 3 |
+
## CNN Frontend (NPU U16, Docker Pulsar2 6.0)
|
| 4 |
+
|
| 5 |
+
| Metric | Value |
|
| 6 |
+
|--------|-------|
|
| 7 |
+
| Cosine vs ONNX | 0.9914 |
|
| 8 |
+
| MAE | 0.0026 |
|
| 9 |
+
|
| 10 |
+
## End-to-End (CNN NPU + Backend CPU)
|
| 11 |
+
|
| 12 |
+
| Metric | Value |
|
| 13 |
+
|--------|-------|
|
| 14 |
+
| Cosine vs full FP32 ONNX | 0.9997 |
|
| 15 |
+
| MAE | 0.2202 |
|
| 16 |
+
|
| 17 |
+
### Per-Class Cosine
|
| 18 |
+
|
| 19 |
+
Class 0: 0.999895 Class 1: 0.998244 Class 2: 0.999692
|
| 20 |
+
Class 3: 0.999913 Class 4: 0.999859 Class 5: 0.998687
|
| 21 |
+
Class 6: 0.999774 Class 7: 0.999509 Class 8: 0.999798
|
| 22 |
+
Class 9: 0.999723 Class 10: 0.999891
|
| 23 |
+
|
| 24 |
+
All classes > 0.998. Overall E2E cosine = 0.9997 (threshold: 0.99).
|
task.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Magnetar 任务记录
|
| 2 |
+
|
| 3 |
+
## 任务目标
|
| 4 |
+
|
| 5 |
+
将 DiariZen 模型从 GitHub 源码转换为 AX650 NPU3 可交付 AXMODEL 包。
|
| 6 |
+
|
| 7 |
+
## 输入参数
|
| 8 |
+
|
| 9 |
+
- SOURCE: https://github.com/BUTSpeechFIT/DiariZen.git
|
| 10 |
+
- TARGET_HARDWARE: AX650
|
| 11 |
+
- NPU_MODE: NPU3
|
| 12 |
+
- MODEL_NAME: diarizen
|
| 13 |
+
- SDK_LANG: both
|
| 14 |
+
- TASK_DIR: todos/work/20260706-124435-diarizen
|
| 15 |
+
- BOARD: 未提供,RUNONBOARD 阶段将跳过并记录未验证
|
| 16 |
+
|
| 17 |
+
## 阶段状态
|
| 18 |
+
|
| 19 |
+
| 阶段 | 状态 | 摘要 |
|
| 20 |
+
| --- | --- | --- |
|
| 21 |
+
| ACQUIRE | DONE | 已 clone 源码到 origin/,未发现内置权重文件 |
|
| 22 |
+
| INIT | IN_PROGRESS | 初始化目录和审计文件 |
|
| 23 |
+
| EXPORT | PENDING | 等待主模型/权重确认 |
|
| 24 |
+
| TOOLCHAIN | PENDING | - |
|
| 25 |
+
| COMPILE | PENDING | - |
|
| 26 |
+
| SIMULATE | PENDING | - |
|
| 27 |
+
| SDK-GEN | PENDING | - |
|
| 28 |
+
| RUNONBOARD | PENDING | 未提供 BOARD |
|
| 29 |
+
| PACKAGE | PENDING | - |
|
| 30 |
+
|
| 31 |
+
## 环境摘要
|
| 32 |
+
|
| 33 |
+
```text
|
| 34 |
+
python: Python 3.12.12
|
| 35 |
+
pip: pip 26.0.1 from /home/yrz/miniforge3/lib/python3.12/site-packages/pip (python 3.12)
|
| 36 |
+
git: git version 2.34.1
|
| 37 |
+
docker: Docker version 29.1.3, build 29.1.3-0ubuntu3~22.04.2
|
| 38 |
+
cmake: cmake version 3.31.5
|
| 39 |
+
pulsar2:
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## ACQUIRE 补充
|
| 43 |
+
|
| 44 |
+
- HuggingFace 权重: BUT-FIT/diarizen-wavlm-large-s80-md
|
| 45 |
+
- 快照路径: todos/work/20260706-124435-diarizen/cache/hf/models--BUT-FIT--diarizen-wavlm-large-s80-md/snapshots/a9b1b0e7974d96dcfd63af417e9da7ad8714040f
|
| 46 |
+
- 文件: config.json, config.toml, pytorch_model.bin, plda/*.npz
|
| 47 |
+
|
| 48 |
+
## ACQUIRE 补充(修正)
|
| 49 |
+
|
| 50 |
+
- HuggingFace 权重: BUT-FIT/diarizen-wavlm-large-s80-md
|
| 51 |
+
- 快照路径: 见 cache/acquire/hf_model_path.txt
|
| 52 |
+
- 文件: config.json, config.toml, pytorch_model.bin, plda/*.npz
|
| 53 |
+
|
| 54 |
+
## EXPORT
|
| 55 |
+
|
| 56 |
+
- 状态: DONE
|
| 57 |
+
- 导出脚本: export/export-static-onnx.py
|
| 58 |
+
- 原模型测试: export/test-source.py
|
| 59 |
+
- ONNX: export/model.onnx
|
| 60 |
+
- Meta: export/model_meta.json
|
| 61 |
+
- 校准数据: export/calib_data/waveform_0.npy(真实示例音频前 16 秒)
|
| 62 |
+
- 对分: cosine=0.9999999999981273, MAE=1.4469963e-05, max_abs_diff=8.4877014e-05
|
| 63 |
+
- 备注: opset17 LayerNormalization 导出失败,已改用 opset16 并记录到 issues/。
|
| 64 |
+
|
| 65 |
+
## TOOLCHAIN
|
| 66 |
+
|
| 67 |
+
- 状态: DONE
|
| 68 |
+
- Pulsar2: 使用本地 Docker 镜像 pulsar2:20260520-temp-61099061-lite
|
| 69 |
+
- C++ 交叉工具链: 待 SDK 阶段检查
|
| 70 |
+
|
| 71 |
+
## COMPILE
|
| 72 |
+
|
| 73 |
+
- 状态: STOP
|
| 74 |
+
- 配置: compile/pulsar2_config.json
|
| 75 |
+
- 日志: compile/compile_disable_transformation_check.log
|
| 76 |
+
- 报告: compile/compile_report.md
|
| 77 |
+
- 失败原因: AX650 NPU backend tiling 在 `/layers.0/attention/Gather_1` 失败,attention tensor 为 `(1,16,799,799)`。
|
| 78 |
+
- 需要用户确认后续策略: 缩短导出音频窗口 / 拆分模型 / 调整 attention 导出 / 混合 CPU+NPU。
|
| 79 |
+
|
| 80 |
+
## EXPORT 4s / COMPILE 4s
|
| 81 |
+
|
| 82 |
+
- EXPORT 4s 状态: DONE
|
| 83 |
+
- 4s ONNX: export_4s/model.onnx
|
| 84 |
+
- 输入/输出: `[1,1,64000] -> [1,199,11]`
|
| 85 |
+
- ONNX 对分: cosine=0.9999999999988147, MAE=1.1889559e-05, max_abs_diff=5.0544739e-05
|
| 86 |
+
- COMPILE 4s 状态: STOP
|
| 87 |
+
- 编译日志: compile_4s/compile.log
|
| 88 |
+
- 编译报告: compile_4s/compile_report.md
|
| 89 |
+
- 失败原因: AX650/NPU3 仍在 `/layers.0/attention/Gather_1` tiling 失败,attention tensor 为 `(1,16,199,199)`。
|
| 90 |
+
|
| 91 |
+
## PULSAR2 SOURCE PATCH 验证
|
| 92 |
+
|
| 93 |
+
- 源码路径: /home/yrz/Codes/npu-codebase
|
| 94 |
+
- 补丁文件: axnn/axnn/backend/ax650npu/oprimpl/gather.py
|
| 95 |
+
- 验证日志: compile_4s_srcpatch/compile.log
|
| 96 |
+
- 结果: 原 `/layers.0/attention/Gather_1` tiling 报错已越过;当前下一个 STOP 是 `AxQuantizedLayerNorm` 后端构建失败。
|
| 97 |
+
|
| 98 |
+
## NPU Codebase Patch
|
| 99 |
+
|
| 100 |
+
- 状态: PARTIAL FIX VERIFIED
|
| 101 |
+
- 修改: /home/yrz/Codes/npu-codebase/axnn/axnn/backend/ax650npu/oprimpl/gather.py
|
| 102 |
+
- 测试: `python -m pytest axnn/axnn/backend/pytests/gather_test.py -k large_stride_payload_slice -q` 通过
|
| 103 |
+
- 源码版 Pulsar2: 使用 `USE_PULSAR2` 对应环境验证,原 Gather tiling 报错已消失;当前阻塞转移到 `AxQuantizedLayerNorm`。
|
| 104 |
+
|
| 105 |
+
## COMPILE 无输入 LayerNorm(source Pulsar2 + gather fix)
|
| 106 |
+
|
| 107 |
+
- 状态: COMPILED (with critical quality issue)
|
| 108 |
+
- ONNX: export_4s/model_no_input_ln.onnx(已去除输入 LayerNorm)
|
| 109 |
+
- 配置: compile_no_ln/pulsar2_config.json
|
| 110 |
+
- AXMODEL: compile_no_ln/model.axmodel (77MB)
|
| 111 |
+
- 输入: waveform (1, 1, 64000) float32, 需 CPU 预做 LayerNorm
|
| 112 |
+
- 输出: log_probs (1, 199, 11) float32
|
| 113 |
+
- MACs: 16.6G
|
| 114 |
+
- NPU 子图: 1 个全 NPU 子图,1115 ops
|
| 115 |
+
|
| 116 |
+
### 关键发现
|
| 117 |
+
|
| 118 |
+
- Docker Pulsar2 (5.1/6.0): 无法编译,AxGather tiling 失败
|
| 119 |
+
- Source Pulsar2 + gather patch: 编译通过但 NPU 后端输出恒值
|
| 120 |
+
- per-op FP32 仿真精度 >0.999(量化参数正确)
|
| 121 |
+
- INT8 Reference 模式 cosine=0.76(量化可用但精度下降)
|
| 122 |
+
- NPUBackend/compiled 模式输出恒值 -16.0319 → NPU 后端 bug
|
| 123 |
+
|
| 124 |
+
### Pulsar2 补丁
|
| 125 |
+
|
| 126 |
+
- 文件: /home/yrz/Codes/npu-codebase/axnn/axnn/backend/ax650npu/oprimpl/gather.py
|
| 127 |
+
- 修改:
|
| 128 |
+
1. `tx[:, i*sub_elems:...]` → `tx[..., i*sub_elems:...]` (修复 3D tensor 索引)
|
| 129 |
+
2. `sub_elems` capped 到 `tx.shape[-1]`(防止越界切片)
|
| 130 |
+
|
| 131 |
+
### 根因判断
|
| 132 |
+
|
| 133 |
+
WavLM 24 层 Transformer + Conformer 的组合对 AX650 NPU INT8 计算路径过于复杂,
|
| 134 |
+
NPU 后端的某个(或多��)算子实现在 INT8 精度下产生数值错误,导致模型输出完全崩溃。
|
| 135 |
+
per-op FP32 仿真表明各算子单独工作正常,问题出在全链路 INT8 执行时的误差累积或控制流错误。
|
| 136 |
+
|
| 137 |
+
### 建议后续方向
|
| 138 |
+
|
| 139 |
+
1. 拆分模型:WavLM CNN 前端 → NPU,Transformer + Conformer → CPU
|
| 140 |
+
2. 尝试更大校准数据集提升 Reference 精度
|
| 141 |
+
3. 等待 Pulsar2 更新修复 NPU 后端 bug
|
| 142 |
+
4. 联系爱芯技术支持排查具体算子问题
|
| 143 |
+
|
| 144 |
+
## 模型拆分方案
|
| 145 |
+
|
| 146 |
+
因完整模型无法在 AX650 NPU 上正确编译(NPU 后端 INT8 bug),采用 CPU+NPU 混合方案:
|
| 147 |
+
|
| 148 |
+
| 组件 | 硬件 | 精度 | 大小 |
|
| 149 |
+
|------|------|------|------|
|
| 150 |
+
| WavLM CNN 前端 (7 conv layers) | AX650 NPU | U16 | 1.3MB axmodel |
|
| 151 |
+
| WavLM Transformer (24L) + Conformer (4L) + Classifier | CPU (ONNX Runtime) | FP32 | 273MB ONNX |
|
| 152 |
+
|
| 153 |
+
## 拆分编译
|
| 154 |
+
|
| 155 |
+
### CNN 前端
|
| 156 |
+
- Docker Pulsar2: 6.0-lite
|
| 157 |
+
- 配置: compile_cnn/pulsar2_config_u16.json (全图 U16)
|
| 158 |
+
- AXMODEL: compile_cnn_u16_full/cnn_features.axmodel
|
| 159 |
+
- 输入: waveform_ln (1, 64000) float32(CPU 预做 LayerNorm)
|
| 160 |
+
- 输出: cnn_features (1, 199, 211) float32
|
| 161 |
+
- SIMULATE: cosine=0.9914 vs ONNX
|
| 162 |
+
|
| 163 |
+
### 后端 (CPU)
|
| 164 |
+
- ONNX: export_backend/backend.onnx (2709 ops, 273MB)
|
| 165 |
+
- 输入: cnn_features (1, 199, 211) float32
|
| 166 |
+
- 输出: log_probs (1, 199, 11) float32
|
| 167 |
+
- 验证: cosine=1.0 vs PyTorch
|
| 168 |
+
|
| 169 |
+
### 端到端
|
| 170 |
+
- cosine=0.9997 vs 完整 FP32 ONNX
|
| 171 |
+
- 所有 11 个 class cosine > 0.998
|
| 172 |
+
|
| 173 |
+
## 最终状态
|
| 174 |
+
|
| 175 |
+
| 阶段 | 状态 | 摘要 |
|
| 176 |
+
| --- | --- | --- |
|
| 177 |
+
| ACQUIRE | DONE | 源码 + HuggingFace 权重 |
|
| 178 |
+
| INIT | DONE | 目录初始化 |
|
| 179 |
+
| EXPORT | DONE | CNN 前端 + 后端 ONNX,cosine=1.0 |
|
| 180 |
+
| TOOLCHAIN | DONE | Docker Pulsar2 6.0-lite |
|
| 181 |
+
| COMPILE | DONE | CNN U16,Docker Pulsar2 |
|
| 182 |
+
| SIMULATE | DONE | E2E cosine=0.9997 |
|
| 183 |
+
| SDK-GEN | DONE | Python + C++ SDK |
|
| 184 |
+
| RUNONBOARD | SKIPPED | 无板端 |
|
| 185 |
+
| PACKAGE | DONE | package/ 264MB,GitHub-ready |
|