Automatic Speech Recognition
GGUF
jais
ifm
arabic
arabic-dialects
speech-to-text
code-switching
diacritization
language-identification
open-weights
custom_code
llama.cpp
imatrix
conversational
Instructions to use IFM/Jais-ASR-GGUF with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use IFM/Jais-ASR-GGUF with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf IFM/Jais-ASR-GGUF:Q4_K_M # Run inference directly in the terminal: llama cli -hf IFM/Jais-ASR-GGUF:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf IFM/Jais-ASR-GGUF:Q4_K_M # Run inference directly in the terminal: llama cli -hf IFM/Jais-ASR-GGUF:Q4_K_M
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf IFM/Jais-ASR-GGUF:Q4_K_M # Run inference directly in the terminal: ./llama-cli -hf IFM/Jais-ASR-GGUF:Q4_K_M
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf IFM/Jais-ASR-GGUF:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama-cli -hf IFM/Jais-ASR-GGUF:Q4_K_M
Use Docker
docker model run hf.co/IFM/Jais-ASR-GGUF:Q4_K_M
- LM Studio
- Jan
- Ollama
How to use IFM/Jais-ASR-GGUF with Ollama:
ollama run hf.co/IFM/Jais-ASR-GGUF:Q4_K_M
- Unsloth Desktop
- Docker Model Runner
How to use IFM/Jais-ASR-GGUF with Docker Model Runner:
docker model run hf.co/IFM/Jais-ASR-GGUF:Q4_K_M
- Lemonade
How to use IFM/Jais-ASR-GGUF with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull IFM/Jais-ASR-GGUF:Q4_K_M
Run and chat with the model
lemonade run user.Jais-ASR-GGUF-Q4_K_M
List all available models
lemonade list
- Atomic Chat
Download llama.cpp-jais-asr.patch from IFM/Jais-ASR-GGUF: direct link, hf CLI and curl.
- Browser
- Download file 53.9 kB
-
https://huggingface.co/IFM/Jais-ASR-GGUF/resolve/main/llama.cpp-jais-asr.patch
- Command line
-
hf download hf://IFM/Jais-ASR-GGUF/llama.cpp-jais-asr.patch
-
curl -L -o llama.cpp-jais-asr.patch https://huggingface.co/IFM/Jais-ASR-GGUF/resolve/main/llama.cpp-jais-asr.patch
53.9 kB
| diff --git a/common/arg.cpp b/common/arg.cpp | |
| index e346863..525c857 100644 | |
| --- a/common/arg.cpp | |
| +++ b/common/arg.cpp | |
| #include "llama.h" | |
| #include "log.h" | |
| #include "sampling.h" | |
| +#include "jais-language.h" | |
| #include "speculative.h" | |
| #include "preset.h" | |
| static bool common_params_parse_ex(int argc, char ** argv, common_params_context | |
| const std::string arg_prefix = "--"; | |
| std::string arg = argv[i]; | |
| + const bool inline_language = arg.compare(0, 11, "--language=") == 0 || | |
| + arg.compare(0, 16, "--jais-language=") == 0; | |
| + std::string language_value; | |
| + if (inline_language) { | |
| + const auto equals = arg.find('='); | |
| + language_value = arg.substr(equals + 1); | |
| + arg.resize(equals); | |
| + } | |
| if (arg.compare(0, arg_prefix.size(), arg_prefix) == 0) { | |
| std::replace(arg.begin(), arg.end(), '_', '-'); | |
| } | |
| static bool common_params_parse_ex(int argc, char ** argv, common_params_context | |
| } | |
| // arg with single value | |
| - check_arg(i); | |
| - std::string val = argv[++i]; | |
| + std::string val; | |
| + if (inline_language) { | |
| + val = language_value; | |
| + } else { | |
| + check_arg(i); | |
| + val = argv[++i]; | |
| + } | |
| if (opt.handler_int) { | |
| opt.handler_int(params, std::stoi(val)); | |
| continue; | |
| common_params_context common_params_parser_init(common_params & params, llama_ex | |
| params.sampling.typ_p = std::stof(value); | |
| } | |
| ).set_sampling()); | |
| + add_opt(common_arg( | |
| + {"--language", "--jais-language"}, "TAG", | |
| + "Jais-ASR language pin (case-insensitive: ar, EG, en, or comma-separated tags; empty disables)", | |
| + [](common_params & params, const std::string & value) { | |
| + common_jais_language_pin pin; | |
| + pin.configure(value); | |
| + params.sampling.jais_language = value; | |
| + } | |
| + ).set_sampling()); | |
| add_opt(common_arg( | |
| {"--repeat-last-n"}, "N", | |
| string_format("last n tokens to consider for penalize (default: %d, 0 = disabled)", params.sampling.penalty_last_n), | |
| diff --git a/common/chat-diff-analyzer.cpp b/common/chat-diff-analyzer.cpp | |
| index a7e3705..0470524 100644 | |
| --- a/common/chat-diff-analyzer.cpp | |
| +++ b/common/chat-diff-analyzer.cpp | |
| static std::vector<std::function<void(const common_chat_template & tmpl, autopar | |
| LOG_DBG(ANSI_ORANGE "[Patch: old Qwen/Deepseek thinking template]\n" ANSI_RESET); | |
| } | |
| }, | |
| + // IFM K2-Horizon family (incl. Jais-ASR) - reasoning is wrapped in <ifm|think> ... </ifm|think> | |
| + [](const common_chat_template & tmpl, autoparser & analysis) -> void { | |
| + if (tmpl.src.find("<ifm|think>") != std::string::npos && | |
| + tmpl.src.find("</ifm|think>") != std::string::npos && | |
| + analysis.reasoning.mode == reasoning_mode::NONE) { | |
| + analysis.reasoning.mode = reasoning_mode::TAG_BASED; | |
| + analysis.reasoning.start = "<ifm|think>"; | |
| + analysis.reasoning.end = "</ifm|think>"; | |
| + analysis.preserved_tokens.push_back("<ifm|think>"); | |
| + analysis.preserved_tokens.push_back("</ifm|think>"); | |
| + LOG_DBG(ANSI_ORANGE "[Patch: IFM K2-Horizon thinking template]\n" ANSI_RESET); | |
| + } | |
| + }, | |
| // Granite 3.3, with separate reasoning and content markers | |
| [](const common_chat_template & tmpl, autoparser & analysis) -> void { | |
| if (tmpl.src.find("Write your thoughts between <think></think> and write your response between " | |
| diff --git a/common/chat.cpp b/common/chat.cpp | |
| index 743ecde..7c4c892 100644 | |
| --- a/common/chat.cpp | |
| +++ b/common/chat.cpp | |
| static bool is_lfm2_template(const std::string & src) { | |
| src.find("<|tool_list_end|>") != std::string::npos; | |
| } | |
| +// Jais-ASR: the user turn holds only the audio, and the language is chosen with | |
| +// language (pin) or language_prefill (template kwarg), never with prompt text | |
| +bool common_chat_templates_is_jais_asr(const struct common_chat_templates * tmpls) { | |
| + if (!tmpls || !tmpls->template_default) { | |
| + return false; | |
| + } | |
| + const auto & src = tmpls->template_default->source(); | |
| + return src.find("<|ifm|audio|>") != std::string::npos && | |
| + src.find("language_prefill") != std::string::npos; | |
| +} | |
| + | |
| common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates) { | |
| common_chat_prompt_preset asr_preset; | |
| asr_preset.system = ""; | |
| common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates | |
| asr_preset.user = ""; | |
| } | |
| + if (common_chat_templates_is_jais_asr(chat_templates)) { | |
| + asr_preset.system = ""; | |
| + asr_preset.user = ""; | |
| + } | |
| + | |
| return asr_preset; | |
| } | |
| static common_chat_params common_chat_templates_apply_legacy(const struct common | |
| common_chat_params common_chat_templates_apply(const struct common_chat_templates * tmpls, | |
| const struct common_chat_templates_inputs & inputs) { | |
| GGML_ASSERT(tmpls != nullptr); | |
| + if (common_chat_templates_is_jais_asr(tmpls) && | |
| + (inputs.enable_thinking || inputs.chat_template_kwargs.count("enable_thinking"))) { | |
| + // Jais-ASR is trained with an empty, closed thinking block and never closes an open | |
| + // one (the language pin would then never act), so thinking stays off for it | |
| + auto jais_inputs = inputs; | |
| + jais_inputs.enable_thinking = false; | |
| + jais_inputs.chat_template_kwargs.erase("enable_thinking"); | |
| + return common_chat_templates_apply(tmpls, jais_inputs); | |
| + } | |
| return inputs.use_jinja ? common_chat_templates_apply_jinja(tmpls, inputs) : | |
| common_chat_templates_apply_legacy(tmpls, inputs); | |
| } | |
| diff --git a/common/chat.h b/common/chat.h | |
| index cb39e34..5ad3254 100644 | |
| --- a/common/chat.h | |
| +++ b/common/chat.h | |
| struct common_chat_prompt_preset { | |
| common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates); | |
| +bool common_chat_templates_is_jais_asr(const struct common_chat_templates * tmpls); | |
| + | |
| common_chat_msg_delimiters common_chat_msg_delimiters_parse(const common_json & delimiters); | |
| diff --git a/common/common.cpp b/common/common.cpp | |
| index 347e8e9..e04310c 100644 | |
| --- a/common/common.cpp | |
| +++ b/common/common.cpp | |
| struct common_init_result::impl { | |
| std::vector<llama_sampler_seq_config> samplers_seq_config; | |
| }; | |
| +void common_params_apply_model_defaults(common_params & params) { | |
| + if (params.model.path.empty()) { | |
| + return; | |
| + } | |
| + struct gguf_init_params meta_gguf_params = { | |
| + /* .no_alloc = */ true, | |
| + /* .ctx = */ nullptr, | |
| + }; | |
| + struct gguf_context * ctx_gguf = gguf_init_from_file(params.model.path.c_str(), meta_gguf_params); | |
| + if (!ctx_gguf) { | |
| + return; | |
| + } | |
| + bool jais_asr = false; | |
| + const int64_t key = gguf_find_key(ctx_gguf, "tokenizer.chat_template"); | |
| + if (key >= 0 && gguf_get_kv_type(ctx_gguf, key) == GGUF_TYPE_STRING) { | |
| + const std::string tmpl = gguf_get_val_str(ctx_gguf, key); | |
| + jais_asr = tmpl.find("<|ifm|audio|>") != std::string::npos && tmpl.find("language_prefill") != std::string::npos; | |
| + } | |
| + gguf_free(ctx_gguf); | |
| + if (!jais_asr) { | |
| + return; | |
| + } | |
| + if (params.n_ctx == 0) { | |
| + params.n_ctx = 4096; // the evaluated context, instead of all the free GPU memory | |
| + } | |
| + if (params.flash_attn_type == LLAMA_FLASH_ATTN_TYPE_AUTO) { | |
| + params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED; // flash attention gave invalid outputs on AMD GPUs | |
| + } | |
| + // the model's generation config: greedy decoding, repetition penalty 1.05 over the whole sequence | |
| + auto & sparams = params.sampling; | |
| + if (!(sparams.user_sampling_config & common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_TEMP)) { | |
| + sparams.temp = 0.0f; | |
| + } | |
| + if (!(sparams.user_sampling_config & common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_PENALTY_REPEAT)) { | |
| + sparams.penalty_repeat = 1.05f; | |
| + } | |
| + if (!(sparams.user_sampling_config & common_params_sampling_config::COMMON_PARAMS_SAMPLING_CONFIG_PENALTY_LAST_N)) { | |
| + sparams.penalty_last_n = params.n_ctx; | |
| + } | |
| +} | |
| + | |
| common_init_result::common_init_result(common_params & params, bool model_only) : | |
| pimpl(new impl{}) { | |
| + common_params_apply_model_defaults(params); | |
| auto mparams = common_model_params_to_llama(params); | |
| auto cparams = common_context_params_to_llama(params); | |
| diff --git a/common/common.h b/common/common.h | |
| index a333f70..601cab0 100644 | |
| --- a/common/common.h | |
| +++ b/common/common.h | |
| inline bool common_grammar_needs_prefill(const common_grammar & g) { | |
| // sampling parameters | |
| struct common_params_sampling { | |
| + std::string jais_language; // empty: automatic detection; otherwise tag or comma-separated tags | |
| uint32_t seed = LLAMA_DEFAULT_SEED; // the seed used to initialize llama_sampler | |
| int32_t n_prev = 64; // number of previous tokens to remember | |
| using common_init_result_ptr = std::unique_ptr<common_init_result>; | |
| common_init_result_ptr common_init_from_params(common_params & params, bool model_only = false); | |
| +// Jais-ASR (detected from the chat template in the GGUF file): fill the settings the user left unset with | |
| +// the model's generation settings and the llama.cpp settings it was evaluated with | |
| +void common_params_apply_model_defaults(common_params & params); | |
| + | |
| struct llama_model_params common_model_params_to_llama ( common_params & params); | |
| struct llama_context_params common_context_params_to_llama(const common_params & params); | |
| diff --git a/common/jais-language.h b/common/jais-language.h | |
| new file mode 100644 | |
| index 0000000..78c2c3c | |
| --- /dev/null | |
| +++ b/common/jais-language.h | |
| +#pragma once | |
| + | |
| +#include "llama.h" | |
| + | |
| +#include <algorithm> | |
| +#include <array> | |
| +#include <cctype> | |
| +#include <cmath> | |
| +#include <stdexcept> | |
| +#include <string> | |
| +#include <vector> | |
| + | |
| +// Token inventory from Jais-ASR postprocess.py. Validate against the loaded vocabulary. | |
| +struct common_jais_tag { llama_token id; const char * name; }; | |
| +static const std::array<common_jais_tag, 24> common_jais_tags = {{{ | |
| + 226606, "msa"}, {272, "en"}, {2075, "fr"}, {273, "es"}, {286, "it"}, | |
| + {7646, "EG"}, {8048, "SA"}, {6101, "MA"}, {29354, "IQ"}, {23129, "JO"}, | |
| + {20056, "SY"}, {27526, "QA"}, {64489, "DZ"}, {23852, "LB"}, {5594, "OM"}, | |
| + {28472, "YE"}, {43852, "TN"}, {17604, "MR"}, {25893, "KW"}, {9373, "SD"}, | |
| + {12933, "LY"}, {5402, "AE"}, {4295, "PS"}, {41905, "BH"} | |
| +}}; | |
| + | |
| +// Same messages as the Python LanguagePin and the vLLM plugin. | |
| +static const char * const common_jais_pin_prefill_conflict = | |
| + "Use either language (pinning) or language_prefill, not both"; | |
| + | |
| +static inline bool common_jais_is_tag(llama_token id) { | |
| + for (const auto & tag : common_jais_tags) { | |
| + if (tag.id == id) { return true; } | |
| + } | |
| + return false; | |
| +} | |
| + | |
| +static inline const char * common_jais_tag_name(llama_token id) { | |
| + for (const auto & tag : common_jais_tags) { | |
| + if (tag.id == id) { return tag.name; } | |
| + } | |
| + return nullptr; | |
| +} | |
| + | |
| +// postprocess.has_prefilled_tag: the prompt ends with a complete "[tag]". | |
| +static inline bool common_jais_ends_with_tag(llama_token a, llama_token b, llama_token c) { | |
| + return a == 60 && common_jais_is_tag(b) && c == 62; | |
| +} | |
| + | |
| +struct common_jais_language_pin { | |
| + static constexpr llama_token open = 60, close = 62, refusal = 5126; | |
| + static constexpr llama_token think_open = 250029, think_close = 250030; | |
| + std::vector<llama_token> allowed; | |
| + std::array<llama_token, 3> prompt_tail = {{LLAMA_TOKEN_NULL, LLAMA_TOKEN_NULL, LLAMA_TOKEN_NULL}}; | |
| + bool thinking = false; | |
| + bool started = false; | |
| + int position = 0; | |
| + llama_token decision = LLAMA_TOKEN_NULL; | |
| + | |
| + bool enabled() const { return !allowed.empty(); } | |
| + bool contains(llama_token id) const { | |
| + return std::find(allowed.begin(), allowed.end(), id) != allowed.end(); | |
| + } | |
| + static bool is_tag(llama_token id) { return common_jais_is_tag(id); } | |
| + void reset() { | |
| + prompt_tail = {{LLAMA_TOKEN_NULL, LLAMA_TOKEN_NULL, LLAMA_TOKEN_NULL}}; | |
| + thinking = false; | |
| + started = false; | |
| + position = 0; | |
| + decision = LLAMA_TOKEN_NULL; | |
| + } | |
| + void configure(const std::string & language) { | |
| + allowed.clear(); | |
| + reset(); | |
| + if (language.empty()) { return; } | |
| + size_t begin = 0; | |
| + do { | |
| + const auto end = language.find(',', begin); | |
| + auto s = language.substr(begin, end == std::string::npos ? end : end - begin); | |
| + const auto first = s.find_first_not_of(" \t\r\n"); | |
| + const auto last = s.find_last_not_of(" \t\r\n"); | |
| + s = first == std::string::npos ? "" : s.substr(first, last - first + 1); | |
| + auto iso = s; | |
| + for (auto & c : iso) { c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); } | |
| + bool found = false; | |
| + for (size_t i = 0; i < common_jais_tags.size(); ++i) { | |
| + const auto & tag = common_jais_tags[i]; | |
| + std::string canonical = tag.name; | |
| + for (auto & c : canonical) { c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); } | |
| + if ((iso == "ar" && (i == 0 || i >= 5)) || iso == canonical) { | |
| + if (!contains(tag.id)) { allowed.push_back(tag.id); } | |
| + found = true; | |
| + } | |
| + } | |
| + if (!found) { throw std::invalid_argument("unknown Jais-ASR language: " + s); } | |
| + if (end == std::string::npos) { break; } | |
| + begin = end + 1; | |
| + } while (true); | |
| + } | |
| + bool prefilled() const { | |
| + return common_jais_ends_with_tag(prompt_tail[0], prompt_tail[1], prompt_tail[2]); | |
| + } | |
| + void accept(llama_token id, bool generated) { | |
| + if (!enabled()) { return; } | |
| + if (!generated) { | |
| + if (id == think_open) { thinking = true; } | |
| + if (id == think_close) { thinking = false; } | |
| + prompt_tail = {{prompt_tail[1], prompt_tail[2], id}}; | |
| + return; | |
| + } | |
| + if (!started && prefilled()) { position = 3; } | |
| + started = true; | |
| + if (thinking) { | |
| + if (id == think_close) { thinking = false; } | |
| + return; | |
| + } | |
| + if (position == 0 && id != open) { position = 3; return; } | |
| + if (position == 1) { decision = id; } | |
| + if (position < 3) { ++position; } | |
| + } | |
| + template <typename Score> | |
| + llama_token select(Score score) const { | |
| + if (!enabled() || thinking || position >= 3) { return LLAMA_TOKEN_NULL; } | |
| + if (!started && prefilled()) { return LLAMA_TOKEN_NULL; } | |
| + llama_token selected = LLAMA_TOKEN_NULL; | |
| + if (position == 0) { | |
| + selected = open; | |
| + } else if (position == 2) { | |
| + if (contains(decision)) { selected = close; } | |
| + } else { | |
| + auto best = common_jais_tags.front().id; | |
| + for (const auto & tag : common_jais_tags) { | |
| + if (score(tag.id) > score(best)) { best = tag.id; } | |
| + } | |
| + // Refusal competes against ALL tags before the caller's restriction. | |
| + if (score(refusal) > score(best)) { | |
| + selected = refusal; | |
| + } else { | |
| + selected = allowed.front(); | |
| + for (const auto id : allowed) { | |
| + if (score(id) > score(selected)) { selected = id; } | |
| + } | |
| + } | |
| + } | |
| + if (selected != LLAMA_TOKEN_NULL && !std::isfinite(score(selected))) { | |
| + throw std::runtime_error("Jais-ASR pin selected a token without a finite score"); | |
| + } | |
| + return selected; | |
| + } | |
| +}; | |
| + | |
| +// language_prefill takes exactly one tag: "en" or "EG" work, a family such as "ar" does not. | |
| +static inline std::string common_jais_prefill_tag(const std::string & language) { | |
| + common_jais_language_pin pin; | |
| + pin.configure(language); | |
| + if (pin.allowed.size() != 1) { | |
| + throw std::invalid_argument("language_prefill must resolve to one tag; use language for a family"); | |
| + } | |
| + return common_jais_tag_name(pin.allowed.front()); | |
| +} | |
| + | |
| +// Two specs name the same tags in the same order ("ar" and the 20 Arabic tags do). | |
| +static inline bool common_jais_same_language(const std::string & a, const std::string & b) { | |
| + common_jais_language_pin pa; | |
| + common_jais_language_pin pb; | |
| + pa.configure(a); | |
| + pb.configure(b); | |
| + return pa.allowed == pb.allowed; | |
| +} | |
| diff --git a/common/sampling.cpp b/common/sampling.cpp | |
| index 06dea1e..a2eb6da 100644 | |
| --- a/common/sampling.cpp | |
| +++ b/common/sampling.cpp | |
| #include "sampling.h" | |
| +#include "jais-language.h" | |
| #include "common.h" | |
| #include "fit.h" | |
| struct common_sampler { | |
| std::vector<llama_token_data> cur; | |
| llama_token_data_array cur_p; | |
| + common_jais_language_pin jais_pin; | |
| void reset() { | |
| prev.clear(); | |
| + jais_pin.reset(); | |
| llama_sampler_reset(chain); | |
| } | |
| struct common_sampler * common_sampler_init( | |
| throw std::invalid_argument("penalty_present must be finite"); | |
| } | |
| const llama_vocab * vocab = llama_model_get_vocab(model); | |
| + common_jais_language_pin jais_pin; | |
| + jais_pin.configure(params.jais_language); | |
| + if (jais_pin.enabled()) { | |
| + if (!common_grammar_value(params.grammar).empty() || !params.logit_bias.empty()) { | |
| + throw std::invalid_argument("Jais-ASR pinning cannot be combined with grammar or logit bias"); | |
| + } | |
| + auto verify = [&](const std::string & text, const llama_tokens & expected) { | |
| + if (common_tokenize(vocab, text, false, true) != expected) { | |
| + throw std::invalid_argument("Jais-ASR tokenizer mismatch for " + text); | |
| + } | |
| + }; | |
| + for (const auto & tag : common_jais_tags) { | |
| + verify("[" + std::string(tag.name) + "]", {60, tag.id, 62}); | |
| + } | |
| + verify("[IFM-NoTranscript]", {60, 5126, 46, 131126, 195862, 62}); | |
| + verify("<ifm|think>", {250029}); | |
| + verify("</ifm|think>", {250030}); | |
| + params.backend_sampling = false; | |
| + } | |
| llama_sampler_chain_params lparams = llama_sampler_chain_default_params(); | |
| lparams.no_perf = params.no_perf; | |
| struct common_sampler * common_sampler_init( | |
| /* .prev = */ ring_buffer<llama_token>(std::max(32, params.n_prev)), | |
| /* .cur = */ {}, | |
| /* .cur_p = */ {}, | |
| + /* .jais_pin = */ jais_pin, | |
| }; | |
| return result; | |
| void common_sampler_accept(struct common_sampler * gsmpl, llama_token token, boo | |
| const auto tm = gsmpl->tm(); | |
| + gsmpl->jais_pin.accept(token, is_generated); | |
| + | |
| // grammar_should_apply() checks the reasoning budget state, so calculate this before we accept | |
| const auto accept_grammar = is_generated && grammar_should_apply(gsmpl); | |
| struct common_sampler * common_sampler_clone(common_sampler * gsmpl) { | |
| /* .prev = */ gsmpl->prev, | |
| /* .cur = */ gsmpl->cur, | |
| /* .cur_p = */ gsmpl->cur_p, | |
| + /* .jais_pin = */ gsmpl->jais_pin, | |
| }; | |
| } | |
| void common_sampler_copy(const common_sampler * src, common_sampler * dst) { | |
| llama_sampler_copy(src->rbudget, dst->rbudget); | |
| llama_sampler_copy(src->chain, dst->chain); | |
| + dst->jais_pin = src->jais_pin; | |
| dst->params = src->params; | |
| dst->prev = src->prev; | |
| dst->cur = src->cur; | |
| llama_token common_sampler_sample(struct common_sampler * gsmpl, struct llama_co | |
| gsmpl->set_logits(ctx, idx); | |
| + if (gsmpl->jais_pin.enabled()) { | |
| + const float * raw = llama_get_logits_ith(ctx, idx); | |
| + if (!raw || llama_get_sampled_token_ith(ctx, idx) != LLAMA_TOKEN_NULL) { | |
| + throw std::runtime_error("Jais-ASR pinning requires CPU sampling and full logits"); | |
| + } | |
| + const auto forced = gsmpl->jais_pin.select([&](llama_token token) { return raw[token]; }); | |
| + if (forced != LLAMA_TOKEN_NULL) { | |
| + for (size_t i = 0; i < cur_p.size; ++i) { | |
| + const bool keep = cur_p.data[i].id == forced; | |
| + cur_p.data[i].p = keep ? 1.0f : 0.0f; | |
| + if (keep) { cur_p.selected = i; } | |
| + else { cur_p.data[i].logit = -INFINITY; } | |
| + } | |
| + if (cur_p.selected < 0) { throw std::runtime_error("missing Jais-ASR pin candidate"); } | |
| + return forced; | |
| + } | |
| + } | |
| + | |
| // Check if a backend sampler has already sampled a token in which case we | |
| // return that token id directly. | |
| { | |
| diff --git a/conversion/__init__.py b/conversion/__init__.py | |
| index 648dd73..1355717 100644 | |
| --- a/conversion/__init__.py | |
| +++ b/conversion/__init__.py | |
| TEXT_MODEL_MAP: dict[str, str] = { | |
| "Qwen2VLModel": "qwenvl", | |
| "Qwen2_5OmniModel": "qwenvl", | |
| "Qwen2_5_VLForConditionalGeneration": "qwenvl", | |
| + "JaisASRForConditionalGeneration": "jais_asr", | |
| "Qwen3ASRForConditionalGeneration": "qwen3vl", | |
| "Qwen3ForCausalLM": "qwen", | |
| "Qwen3Model": "qwen", | |
| MMPROJ_MODEL_MAP: dict[str, str] = { | |
| "Qwen2VLModel": "qwenvl", | |
| "Qwen2_5OmniModel": "qwenvl", | |
| "Qwen2_5_VLForConditionalGeneration": "qwenvl", | |
| + "JaisASRForConditionalGeneration": "jais_asr", | |
| "Qwen3ASRForConditionalGeneration": "qwen3vl", | |
| "Qwen3OmniMoeForConditionalGeneration": "qwen3vl", | |
| "PocketTTSModel": "pockettts", | |
| diff --git a/conversion/jais_asr.py b/conversion/jais_asr.py | |
| new file mode 100644 | |
| index 0000000..69b1836 | |
| --- /dev/null | |
| +++ b/conversion/jais_asr.py | |
| +from __future__ import annotations | |
| + | |
| +from typing import Any, Callable, Iterable, TYPE_CHECKING | |
| + | |
| +if TYPE_CHECKING: | |
| + from torch import Tensor | |
| + | |
| +from .base import ModelBase, TextModel, gguf | |
| + | |
| +from .k2_horizon import K2HorizonModel | |
| +from .qwenvl import Qwen25AudioModel | |
| + | |
| + | |
| +# Jais-ASR = Qwen3-Omni audio tower + extra MLP projector + K2-Horizon text decoder. | |
| +# Checkpoint prefixes: model.audio_tower.*, model.embed_audio.*, model.language_model.*, lm_head.* | |
| + | |
| + | |
| +@ModelBase.register("JaisASRForConditionalGeneration") | |
| +@ModelBase.example("MBZUAI-Paris/Jais-ASR") | |
| +class JaisASRTextModel(K2HorizonModel): | |
| + model_arch = gguf.MODEL_ARCH.K2HORIZON | |
| + | |
| + # "model.language_model." is reduced to "model." by ModelBase.filter_tensors, and the | |
| + # audio tensors are dropped by TextModel.filter_tensors, so only set_vocab differs. | |
| + | |
| + # generation_config.json stops on this in addition to the tokenizer's eos token | |
| + _EXTRA_STOP_TOKEN = "<|ifm|endoftext|>" | |
| + | |
| + def set_vocab(self): | |
| + # skip K2HorizonModel.set_vocab: it replaces the chat template with the | |
| + # K2-Horizon one, but Jais-ASR ships its own audio chat template | |
| + TextModel.set_vocab(self) | |
| + | |
| + # llama.cpp derives its end-of-generation set from the single eos token, so the | |
| + # second stop token would never terminate generation. Put it in the EOT slot. | |
| + tokens, _, _ = self.get_vocab_base() | |
| + self.gguf_writer.add_eot_token_id(tokens.index(self._EXTRA_STOP_TOKEN)) | |
| + | |
| + | |
| +@ModelBase.register("JaisASRForConditionalGeneration") | |
| +@ModelBase.example("MBZUAI-Paris/Jais-ASR") | |
| +class JaisASRMmprojModel(Qwen25AudioModel): | |
| + has_audio_encoder = True | |
| + has_vision_encoder = False | |
| + | |
| + # model.embed_audio.* -> the extra projector stage read by PROJECTOR_TYPE_JAISASR | |
| + _EMBED_AUDIO_MAP = { | |
| + "norm": gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.A_MM_NORM_PRE], | |
| + "linear_1": gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.A_MMPROJ].format(bid=3), | |
| + "linear_2": gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.A_MMPROJ].format(bid=4), | |
| + } | |
| + | |
| + def get_vision_config(self) -> dict[str, Any] | None: | |
| + return None | |
| + | |
| + def set_gguf_parameters(self): | |
| + super().set_gguf_parameters() | |
| + self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.JAISASR) | |
| + | |
| + @classmethod | |
| + def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None: | |
| + name, gen = item | |
| + | |
| + if name.startswith("model.audio_tower."): | |
| + # rename to the prefix the qwen3omni tensor mappings expect | |
| + return name.replace("model.audio_tower.", "audio_tower.", 1), gen | |
| + | |
| + if name.startswith("model.embed_audio."): | |
| + return item | |
| + | |
| + return None | |
| + | |
| + def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]: | |
| + if name.startswith("model.embed_audio."): | |
| + module, suffix = name[len("model.embed_audio."):].rsplit(".", 1) | |
| + yield (f"{self._EMBED_AUDIO_MAP[module]}.{suffix}", data_torch) | |
| + return | |
| + | |
| + if "conv2d" in name and name.endswith(".bias"): | |
| + # transform conv2d bias [n_embd] --> [1, 1, n_embd] | |
| + data_torch = data_torch.unsqueeze(-1).unsqueeze(-1) | |
| + | |
| + yield from Qwen25AudioModel.modify_tensors(self, data_torch, name, bid) | |
| diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py | |
| index 13d5567..7c4ddae 100644 | |
| --- a/gguf-py/gguf/constants.py | |
| +++ b/gguf-py/gguf/constants.py | |
| class VisionProjectorType: | |
| INTERNVL = "internvl" | |
| QWEN2A = "qwen2a" # audio | |
| QWEN3A = "qwen3a" # audio | |
| + JAISASR = "jais-asr" # audio (Qwen3-Omni audio tower + Jais-ASR MLP projector) | |
| GLMA = "glma" # audio | |
| QWEN25O = "qwen2.5o" # omni | |
| VOXTRAL = "voxtral" | |
| diff --git a/tests/test-arg-parser.cpp b/tests/test-arg-parser.cpp | |
| index e090763..823a4e6 100644 | |
| --- a/tests/test-arg-parser.cpp | |
| +++ b/tests/test-arg-parser.cpp | |
| static void test(void) { | |
| std::vector<std::string> argv; | |
| + for (const auto & example : {LLAMA_EXAMPLE_MTMD, LLAMA_EXAMPLE_SERVER}) { | |
| + for (const auto & option : {"--language=ar", "--language=eg", "--language=EG", "--language=eG", "--jais-language=ar", "--language="}) { | |
| + common_params language_params; | |
| + argv = {"binary_name", "-m", "jais-test.gguf", option}; | |
| + assert(common_params_parse(argv.size(), list_str_to_char(argv).data(), language_params, example)); | |
| + assert(language_params.sampling.jais_language == std::string(option).substr(std::string(option).find('=') + 1)); | |
| + } | |
| + } | |
| + | |
| + for (const auto & option : {"--language", "--jais-language"}) { | |
| + common_params language_params; | |
| + argv = {"binary_name", "-m", "jais-test.gguf", option, "eG"}; | |
| + assert(common_params_parse(argv.size(), list_str_to_char(argv).data(), language_params, LLAMA_EXAMPLE_MTMD)); | |
| + assert(language_params.sampling.jais_language == "eG"); | |
| + } | |
| + | |
| printf("test-arg-parser: test invalid usage\n\n"); | |
| // missing value | |
| diff --git a/tests/test-sampling.cpp b/tests/test-sampling.cpp | |
| index d727ab6..a53f479 100644 | |
| --- a/tests/test-sampling.cpp | |
| +++ b/tests/test-sampling.cpp | |
| #include "ggml.h" | |
| #include "llama.h" | |
| +#include "jais-language.h" | |
| #ifdef NDEBUG | |
| #undef NDEBUG | |
| #endif | |
| #include <algorithm> | |
| +#include <cassert> | |
| #include <cmath> | |
| #include <string> | |
| #include <vector> | |
| static void test_perf() { | |
| BENCH(llama_sampler_init_xtc (1.0f, 0.1f, 1, 1), data, 32); | |
| } | |
| +static void test_jais_language_pin() { | |
| + common_jais_language_pin pin; | |
| + for (const auto & tag : common_jais_tags) { | |
| + std::string lower = tag.name; | |
| + for (auto & c : lower) { c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); } | |
| + pin.configure(lower); | |
| + assert(pin.allowed == std::vector<llama_token>{tag.id}); | |
| + } | |
| + for (const auto & spelling : {"eg", "EG", "eG", "Eg"}) { | |
| + pin.configure(spelling); | |
| + assert(pin.allowed == std::vector<llama_token>{7646}); | |
| + } | |
| + pin.configure("AR"); | |
| + assert(pin.allowed.size() == 20); | |
| + pin.configure("eg,EG,eG,mA"); | |
| + assert((pin.allowed == std::vector<llama_token>{7646, 6101})); | |
| + pin.configure("MSA"); | |
| + assert(pin.allowed == std::vector<llama_token>{226606}); | |
| + pin.configure("EG"); | |
| + std::vector<float> logits(250031, -20.0f); | |
| + auto score = [&](llama_token id) { return logits[id]; }; | |
| + assert(pin.select(score) == 60); | |
| + pin.accept(60, true); | |
| + logits[272] = 9; logits[7646] = 6; logits[5126] = 7; | |
| + assert(pin.select(score) == 7646); // An unallowed tag still wins over refusal. | |
| + logits[5126] = 9; | |
| + assert(pin.select(score) == 7646); // Ties transcribe. | |
| + logits[5126] = 10; | |
| + assert(pin.select(score) == 5126); | |
| + auto refusal = pin; | |
| + refusal.accept(5126, true); | |
| + assert(refusal.select(score) == LLAMA_TOKEN_NULL); | |
| + pin.accept(7646, true); | |
| + assert(pin.select(score) == 62); | |
| + pin.accept(62, true); | |
| + assert(pin.select(score) == LLAMA_TOKEN_NULL); | |
| + pin.configure("ar"); | |
| + assert(pin.allowed.size() == 20); | |
| + pin.accept(60, true); | |
| + logits[5126] = 7; logits[8048] = 8; | |
| + assert(pin.select(score) == 8048); | |
| + pin.configure("EG,MA,EG"); | |
| + assert(pin.allowed.size() == 2); | |
| + pin.accept(250029, false); | |
| + assert(pin.select(score) == LLAMA_TOKEN_NULL); | |
| + pin.accept(123, true); | |
| + pin.accept(250030, true); | |
| + assert(pin.select(score) == 60); | |
| + pin.reset(); | |
| + assert(pin.select(score) == 60); | |
| + pin.accept(60, false); pin.accept(7646, false); pin.accept(62, false); | |
| + assert(pin.select(score) == LLAMA_TOKEN_NULL); | |
| + pin.accept(123, true); | |
| + assert(pin.select(score) == LLAMA_TOKEN_NULL); | |
| + bool threw = false; | |
| + pin.configure(""); | |
| + assert(pin.select(score) == LLAMA_TOKEN_NULL); | |
| + threw = false; | |
| + try { pin.configure("unknown"); } catch (const std::invalid_argument &) { threw = true; } | |
| + assert(threw); | |
| + pin.configure("en"); | |
| + logits[60] = -INFINITY; | |
| + threw = false; | |
| + try { pin.select(score); } catch (const std::runtime_error &) { threw = true; } | |
| + assert(threw); | |
| + | |
| + // prefill takes one tag; ISO codes are canonicalized like resolve_language | |
| + assert(common_jais_prefill_tag("EG") == "EG"); | |
| + assert(common_jais_prefill_tag(" EN ") == "en"); | |
| + assert(common_jais_prefill_tag("eg") == "EG"); | |
| + assert(common_jais_prefill_tag("eG") == "EG"); | |
| + for (const char * family : {"ar", "EG,MA"}) { | |
| + threw = false; | |
| + try { common_jais_prefill_tag(family); } catch (const std::invalid_argument &) { threw = true; } | |
| + assert(threw); | |
| + } | |
| + assert(common_jais_same_language("ar", "msa,EG,SA,MA,IQ,JO,SY,QA,DZ,LB,OM,YE,TN,MR,KW,SD,LY,AE,PS,BH")); | |
| + assert(!common_jais_same_language("EG,MA", "MA,EG")); | |
| + assert(common_jais_ends_with_tag(60, 7646, 62)); | |
| + assert(!common_jais_ends_with_tag(60, 5126, 62)); | |
| +} | |
| + | |
| int main(void) { | |
| + test_jais_language_pin(); | |
| ggml_time_init(); | |
| test_dist_singleton_rng(); | |
| diff --git a/tools/mtmd/clip-impl.h b/tools/mtmd/clip-impl.h | |
| index f604509..5e6ae15 100644 | |
| --- a/tools/mtmd/clip-impl.h | |
| +++ b/tools/mtmd/clip-impl.h | |
| enum projector_type { | |
| PROJECTOR_TYPE_LLAMA4, | |
| PROJECTOR_TYPE_QWEN2A, | |
| PROJECTOR_TYPE_QWEN3A, | |
| + PROJECTOR_TYPE_JAISASR, | |
| PROJECTOR_TYPE_GLMA, | |
| PROJECTOR_TYPE_QWEN25O, // will be replaced by QWEN2A or QWEN25VL depending on clip_ctx | |
| PROJECTOR_TYPE_VOXTRAL, | |
| static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = { | |
| { PROJECTOR_TYPE_LLAMA4, "llama4"}, | |
| { PROJECTOR_TYPE_QWEN2A, "qwen2a"}, | |
| { PROJECTOR_TYPE_QWEN3A, "qwen3a"}, | |
| + { PROJECTOR_TYPE_JAISASR, "jais-asr"}, | |
| { PROJECTOR_TYPE_GLMA, "glma"}, | |
| { PROJECTOR_TYPE_QWEN25O, "qwen2.5o"}, | |
| { PROJECTOR_TYPE_VOXTRAL, "voxtral"}, | |
| diff --git a/tools/mtmd/clip.cpp b/tools/mtmd/clip.cpp | |
| index 90de195..e83826c 100644 | |
| --- a/tools/mtmd/clip.cpp | |
| +++ b/tools/mtmd/clip.cpp | |
| static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const | |
| builder = std::make_unique<clip_graph_glm4v>(ctx, img); | |
| } break; | |
| case PROJECTOR_TYPE_QWEN3A: | |
| + case PROJECTOR_TYPE_JAISASR: | |
| { | |
| builder = std::make_unique<clip_graph_qwen3a>(ctx, img); | |
| } break; | |
| struct clip_model_loader { | |
| case PROJECTOR_TYPE_ULTRAVOX: | |
| case PROJECTOR_TYPE_QWEN2A: | |
| case PROJECTOR_TYPE_QWEN3A: | |
| + case PROJECTOR_TYPE_JAISASR: | |
| case PROJECTOR_TYPE_GLMA: | |
| case PROJECTOR_TYPE_VOXTRAL: | |
| case PROJECTOR_TYPE_MERALION: | |
| struct clip_model_loader { | |
| model.mm_fc_b = get_tensor(string_format(TN_MM_AUDIO_FC, "bias")); | |
| } break; | |
| case PROJECTOR_TYPE_QWEN3A: | |
| + case PROJECTOR_TYPE_JAISASR: | |
| { | |
| model.conv2d_1_w = get_tensor(string_format(TN_CONV2D, 1, "weight")); | |
| model.conv2d_1_b = get_tensor(string_format(TN_CONV2D, 1, "bias")); | |
| struct clip_model_loader { | |
| model.mm_1_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 1, "bias")); | |
| model.mm_2_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 2, "weight")); | |
| model.mm_2_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 2, "bias")); | |
| + if (model.proj_type == PROJECTOR_TYPE_JAISASR) { | |
| + // Jais-ASR adds LayerNorm -> Linear -> GELU -> Linear on top of the | |
| + // Qwen3-Omni audio tower, mapping audio output_dim -> LLM hidden size | |
| + model.mm_norm_pre_w = get_tensor(string_format(TN_MM_NORM_PRE, "weight")); | |
| + model.mm_norm_pre_b = get_tensor(string_format(TN_MM_NORM_PRE, "bias")); | |
| + model.mm_3_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 3, "weight")); | |
| + model.mm_3_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 3, "bias")); | |
| + model.mm_4_w = get_tensor(string_format(TN_MM_AUDIO_MLP, 4, "weight")); | |
| + model.mm_4_b = get_tensor(string_format(TN_MM_AUDIO_MLP, 4, "bias")); | |
| + } | |
| } break; | |
| case PROJECTOR_TYPE_MIMO_AUDIO: | |
| { | |
| int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) { | |
| } | |
| } break; | |
| case PROJECTOR_TYPE_QWEN3A: | |
| + case PROJECTOR_TYPE_JAISASR: | |
| { | |
| // chunk_size=100 frames --> 3x stride-2 conv2d --> 13 tokens per chunk | |
| const int chunk_size = 100; | |
| bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) { | |
| case PROJECTOR_TYPE_NEMOTRON_V2_VL: | |
| case PROJECTOR_TYPE_QWEN2A: | |
| case PROJECTOR_TYPE_QWEN3A: | |
| + case PROJECTOR_TYPE_JAISASR: | |
| case PROJECTOR_TYPE_GLMA: | |
| case PROJECTOR_TYPE_ULTRAVOX: | |
| case PROJECTOR_TYPE_LFM2: | |
| int clip_n_mmproj_embd(const struct clip_ctx * ctx) { | |
| return ctx->model.mm_fc_w->ne[1]; | |
| case PROJECTOR_TYPE_QWEN3A: | |
| return ctx->model.mm_2_w->ne[1]; | |
| + case PROJECTOR_TYPE_JAISASR: | |
| + return ctx->model.mm_4_w->ne[1]; // extra projector maps audio dim -> LLM hidden | |
| case PROJECTOR_TYPE_GLMA: | |
| case PROJECTOR_TYPE_LFM2: | |
| case PROJECTOR_TYPE_KIMIVL: | |
| diff --git a/tools/mtmd/models/qwen3a.cpp b/tools/mtmd/models/qwen3a.cpp | |
| index 4de9695..a898e9f 100644 | |
| --- a/tools/mtmd/models/qwen3a.cpp | |
| +++ b/tools/mtmd/models/qwen3a.cpp | |
| ggml_cgraph * clip_graph_qwen3a::build() { | |
| FFN_GELU_ERF, -1); | |
| cb(cur, "projected", -1); | |
| + // Jais-ASR: extra projector on top of the Qwen3-Omni tower | |
| + // (LayerNorm -> Linear -> GELU -> Linear), audio output_dim -> LLM hidden size | |
| + if (proj_type == PROJECTOR_TYPE_JAISASR) { | |
| + cur = build_norm(cur, model.mm_norm_pre_w, model.mm_norm_pre_b, NORM_TYPE_NORMAL, eps, -1); | |
| + cur = build_ffn(cur, | |
| + model.mm_3_w, model.mm_3_b, | |
| + nullptr, nullptr, | |
| + model.mm_4_w, model.mm_4_b, | |
| + FFN_GELU_ERF, -1); | |
| + cb(cur, "projected_jais", -1); | |
| + } | |
| + | |
| ggml_build_forward_expand(gf, cur); | |
| return gf; | |
| } | |
| diff --git a/tools/mtmd/mtmd-cli.cpp b/tools/mtmd/mtmd-cli.cpp | |
| index 97678c6..ddad415 100644 | |
| --- a/tools/mtmd/mtmd-cli.cpp | |
| +++ b/tools/mtmd/mtmd-cli.cpp | |
| #include "log.h" | |
| #include "common.h" | |
| #include "sampling.h" | |
| +#include "jais-language.h" | |
| #include "llama.h" | |
| #include "ggml.h" | |
| #include "console.h" | |
| struct mtmd_cli_context { | |
| common_chat_templates_ptr tmpls; | |
| std::vector<common_chat_msg> chat_history; | |
| bool use_jinja = false; | |
| + bool jais_pin_enabled = false; | |
| + bool jais_asr = false; | |
| // TODO: support for --system-prompt with /clear command | |
| // support for legacy templates (models not having EOT token) | |
| struct mtmd_cli_context { | |
| lctx = llama_init->context(); | |
| vocab = llama_model_get_vocab(model); | |
| smpl = common_sampler_init(model, params.sampling); | |
| + jais_pin_enabled = !params.sampling.jais_language.empty(); | |
| n_threads = params.cpuparams.n_threads; | |
| batch = llama_batch_init(1, 0, 1); // batch for next token generation | |
| n_batch = params.n_batch; | |
| struct mtmd_cli_context { | |
| } | |
| tmpls = common_chat_templates_init(model, params.chat_template); | |
| - use_jinja = params.use_jinja; | |
| + jais_asr = common_chat_templates_is_jais_asr(tmpls.get()); | |
| + use_jinja = params.use_jinja || jais_asr; // the Jais-ASR template needs the jinja engine | |
| chat_history.clear(); | |
| - LOG_INF("%s: chat template example:\n%s\n", __func__, common_chat_format_example(tmpls.get(), params.use_jinja, params.default_template_kwargs).c_str()); | |
| + LOG_INF("%s: chat template example:\n%s\n", __func__, common_chat_format_example(tmpls.get(), use_jinja, params.default_template_kwargs).c_str()); | |
| // load antiprompt tokens for legacy templates | |
| if (params.chat_template == "vicuna") { | |
| static int generate_response(mtmd_cli_context & ctx, int n_predict) { | |
| break; | |
| } | |
| - llama_token token_id = common_sampler_sample(ctx.smpl, ctx.lctx, -1); | |
| + llama_token token_id; | |
| + try { | |
| + token_id = common_sampler_sample(ctx.smpl, ctx.lctx, -1); | |
| + } catch (const std::exception & e) { | |
| + // e.g. the Jais-ASR language pin found no finite score: report it like llama-server does | |
| + LOG_ERR("\n%s: %s\n", __func__, e.what()); | |
| + return 1; | |
| + } | |
| generated_tokens.push_back(token_id); | |
| common_sampler_accept(ctx.smpl, token_id, true); | |
| static int eval_message(mtmd_cli_context & ctx, common_chat_msg & msg) { | |
| ctx.bitmaps.entries.clear(); | |
| ctx.videos.clear(); | |
| + if (ctx.jais_pin_enabled || ctx.jais_asr) { | |
| + // like llama-server: the prompt text goes through the sampler, so the repeat penalty | |
| + // sees the same history and the pin sees the prompt | |
| + common_sampler_reset(ctx.smpl); | |
| + llama_token tail[3] = {LLAMA_TOKEN_NULL, LLAMA_TOKEN_NULL, LLAMA_TOKEN_NULL}; | |
| + for (size_t i = 0; i < mtmd_input_chunks_size(chunks.ptr.get()); ++i) { | |
| + const auto * chunk = mtmd_input_chunks_get(chunks.ptr.get(), i); | |
| + if (mtmd_input_chunk_get_type(chunk) == MTMD_INPUT_CHUNK_TYPE_TEXT) { | |
| + size_t count = 0; | |
| + const auto * ids = mtmd_input_chunk_get_tokens_text(chunk, &count); | |
| + for (size_t j = 0; j < count; ++j) { | |
| + common_sampler_accept(ctx.smpl, ids[j], false); | |
| + tail[0] = tail[1]; | |
| + tail[1] = tail[2]; | |
| + tail[2] = ids[j]; | |
| + } | |
| + } | |
| + } | |
| + if (ctx.jais_pin_enabled && common_jais_ends_with_tag(tail[0], tail[1], tail[2])) { | |
| + LOG_ERR("%s\n", common_jais_pin_prefill_conflict); | |
| + return 1; | |
| + } | |
| + } | |
| + | |
| // batch encode all media chunks, then decode each | |
| size_t n_chunks = mtmd_input_chunks_size(chunks.ptr.get()); | |
| for (size_t i = 0; i < n_chunks; i++) { | |
| diff --git a/tools/mtmd/mtmd.cpp b/tools/mtmd/mtmd.cpp | |
| index 5b30618..9f7e305 100644 | |
| --- a/tools/mtmd/mtmd.cpp | |
| +++ b/tools/mtmd/mtmd.cpp | |
| struct mtmd_context { | |
| aud_end = "<|audio_end|>"; | |
| audio_preproc = std::make_unique<mtmd_audio_preprocessor_qwen3a>(ctx_a); | |
| } break; | |
| + case PROJECTOR_TYPE_JAISASR: | |
| + { | |
| + // Jais-ASR wraps the audio span with <audio> ... </audio> | |
| + aud_beg = "<audio>"; | |
| + aud_end = "</audio>"; | |
| + audio_preproc = std::make_unique<mtmd_audio_preprocessor_qwen3a>(ctx_a); | |
| + } break; | |
| case PROJECTOR_TYPE_VOXTRAL: | |
| { | |
| // [BEGIN_AUDIO] ... (embeddings) ... | |
| diff --git a/tools/server/server-chat.cpp b/tools/server/server-chat.cpp | |
| index a6fe3c6..ca402a5 100644 | |
| --- a/tools/server/server-chat.cpp | |
| +++ b/tools/server/server-chat.cpp | |
| json convert_transcriptions_to_chatcmpl( | |
| if (response_format != "json") { | |
| throw std::invalid_argument("Only 'json' response_format is supported for transcription"); | |
| } | |
| - const common_chat_prompt_preset preset = common_chat_get_asr_prompt(tmpls); | |
| - if (prompt.empty()) { | |
| - prompt = preset.user; | |
| - } | |
| - if (!language.empty()) { | |
| - prompt += string_format(" (language: %s)", language.c_str()); | |
| - } | |
| - prompt += get_media_marker(); | |
| - | |
| json messages = json::array(); | |
| - if (!preset.system.empty()) { | |
| - messages.push_back({{"role", "system"}, {"content", preset.system}}); | |
| + if (common_chat_templates_is_jais_asr(tmpls)) { | |
| + // Jais-ASR, as in its vLLM plugin: the user turn holds only the audio and `prompt` | |
| + // is the system turn. `language` and `language_prefill` stay in the body as pin | |
| + // and prefill controls, handled by oaicompat_chat_params_parse. | |
| + if (!prompt.empty()) { | |
| + messages.push_back({{"role", "system"}, {"content", prompt}}); | |
| + } | |
| + messages.push_back({{"role", "user"}, {"content", get_media_marker()}}); | |
| + } else { | |
| + const common_chat_prompt_preset preset = common_chat_get_asr_prompt(tmpls); | |
| + if (prompt.empty()) { | |
| + prompt = preset.user; | |
| + } | |
| + if (!language.empty()) { | |
| + prompt += string_format(" (language: %s)", language.c_str()); | |
| + } | |
| + prompt += get_media_marker(); | |
| + | |
| + if (!preset.system.empty()) { | |
| + messages.push_back({{"role", "system"}, {"content", preset.system}}); | |
| + } | |
| + messages.push_back({{"role", "user"}, {"content", prompt}}); | |
| } | |
| - messages.push_back({{"role", "user"}, {"content", prompt}}); | |
| json chatcmpl_body = inp_body; // copy all fields | |
| chatcmpl_body["messages"] = messages; | |
| diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp | |
| index c30955e..4edcd0a 100644 | |
| --- a/tools/server/server-common.cpp | |
| +++ b/tools/server/server-common.cpp | |
| #include "mtmd.h" | |
| #include "mtmd-helper.h" | |
| #include "chat.h" | |
| +#include "jais-language.h" | |
| #include "base64.hpp" | |
| #include "server-common.h" | |
| json oaicompat_chat_params_parse( | |
| throw std::invalid_argument("Expected 'content' to be a string or an array"); | |
| } | |
| + // Jais-ASR: the user turn is the audio alone. llama-cli adds its -p text after the | |
| + // audio, and "<__media__>" there is only a placeholder for the attached audio. | |
| + if (common_chat_templates_is_jais_asr(opt.tmpls.get())) { | |
| + bool has_audio = false; | |
| + for (const auto & p : content) { | |
| + has_audio = has_audio || json_value(p, "type", std::string()) == "input_audio"; | |
| + } | |
| + if (has_audio) { | |
| + json kept = json::array(); | |
| + for (const auto & p : content) { | |
| + if (json_value(p, "type", std::string()) != "text" || | |
| + json_value(p, "text", std::string()) != mtmd_default_marker()) { | |
| + kept.push_back(p); | |
| + } | |
| + } | |
| + content = kept; | |
| + } | |
| + } | |
| + | |
| for (auto & p : content) { | |
| std::string type = json_value(p, "type", std::string()); | |
| if (type == "image_url") { | |
| json oaicompat_chat_params_parse( | |
| } | |
| } | |
| + // Jais-ASR language controls, with the same contract as its vLLM plugin: | |
| + // language, jais_language or the `language` template kwarg -> generation-time pin | |
| + // language_prefill, top level or template kwarg -> "[tag]" written into the prompt | |
| + // Setting either control in the request replaces the server defaults for both. | |
| + if (common_chat_templates_is_jais_asr(opt.tmpls.get())) { | |
| + auto to_spec = [](const json & value) { | |
| + if (value.is_string()) { | |
| + return value.get<std::string>(); | |
| + } | |
| + std::string spec; | |
| + if (value.is_array() && !value.empty()) { | |
| + for (const auto & item : value) { | |
| + if (!item.is_string()) { | |
| + throw std::invalid_argument("language must be a string or a list of strings"); | |
| + } | |
| + spec += (spec.empty() ? "" : ",") + item.get<std::string>(); | |
| + } | |
| + return spec; | |
| + } | |
| + throw std::invalid_argument("language must be a string or a non-empty list of strings"); | |
| + }; | |
| + // the first value given wins; any other must name the same tags | |
| + auto choose = [&](const char * name, const std::vector<json> & values, std::string & chosen) { | |
| + bool found = false; | |
| + for (const auto & value : values) { | |
| + if (value.is_null()) { | |
| + continue; | |
| + } | |
| + const std::string spec = to_spec(value); | |
| + if (!found) { | |
| + chosen = spec; | |
| + found = true; | |
| + } else if (!common_jais_same_language(spec, chosen)) { | |
| + throw std::invalid_argument(string_format("Conflicting %s values in the request", name)); | |
| + } | |
| + } | |
| + return found; | |
| + }; | |
| + auto request_kwarg = [&](const char * key) { | |
| + return json_value(chat_template_kwargs_object, key, json()); | |
| + }; | |
| + auto default_kwarg = [&](const char * key) { | |
| + const auto it = opt.chat_template_kwargs.find(key); | |
| + return it == opt.chat_template_kwargs.end() ? json() : json::parse(it->second); | |
| + }; | |
| + | |
| + std::string pin; | |
| + std::string prefill; | |
| + bool has_pin = choose("language", | |
| + {json_value(body, "jais_language", json()), json_value(body, "language", json()), request_kwarg("language")}, pin); | |
| + bool has_prefill = choose("language_prefill", | |
| + {json_value(body, "language_prefill", json()), request_kwarg("language_prefill")}, prefill); | |
| + const bool explicit_control = has_pin || has_prefill; | |
| + if (has_pin && has_prefill) { | |
| + throw std::invalid_argument(common_jais_pin_prefill_conflict); | |
| + } | |
| + if (!explicit_control) { | |
| + has_pin = choose("language", {default_kwarg("language")}, pin); | |
| + has_prefill = choose("language_prefill", {default_kwarg("language_prefill")}, prefill); | |
| + } | |
| + | |
| + // the template rejects `language`: a pin cannot be written into a prompt | |
| + inputs.chat_template_kwargs.erase("language"); | |
| + inputs.chat_template_kwargs.erase("language_prefill"); | |
| + if (has_prefill) { | |
| + inputs.chat_template_kwargs["language_prefill"] = json(common_jais_prefill_tag(prefill)).dump(); | |
| + } | |
| + if (has_pin) { | |
| + common_jais_language_pin check; | |
| + check.configure(pin); | |
| + llama_params["jais_language"] = pin; | |
| + } else if (explicit_control) { | |
| + llama_params["jais_language"] = ""; // a requested prefill replaces --jais-language | |
| + } | |
| + } | |
| + | |
| inputs.force_pure_content = opt.force_pure_content; | |
| // Apply chat template to the list of messages | |
| diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp | |
| index f547735..f2c1dbe 100644 | |
| --- a/tools/server/server-context.cpp | |
| +++ b/tools/server/server-context.cpp | |
| #include "fit.h" | |
| #include "llama.h" | |
| #include "log.h" | |
| +#include "jais-language.h" | |
| #include "sampling.h" | |
| #include "speculative.h" | |
| #include "mtmd.h" | |
| struct server_slot { | |
| } | |
| bool can_speculate() const { | |
| - return !!spec; | |
| + return !!spec && (!task || task->params.sampling.jais_language.empty()); | |
| } | |
| void add_token(const completion_token_output & token) { | |
| private: | |
| const bool is_resume = sleeping; | |
| params_base = params; | |
| + common_params_apply_model_defaults(params_base); // Jais-ASR defaults, before anything reads them | |
| const auto output_limits = server_output_limits(params_base); | |
| params_base.n_outputs_max = output_limits.total; | |
| params_base.n_outputs_max_per_seq = output_limits.per_seq; | |
| std::unique_ptr<server_res_generator> server_routes::handle_completions_impl( | |
| meta->logit_bias_eog, | |
| data); | |
| + // Jais-ASR: a language pin and a prompt that already ends with "[tag]" are exclusive | |
| + if (!task.params.sampling.jais_language.empty() && task.tokens.size() >= 3) { | |
| + const size_t n = task.tokens.size(); | |
| + if (common_jais_ends_with_tag(task.tokens[n - 3], task.tokens[n - 2], task.tokens[n - 1])) { | |
| + throw std::invalid_argument(common_jais_pin_prefill_conflict); | |
| + } | |
| + } | |
| + | |
| task.params.message_spans = task.tokens.find_message_spans(delimiters); | |
| task.id_slot = json_value(data, "id_slot", -1); | |
| diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp | |
| index 64b9251..90756d2 100644 | |
| --- a/tools/server/server-schema.cpp | |
| +++ b/tools/server/server-schema.cpp | |
| #include "server-schema.h" | |
| +#include "jais-language.h" | |
| #include "json-schema-to-grammar.h" | |
| std::vector<std::unique_ptr<field>> make_llama_cmpl_schema(const common_params & | |
| ->set_hard_limits(0, INT32_MAX) | |
| ->set_desc("Last n tokens to consider for penalizing repetition (0 = disabled)")); | |
| + add((new field_str("jais_language")) | |
| + ->set_desc("Jais-ASR language pin: ar, a concrete tag, or comma-separated tags; empty disables (chat and transcription requests also accept `language`)") | |
| + ->set_handler([&](field_eval_context & ctx, const json & data) { | |
| + const auto language = data.at("jais_language").get<std::string>(); | |
| + common_jais_language_pin pin; | |
| + pin.configure(language); | |
| + ctx.params.sampling.jais_language = language; | |
| + })); | |
| + | |
| add((new field_num("repeat_penalty", params.sampling.penalty_repeat)) | |
| ->set_desc("Control the repetition of token sequences in the generated text (1.0 = disabled)")); | |
| diff --git a/tools/server/server-task.cpp b/tools/server/server-task.cpp | |
| index 0d3beb3..fac93f6 100644 | |
| --- a/tools/server/server-task.cpp | |
| +++ b/tools/server/server-task.cpp | |
| json task_params::to_json(bool only_metrics) const { | |
| {"typical_p", sampling.typ_p}, | |
| {"repeat_last_n", sampling.penalty_last_n}, | |
| {"repeat_penalty", sampling.penalty_repeat}, | |
| + {"jais_language", sampling.jais_language}, | |
| {"presence_penalty", sampling.penalty_present}, | |
| {"frequency_penalty", sampling.penalty_freq}, | |
| {"dry_multiplier", sampling.dry_multiplier}, | |
| json task_params::to_json(bool only_metrics) const { | |
| {"typical_p", sampling.typ_p}, | |
| {"repeat_last_n", sampling.penalty_last_n}, | |
| {"repeat_penalty", sampling.penalty_repeat}, | |
| + {"jais_language", sampling.jais_language}, | |
| {"presence_penalty", sampling.penalty_present}, | |
| {"frequency_penalty", sampling.penalty_freq}, | |
| {"dry_multiplier", sampling.dry_multiplier}, | |