Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,18 @@ audiocpp_add_model(niagara_asr
engine::models::niagara_asr::make_niagara_asr_loader
)

audiocpp_add_model(edda_asr
SOURCES
src/community_models/edda_asr/assets.cpp
src/community_models/edda_asr/weights.cpp
src/community_models/edda_asr/runtime.cpp
src/community_models/edda_asr/session.cpp
INCLUDES
engine/community_models/edda_asr/model.h
LOADERS
engine::community_models::edda_asr::make_edda_asr_loader
)

audiocpp_add_model(whistle_asr
SOURCES
src/community_models/whistle_asr/assets.cpp
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Community model ports live under `community_models` to make the ownership bounda
| **confucius4_r2t2** | ASR | 30 languages | Safetensors, GGUF F16/Q8/Q4, Stream | [@davidxifeng](https://github.com/davidxifeng) | [Confucius4-R2T2](docs/models/r2t2.md) streaming ASR with context and hotword prompts |
| **earshot_vad** | VAD | lang agnostic | Bundled, Stream | [@Etherll](https://github.com/Etherll) | [Earshot VAD](docs/models/earshot_vad.md) native C++ port with offline and streaming speech detection |
| **echo_tts** | TTS, Clone | en | GGUF 16/Q8 | [@5uck1ess](https://github.com/5uck1ess) | [Echo-TTS](docs/models/echo_tts.md) 44.1 kHz zero-shot voice cloning with EchoDiT latents and Fish S1-DAC decoding |
| **edda_asr** | ASR | da | Safetensors, GGUF 16/Q8, Stream | Lars Karlslund [@lkarlslund](https://github.com/lkarlslund) | [Edda v0.2 large](docs/models/edda_asr.md) Danish Whisper-large-v3 with lossless DFlash2 speculative decoding from the edda-v0.2-large-hurtig drafter |
| **f5_tts** | TTS, Clone | en, ar (Habibi) | GGUF, LoRA | [@tareko](https://github.com/tareko) | [F5-TTS](docs/models/f5_tts.md) flow-matching DiT synthesis and voice cloning, with Habibi Arabic aliases `habibi`/`habibi_tts` |
| **fish_drama3** | TTS, Clone, Ctrl, Edit | auto | Mixed Q2_K/Q3_K/Q4_K GGUF, IQ3_XXS (small), Stream | [@JarodMica](https://github.com/JarodMica) (base), [@Th-Underscore](https://github.com/Th-Underscore) (small) | Experimental Fish Audio Drama 3 port and Drama3-small variant support |
| **glm_tts** | TTS, Clone | zh, en | GGUF | Mirek [@mirek190](https://github.com/mirek190) | [GLM-TTS](docs/models/glm_tts.md) zero-shot synthesis and voice cloning support |
Expand Down
1 change: 1 addition & 0 deletions docs/asr.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ For Qwen3-ASR adapter loading and merged GGUF export, see the [LoRA guide](lora.
| Cohere Transcribe | `cohere_asr` | offline | [Cohere Transcribe](models/cohere_asr.md) |
| Confucius4-R2T2 | `confucius4_r2t2` | offline, streaming | [Confucius4-R2T2](models/r2t2.md) |
| CrisperWhisper 2.0 Large | `crisperwhisper` | offline, streaming | [CrisperWhisper 2.0](models/crisperwhisper.md) |
| Edda v0.2 Large (experimental) | `edda_asr` | offline, streaming | [Edda ASR](models/edda_asr.md) |
| Fun-ASR-Nano | `fun_asr_nano` | offline | [Fun-ASR-Nano](#fun-asr-nano) |
| GigaAM v3 / Multilingual | `gigaam_asr` | offline | [GigaAM ASR](models/gigaam_asr.md) |
| Granite Speech 5.0 TurboCTC | `granite5asr` | offline | [Granite Speech 5.0 TurboCTC](models/granite5asr.md) |
Expand Down
1 change: 1 addition & 0 deletions docs/model_licenses.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Licenses change. If a row is wrong or out of date, please open a PR.
| `dramabox` | [ResembleAI/Dramabox](https://huggingface.co/ResembleAI/Dramabox) | [LTX-2 Community License](https://huggingface.co/ResembleAI/Dramabox/blob/main/LICENSE) | Conditional | A paid license is needed from USD 10M yearly revenue. Generated content must be disclosed as machine-generated. Not for products that compete with Lightricks'. The Gemma 3 text encoder falls under the Gemma Terms of Use. | 2026-09-21 |
| `earshot_vad` | [pykeio/earshot](https://github.com/pykeio/earshot) | MIT OR Apache-2.0 | Yes | Bundled in `assets/framework/models`; converted from the 1.2.2 crate. | 2026-10-26 |
| `echo_tts` | [jordand/echo-tts-base](https://huggingface.co/jordand/echo-tts-base)<br>[jordand/fish-s1-dac-min](https://huggingface.co/jordand/fish-s1-dac-min) | CC-BY-NC-SA-4.0 | No | | 2026-09-21 |
| `edda_asr` | [danish-foundation-models/edda-v0.2-large](https://huggingface.co/danish-foundation-models/edda-v0.2-large)<br>[syvai/edda-v0.2-large-hurtig](https://huggingface.co/syvai/edda-v0.2-large-hurtig) | Apache-2.0 | Yes | Retain the hurtig NOTICE, which credits syv.ai for the drafter and the Alexandra Institute and Danish Foundation Models for edda. The base model, openai/whisper-large-v3, is MIT. | 2026-10-30 |
| `f5_tts` | [SWivid/F5-TTS](https://huggingface.co/SWivid/F5-TTS)<br>[SWivid/Habibi-TTS](https://huggingface.co/SWivid/Habibi-TTS) | CC-BY-NC-4.0 (F5-TTS); CC-BY-NC-SA-4.0 (Habibi-TTS) | No | | 2026-09-21 |
| `firered_audio` | [FireRedTeam/FireRedAudio](https://huggingface.co/FireRedTeam/FireRedAudio) | Apache-2.0 | Yes | | 2026-09-21 |
| `firered_vad` | [FireRedTeam/FireRedVAD](https://huggingface.co/FireRedTeam/FireRedVAD) | Apache-2.0 | Yes | VAD and Stream-VAD checkpoints. | 2026-10-07 |
Expand Down
167 changes: 167 additions & 0 deletions docs/models/edda_asr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Edda ASR (experimental)

The `edda_asr` community family runs
[Edda v0.2 large](https://huggingface.co/danish-foundation-models/edda-v0.2-large), a Danish
Whisper-large-v3 fine-tune by the Alexandra Institute and Danish Foundation Models.
It also runs [edda-v0.2-large-hurtig](https://huggingface.co/syvai/edda-v0.2-large-hurtig),
which adds a 170M-parameter DFlash2 drafter trained by syv.ai. The edda weights in that
repository are byte-identical to the original release; the drafter only speeds up
greedy decoding.

| Field | Value |
|---|---|
| Family | `edda_asr` |
| Task | `asr` |
| Modes | `offline`, `streaming` (complete input; one event per audio chunk) |
| Language | Danish (`da`). Other Whisper language tokens are accepted, but the drafter was trained on Danish only. |
| Decoding | Greedy with optional lossless speculative decoding, or Transformers-compatible beam search |
| Long-form audio | Split at silence into windows of at most 30 seconds |
| Timestamps | Not supported |

## How speculative decoding works

Edda's 32-layer decoder normally produces one token per forward pass. With the drafter,
each step drafts a block: the last verified token followed by 15 mask slots, all predicted
in one drafter pass. Edda then verifies the whole block in one forward pass. It keeps the
longest drafted prefix it agrees with, plus its own next token. Edda's greedy choice still
decides every token, so the transcript is edda's greedy transcript.

The drafter reads hidden states from edda decoder layers 1, 8, 15, 22 and 29 as extra
attention context, cross-attends to edda's encoder output, and uses DFlash2's grouped
dynamic causal convolutions and candidate selector. The whole drafter runs as one GGML
graph. The selector's greedy walk over the top 16 candidates per slot runs on the host.

Verifying 16 tokens costs little more than decoding one. On gfx1151 a 16-token verify pass
takes about 17.5 ms against 13.4 ms for a single token, and a drafter pass takes about
5.4 ms. With 5 to 10 tokens accepted per pass, the decoder runs 3 to 7 times faster.

Speculative and plain greedy output can differ when two tokens are almost equally likely.
A 16-token verify pass runs different kernels than a single-token pass, and the small
numeric difference can flip such a tie. The model card reports the same effect in fp16
(299 of 300 clips identical).

## Models

Download either Hugging Face repository as-is; the runtime reads the safetensors
directory directly.

| Package id | Source | Contents |
|---|---|---|
| `edda_v0_2_large_hurtig_safetensors` | [syvai/edda-v0.2-large-hurtig](https://huggingface.co/syvai/edda-v0.2-large-hurtig) | Edda plus the drafter (about 3.4 GB) |
| `edda_v0_2_large_safetensors` | [danish-foundation-models/edda-v0.2-large](https://huggingface.co/danish-foundation-models/edda-v0.2-large) | Edda only, greedy decoding (about 3.1 GB) |

No audio.cpp GGUF is published yet. The `edda_v0_2_large_hurtig_q8_0` and
`edda_v0_2_large_hurtig_f16` package entries describe the layout of a local conversion.

```bash
huggingface-cli download syvai/edda-v0.2-large-hurtig --local-dir models/edda-v0.2-large-hurtig
```

## CLI

```bash
audiocpp_cli --task asr --family edda_asr \
--model models/edda-v0.2-large-hurtig \
--backend cuda --audio danish.wav --log
```

With `--log`, each audio chunk reports its token count, target passes, tokens per pass and
accepted drafted tokens, plus `edda.encoder_ms`, `edda.decoder_ms` and `edda.drafter_ms`.

Request options:

| Option | Default | Meaning |
|---|---|---|
| `language` | `da` | Whisper language token in the decoder prompt. |
| `speculative` | `true` | Use the drafter when the model ships one. |
| `draft_block` | `16` | Anchor token plus up to `draft_block - 1` drafted tokens per verify pass (2 to 16). |
| `draft_selector` | `true` | Pick drafted tokens with the DFlash2 candidate selector instead of per-slot argmax. |
| `num_beams` | `1` | Beam-search width (up to 16). Above 1 the drafter is not used. Edda's own generation config uses 5. |
| `length_penalty` | `1.0` | Exponent that normalizes finished beam scores by generated length. |
| `max_tokens` | `444` | Generated-token limit per chunk, bounded by the 448-position decoder context. |
| `audio_chunk_mode` | `auto` | `auto` splits audio longer than 30 seconds at silence; `silence`, `fixed` and `none` as in other ASR families. |
| `audio_chunk_duration_sec` | `30` | Maximum chunk length when splitting. |

Session options:

| Option | Default | Meaning |
|---|---|---|
| `weight_type` | `native` | Matmul weight storage, for example `q8_0`. Applies to edda and the drafter. |
| `drafter` | `true` | Load the drafter. `false` saves its memory (about 340 MB in F16) and decodes greedily. |

## GGUF conversion

The converter packs edda, the drafter (under the `drafter.` prefix), the tokenizer and
both configs into one GGUF:

```bash
cmake --build build/release --target audiocpp_gguf
audiocpp_gguf --family edda_asr \
--input models/edda-v0.2-large-hurtig/model.safetensors \
--input drafter=models/edda-v0.2-large-hurtig/drafter.safetensors \
--root models/edda-v0.2-large-hurtig \
--output models/Edda-v0.2-Large-Hurtig-GGUF/edda-v0.2-large-hurtig-q8_0.gguf \
--type q8_0
```

Drop the `drafter=` input to convert the plain edda release. On HIP, the encoder is faster
with F16 than with Q8_0 weights. Add `--keep-type 'model.encoder.*=f16'` to quantize only
the decoder and the drafter.

## Beam search

Edda's published WER uses 5-beam search, which is slower but somewhat more accurate than
greedy decoding. `--request-option num_beams=5` follows the Transformers beam search used by
`WhisperForConditionalGeneration.generate`: log-softmax before the suppress-token processors,
the top `2 * num_beams` continuations per step, length-normalized finished scores and the
default `early_stopping=False` stopping heuristic.

The beams share one KV cache as a token tree. Each beam's newest token takes a fresh cache
slot, and its attention mask admits only that beam's ancestors, so no cache is copied or
reordered when beams are pruned. The tree cache holds `prompt + num_beams * max_tokens`
slots (about 380 MB for 5 beams) and is allocated on the first beam-search request.

## Performance

Mean per clip over 59 FLEURS `da_dk` clips (11.3 seconds of audio on average), after one
warm-up clip, on an AMD Radeon 8060S (gfx1151, HIP). Encoder times vary by about ±30 ms
between runs.

| Mode | Weights | Encoder | Decoder | Total | Tokens per edda pass |
|---|---|---|---|---|---|
| Greedy (`speculative=false`) | F16 | 265 ms | 597 ms | 862 ms | 1.00 |
| Speculative | F16 | 265-325 ms | 193 ms | 519 ms | 5.24 |
| Speculative | Q8_0 decoder, F16 encoder GGUF | 277 ms | 152 ms | 428 ms | 5.24 |
| Beam search, 5 beams | F16 | 304 ms | 929 ms | 1234 ms | 0.98 |

The encoder always processes a full 30-second window, so it dominates short clips once
decoding is speculative. It uses flash attention over F16 keys and values; on this GPU,
ggml's HIP flash-attention tile kernel is its largest single cost.

## Validation

The validation clips are 5 NST Danish test sentences and the first 60 FLEURS `da_dk`
test clips by file name. References come from PyTorch FP32 on the CPU: syv.ai's
`hurtig.spec_generate` for speculative and greedy decoding (it equals Transformers greedy
`generate()` on all 60 FLEURS clips), and `generate(num_beams=5)` for beam search.

| Check | Result |
|---|---|
| NST, 5 clips, HIP F16, speculative | 5/5 identical to the reference |
| NST, 5 clips, F16 GGUF, Q8_0 GGUF, Q8_0 decoder with F16 encoder, HIP | 5/5 identical |
| NST, 5 clips, HIP F16, 5-beam search | 5/5 identical to Transformers beam search |
| FLEURS, the 10 clips where 5-beam output differs from greedy, HIP F16 | 10/10 identical to Transformers beam search |
| NST, 1 clip, CPU backend | Identical |
| FLEURS, 60 clips, HIP F16, speculative | 59/60 identical to the reference |
| FLEURS, 60 clips, HIP F16, greedy | 59/60 identical to the reference |
| FLEURS, 60 clips, Q8_0 decoder with F16 encoder, speculative | 55/60 identical to the reference |
| NST sentences concatenated into 180 seconds, default silence chunking | 0.0% WER against the source sentences |

The single FLEURS difference in each F16 mode is a near-tie between two spellings of a name.
Speculative and greedy runs each miss a different one of the two such clips, in the same way
the model card reports 299 of 300 identical clips in fp16.

Known limitations:

- No timestamps.
- One request at a time per session (batch size 1).
153 changes: 153 additions & 0 deletions include/engine/community_models/edda_asr/model.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#pragma once

#include "engine/framework/assets/resource_bundle.h"
#include "engine/framework/audio/dsp.h"
#include "engine/framework/core/backend_weight_store.h"
#include "engine/framework/core/execution_context.h"
#include "engine/framework/modules/attention/types.h"
#include "engine/framework/modules/speech_encoders/whisper_embedding.h"
#include "engine/framework/runtime/model.h"
#include "engine/framework/tokenizers/llama_bpe.h"

#include <array>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>

namespace engine::community_models::edda_asr {

// DFlash2 block-diffusion drafter shipped as drafter.safetensors by
// syvai/edda-v0.2-large-hurtig. Hyperparameters come from drafter_config.json.
struct DrafterConfig {
int64_t layers = 5;
int64_t target_layers = 32;
int64_t block = 16;
int64_t conv_kernel = 2;
int64_t conv_group = 16;
int64_t selector_rank = 256;
int64_t selector_top_k = 16;
std::vector<int64_t> target_layer_ids;
};

struct EddaAssets {
assets::ResourceBundle resources;
std::shared_ptr<const assets::TensorSource> source;
std::shared_ptr<const assets::TensorSource> drafter_source;
std::shared_ptr<tokenizers::LlamaBpeTokenizer> tokenizer;
modules::WhisperEmbeddingConfig encoder;
int64_t decoder_layers = 0, decoder_heads = 0, decoder_ffn = 0;
int64_t vocabulary_size = 0, max_positions = 0;
int32_t eos = 0, start_of_transcript = 0, transcribe = 0, no_timestamps = 0;
std::vector<int32_t> suppress, begin_suppress;
std::vector<bool> special_tokens;
std::optional<DrafterConfig> drafter;
bool clean_up_tokenization_spaces = false;

std::vector<int32_t> prompt(const std::string & language) const;
std::string decode_text(const std::vector<int32_t> & tokens) const;
};

std::shared_ptr<const EddaAssets> load_assets(const std::filesystem::path & path);

struct DrafterConvWeights {
core::TensorValue base; // [2, kernel, hidden]: prepare and finish taps
core::TensorValue projection;
};

struct DrafterLayerWeights {
modules::NormWeights attention_norm, cross_norm, mlp_norm;
core::TensorValue qkv_weight, qkv_bias, out_weight, out_bias;
modules::AttentionWeights cross_attention;
modules::FeedForwardWeights mlp;
DrafterConvWeights attention_conv, mlp_conv;
};

struct DrafterWeights {
std::vector<DrafterLayerWeights> layers;
core::TensorValue context_fc, mask_embedding, selector_projection;
modules::NormWeights context_norm, final_norm;
// The candidate selector walks the block on the host, so its codebooks stay in host memory.
std::vector<uint16_t> predecessor_codebook, successor_codebook;
};

struct TargetLayerWeights {
modules::NormWeights attention_norm, cross_norm, mlp_norm;
core::TensorValue qkv_weight, qkv_bias, out_weight, out_bias;
modules::AttentionWeights cross_attention;
modules::FeedForwardWeights mlp;
};

struct EncoderLayerWeights {
modules::NormWeights attention_norm, mlp_norm;
core::TensorValue qkv_weight, qkv_bias, out_weight, out_bias;
modules::FeedForwardWeights mlp;
};

struct EncoderWeights {
modules::Conv1dWeights conv1, conv2;
core::TensorValue positions;
std::vector<EncoderLayerWeights> layers;
modules::NormWeights final_norm;
};

struct EddaWeights {
std::unique_ptr<core::BackendWeightStore> store;
EncoderWeights encoder;
std::vector<TargetLayerWeights> decoder;
core::TensorValue embedding, positions;
modules::NormWeights decoder_norm;
std::optional<DrafterWeights> drafter;
};

std::unique_ptr<EddaWeights> load_weights(const EddaAssets & assets, core::ExecutionContext & execution,
assets::TensorStorageType type, bool with_drafter);

struct DecodeOptions {
int64_t max_tokens = 440;
bool speculative = true;
bool use_selector = true;
int64_t block = 16;
int64_t num_beams = 1;
float length_penalty = 1.0f;
};

struct DecodeResult {
std::vector<int32_t> tokens; // generated tokens, without the prompt or end-of-text
bool reached_eos = false;
int64_t target_passes = 0;
int64_t drafted = 0, accepted = 0;
double encoder_ms = 0.0, decoder_ms = 0.0, drafter_ms = 0.0;
};

class EddaRuntime {
public:
EddaRuntime(const EddaAssets & assets, const EddaWeights & weights, core::ExecutionContext & execution);
~EddaRuntime();
DecodeResult transcribe(const std::vector<float> & samples, const std::vector<int32_t> & prompt,
const DecodeOptions & options);

private:
struct State;
struct TargetGraph;
struct DrafterGraph;
TargetGraph & target_graph(int64_t tokens, bool beam = false);
DrafterGraph & drafter_graph(int64_t block);
std::vector<int32_t> select_path(const DrafterGraph & graph, int32_t anchor) const;
void ensure_beam_cache(int64_t rows);
std::vector<int32_t> beam_search(const std::vector<int32_t> & prompt, const DecodeOptions & options,
DecodeResult & result);

const EddaAssets & assets_;
const EddaWeights & weights_;
core::ExecutionContext & execution_;
audio::WhisperLogMelExtractor frontend_;
std::unique_ptr<State> state_;
std::vector<std::unique_ptr<TargetGraph>> target_graphs_, beam_graphs_;
std::vector<std::unique_ptr<DrafterGraph>> drafter_graphs_;
};

std::shared_ptr<runtime::IVoiceModelLoader> make_edda_asr_loader();

} // namespace engine::community_models::edda_asr
1 change: 1 addition & 0 deletions include/engine/framework/assets/resource_bundle.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class ResourceBundle {
void set_model_directory(std::filesystem::path path);
[[nodiscard]] const std::filesystem::path & model_directory() const noexcept;
[[nodiscard]] bool has_file(std::string_view id) const noexcept;
[[nodiscard]] bool has_tensor_source(std::string_view id) const noexcept;
[[nodiscard]] const std::filesystem::path * find_file(std::string_view id) const noexcept;
[[nodiscard]] const std::filesystem::path & require_file(std::string_view id) const;
[[nodiscard]] std::vector<ResourceFile> files() const;
Expand Down
Loading
Loading