Skip to content

Commit a80fb97

Browse files
mirek1900xShug0
andauthored
Add standalone GGUF support for Hviske v5.3 and Citrinet-256 (0xShug0#46)
* Add standalone GGUF support for Hviske and Citrinet Define package specs for Hviske v5.3 and Citrinet-256 so the framework resolves native safetensors, standalone GGUF weights, and named config, tokenizer, and vocabulary resources. Decode incompatible quantized convolution reshapes to F32, add native/GGUF regression coverage, document Q8_0 conversion, and record the tested GGUF status. * Add native q8 reshape preservation test --------- Co-authored-by: 0xShug0 <231717474+0xShug0@users.noreply.github.com>
1 parent f2af1b7 commit a80fb97

14 files changed

Lines changed: 402 additions & 148 deletions

File tree

CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,17 @@ if (ENGINE_BUILD_TESTS)
822822
COMMAND scaled_dot_product_attention_test
823823
)
824824
825+
add_engine_unittest(asr_standalone_gguf_test tests/unittests/test_asr_standalone_gguf.cpp)
826+
target_include_directories(asr_standalone_gguf_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/unittests)
827+
target_compile_definitions(asr_standalone_gguf_test PRIVATE
828+
ENGINE_TEST_ASSET_ROOT="${ENGINE_UNITTEST_ASSET_ROOT}"
829+
)
830+
831+
add_test(
832+
NAME asr_standalone_gguf_test
833+
COMMAND asr_standalone_gguf_test
834+
)
835+
825836
add_executable(server_multipart_test
826837
tests/unittests/test_server_multipart.cpp
827838
app/server/multipart.cpp

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -706,11 +706,12 @@ The framework also has a reusable GGUF tensor source and a streaming converter.
706706
container reader is shared by all model families; a family still has to list a `.gguf`
707707
checkpoint as one of its accepted assets because model configuration and tensor naming
708708
remain architecture-specific. Qwen3 ASR, Qwen3 Forced Aligner, Qwen3 TTS, Nemotron
709-
3.5 ASR, VibeVoice-ASR, and Higgs Audio STT currently accept `model.gguf` (including
710-
`speech_tokenizer/model.gguf` for TTS). The converter recursively embeds sidecar files
709+
3.5 ASR, VibeVoice-ASR, Higgs Audio STT, Hviske ASR, and Citrinet ASR currently accept
710+
`model.gguf` (including `speech_tokenizer/model.gguf` for TTS). The converter recursively embeds sidecar files
711711
up to 64 MiB by default using binary-safe metadata, including nested tokenizer models,
712-
and Qwen3 ASR, Nemotron ASR, VibeVoice-ASR, and Higgs Audio STT can load the resulting
713-
`model.gguf` as a standalone file. Pass `--no-sidecars` when a tensor-only container is desired. A
712+
and Qwen3 ASR, Nemotron ASR, VibeVoice-ASR, Higgs Audio STT, Hviske ASR, and Citrinet
713+
ASR can load the resulting `model.gguf` as a standalone file. Pass `--no-sidecars` when a
714+
tensor-only container is desired. A
714715
`model.safetensors.index.json` is also a first-class tensor source and is merged from
715716
its routed shards while converting. Exact original tensor ranks are stored separately
716717
because GGML normally collapses trailing singleton dimensions. Rank-0 safetensors

docs/asr.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ Citrinet is an offline CTC ASR model. It produces transcription text from speech
2727
audiocpp_cli --task asr --family citrinet_asr --model models/citrinet --backend cuda --audio speech_16k.wav
2828
```
2929

30+
Create a standalone Q8_0 GGUF from the converted Citrinet safetensors layout:
31+
32+
```powershell
33+
audiocpp_gguf.exe --input models\citrinet\citrinet_256.safetensors --root models\citrinet --output models\citrinet-Q8_0\model.gguf --type q8_0
34+
```
35+
36+
The GGUF embeds `citrinet_256_config.json` and the vocabulary/tokenizer sidecars, so the
37+
completed `model.gguf` can be moved, renamed, and passed directly to `--model`.
38+
3039
| Option | Values | Default | Meaning |
3140
|---|---|---:|---|
3241
| `--audio` | WAV path | required | Speech input. Use 16 kHz WAV for the example path. |
@@ -100,6 +109,15 @@ Hviske ASR is an offline Cohere ASR model path. The integration exposes Danish p
100109
audiocpp_cli --task asr --family hviske_asr --model models/hviske-v5.3 --backend cuda --audio speech_16k.wav --text-out transcript.txt
101110
```
102111

112+
Create a standalone Q8_0 GGUF:
113+
114+
```powershell
115+
audiocpp_gguf.exe --input models\hviske-v5.3\model.safetensors --root models\hviske-v5.3 --output models\hviske-v5.3-Q8_0\model.gguf --type q8_0
116+
```
117+
118+
Configuration, generation settings, and the SentencePiece tokenizer are embedded. The
119+
completed GGUF can therefore be moved, renamed, and passed directly to `--model`.
120+
103121
| Option | Values | Default | Meaning |
104122
|---|---|---:|---|
105123
| `--audio` | WAV path | required | Speech input. |

docs/gguf.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ Status labels:
139139
|---|---|---|---|---|---|
140140
| `ace_step` | No | --- | --- | --- | --- |
141141
| `chatterbox` | No | --- | --- | --- | --- |
142-
| `citrinet_asr` | No | --- | --- | --- | --- |
142+
| `citrinet_asr` | Done | Pass | --- | --- | Pass |
143143
| `heartmula` | No | --- | --- | --- | --- |
144144
| `higgs_audio_stt` | Done | Pass | Pass | --- | Pass |
145145
| `htdemucs` | No | --- | --- | --- | --- |
146-
| `hviske_asr` | No | --- | --- | --- | --- |
146+
| `hviske_asr` | Done | Pass | --- | --- | Pass |
147147
| `index_tts2` | Done | Pass | Pass (drift) | Pass | No (similarity drift, frame drift, text minor drift) |
148148
| `irodori_tts` | Done | Pass | Pass | --- | Pass (drift) |
149149
| `marblenet_vad` | No | --- | --- | --- | --- |

include/engine/framework/core/backend_weight_store.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,10 @@ class BackendWeightStore {
244244
if (!ggml_is_quantized(type)) {
245245
return resolved_type;
246246
}
247-
if (storage_type == assets::TensorStorageType::Native) {
248-
return resolved_type;
249-
}
247+
// A GGUF may store a source matrix in quantized form even when a model
248+
// reshapes it into a convolution kernel. Quantized GGML rows cannot end
249+
// in a non-block-sized dimension (for example a 1-wide Conv1D kernel),
250+
// so decode such native tensors to F32 for the reshaped backend weight.
250251
if (shape.rank < 2 || shape.last_dim() % ggml_blck_size(type) != 0) {
251252
return assets::TensorStorageType::F32;
252253
}

include/engine/models/citrinet_asr/assets.h

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#pragma once
22

3-
#include "engine/framework/assets/tensor_source.h"
3+
#include "engine/framework/assets/resource_bundle.h"
44

55
#include <cstdint>
66
#include <filesystem>
@@ -100,14 +100,6 @@ struct CitrinetWeights {
100100
std::vector<std::string> vocab;
101101
};
102102

103-
struct CitrinetAssetPaths {
104-
std::filesystem::path model_root;
105-
std::filesystem::path checkpoint_path;
106-
std::filesystem::path config_path;
107-
std::filesystem::path vocab_path;
108-
};
109-
110-
CitrinetAssetPaths resolve_citrinet_assets(const std::filesystem::path & checkpoint_path);
111-
std::shared_ptr<const CitrinetWeights> load_citrinet_weights_cached(const std::filesystem::path & checkpoint_path);
103+
std::shared_ptr<const CitrinetWeights> load_citrinet_weights_cached(const std::filesystem::path & model_path);
112104

113105
} // namespace engine::models::citrinet_asr

include/engine/models/hviske_asr/assets.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#pragma once
22

3-
#include "engine/framework/assets/tensor_source.h"
3+
#include "engine/framework/assets/resource_bundle.h"
44
#include "engine/framework/tokenizers/sentencepiece.h"
55

66
#include <cstdint>
@@ -63,13 +63,13 @@ struct HviskeConfig {
6363
};
6464

6565
struct HviskeAssets {
66-
std::filesystem::path model_root;
66+
engine::assets::ResourceBundle resources;
6767
HviskeConfig config;
6868
std::vector<engine::tokenizers::SentencePiecePiece> tokenizer_pieces;
6969
std::shared_ptr<const engine::assets::TensorSource> model_weights;
7070
};
7171

72-
std::shared_ptr<const HviskeAssets> load_hviske_assets(const std::filesystem::path & model_root);
72+
std::shared_ptr<const HviskeAssets> load_hviske_assets(const std::filesystem::path & model_path);
7373

7474
std::vector<int32_t> tokenize_hviske_prompt(
7575
const HviskeAssets & assets,

model_specs/citrinet_asr.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"family": "citrinet_asr",
3+
"sources": [
4+
{
5+
"format": "gguf",
6+
"roots": {
7+
"model": ".",
8+
"weights": "$gguf"
9+
},
10+
"files": {
11+
"config": "model:citrinet_256_config.json",
12+
"tokenizer": "model:citrinet_256_tokenizer.model",
13+
"vocab": "model:citrinet_256_vocab.txt"
14+
},
15+
"tensors": {
16+
"weights": "weights:"
17+
}
18+
},
19+
{
20+
"format": "safetensors",
21+
"roots": {
22+
"model": "."
23+
},
24+
"files": {
25+
"config": "model:citrinet_256_config.json",
26+
"tokenizer": "model:citrinet_256_tokenizer.model",
27+
"vocab": "model:citrinet_256_vocab.txt"
28+
},
29+
"tensors": {
30+
"weights": "model:citrinet_256.safetensors"
31+
}
32+
}
33+
]
34+
}

model_specs/hviske_asr.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"family": "hviske_asr",
3+
"sources": [
4+
{
5+
"format": "gguf",
6+
"roots": {
7+
"model": ".",
8+
"weights": "$gguf"
9+
},
10+
"files": {
11+
"config": "model:config.json",
12+
"generation_config": "model:generation_config.json",
13+
"tokenizer": "model:tokenizer.model"
14+
},
15+
"tensors": {
16+
"weights": "weights:"
17+
}
18+
},
19+
{
20+
"format": "safetensors",
21+
"roots": {
22+
"model": "."
23+
},
24+
"files": {
25+
"config": "model:config.json",
26+
"generation_config": "model:generation_config.json",
27+
"tokenizer": "model:tokenizer.model"
28+
},
29+
"tensors": {
30+
"weights": "model:model.safetensors"
31+
}
32+
}
33+
]
34+
}

src/models/citrinet_asr/assets.cpp

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#include "engine/models/citrinet_asr/assets.h"
22

3-
#include "engine/framework/assets/resource_bundle.h"
3+
#include "engine/framework/assets/model_package.h"
44
#include "engine/framework/assets/tensor_source.h"
55
#include "engine/framework/assets/weight_metadata.h"
66
#include "engine/framework/io/filesystem.h"
@@ -15,25 +15,6 @@
1515
namespace engine::models::citrinet_asr {
1616
namespace io = engine::io;
1717
namespace asset_meta = engine::assets;
18-
CitrinetAssetPaths resolve_citrinet_assets(const std::filesystem::path & checkpoint_path) {
19-
assets::ResourceBundle resources(checkpoint_path.parent_path());
20-
resources.add_file("weights", checkpoint_path);
21-
resources.add_file("config", assets::checkpoint_sidecar_config_path(checkpoint_path));
22-
23-
const auto config = resources.parse_json("config");
24-
const auto * vocab_file = config.find("vocab_file");
25-
if (vocab_file == nullptr || !vocab_file->is_string()) {
26-
throw std::runtime_error("Citrinet config is missing vocab_file");
27-
}
28-
resources.add_model_file("vocab", vocab_file->as_string());
29-
30-
CitrinetAssetPaths paths;
31-
paths.model_root = resources.model_root();
32-
paths.checkpoint_path = resources.require_file("weights");
33-
paths.config_path = resources.require_file("config");
34-
paths.vocab_path = resources.require_file("vocab");
35-
return paths;
36-
}
3718

3819
namespace {
3920

@@ -153,20 +134,17 @@ CitrinetConfig parse_config(const io::json::Value & root) {
153134
return cfg;
154135
}
155136

156-
CitrinetWeights load_citrinet_weights(const std::filesystem::path & checkpoint_path) {
157-
const auto assets = resolve_citrinet_assets(checkpoint_path);
137+
CitrinetWeights load_citrinet_weights(engine::assets::ResourceBundle resources) {
158138
CitrinetWeights weights;
159-
engine::assets::ResourceBundle resources(assets.model_root);
160-
resources.add_file("weights", assets.checkpoint_path);
161139
const auto source = resources.open_tensor_source("weights");
162140
weights.source = source;
163-
weights.config = parse_config(io::json::parse_file(assets.config_path));
141+
weights.config = parse_config(resources.parse_json("config"));
164142
weights.window = source->require_f32("preprocessor.featurizer.window", {weights.config.win_length});
165143
weights.fb = source->require_f32(
166144
"preprocessor.featurizer.fb",
167145
{1, weights.config.n_mels, weights.config.n_fft / 2 + 1});
168146

169-
weights.vocab = load_vocab_file(assets.vocab_path);
147+
weights.vocab = load_vocab_file(resources.require_file("vocab"));
170148
if (static_cast<int64_t>(weights.vocab.size()) != weights.config.vocab_size) {
171149
throw std::runtime_error("vocab size mismatch");
172150
}
@@ -262,10 +240,13 @@ std::string checkpoint_cache_key(const std::filesystem::path & checkpoint_path)
262240

263241
} // namespace
264242

265-
std::shared_ptr<const CitrinetWeights> load_citrinet_weights_cached(const std::filesystem::path & checkpoint_path) {
243+
std::shared_ptr<const CitrinetWeights> load_citrinet_weights_cached(const std::filesystem::path & model_path) {
266244
static std::mutex cache_mutex;
267245
static std::unordered_map<std::string, std::weak_ptr<const CitrinetWeights>> cache;
268-
const auto key = checkpoint_cache_key(checkpoint_path);
246+
auto resources = engine::assets::load_resource_bundle_from_package_spec(
247+
model_path,
248+
engine::assets::default_model_package_spec_path("citrinet_asr"));
249+
const auto key = checkpoint_cache_key(resources.require_file("weights"));
269250
{
270251
std::lock_guard<std::mutex> lock(cache_mutex);
271252
if (const auto it = cache.find(key); it != cache.end()) {
@@ -274,7 +255,7 @@ std::shared_ptr<const CitrinetWeights> load_citrinet_weights_cached(const std::f
274255
}
275256
}
276257
}
277-
auto loaded = std::make_shared<const CitrinetWeights>(load_citrinet_weights(checkpoint_path));
258+
auto loaded = std::make_shared<const CitrinetWeights>(load_citrinet_weights(std::move(resources)));
278259
{
279260
std::lock_guard<std::mutex> lock(cache_mutex);
280261
cache[key] = loaded;

0 commit comments

Comments
 (0)