Compare commits

...
Author SHA1 Message Date
Xuan-Son NguyenandPascal 5851a8e505 model, mtmd: drama-3 tts support (#31613)
* model, mtmd: drama-3 tts support

* clean up

* mtmd: drama3 helper stops at the codebook tower limit

---------

Co-authored-by: Pascal <admin@serveurperso.com>
2026-11-04 15:15:09 +01:00
Aleksander Grygier efbf6e80a5 ui: show text and page thumbnails for PDF attachments, stop endless conversion retries (#31468)
* fix(ui): show extracted text in PDF attachment preview

Assisted-by: Claude (Opus 5.5 Medium)

* feat(ui): show page thumbnails for PDF attachments sent as images

Assisted-by: Claude (Opus 5.5 Medium)

* ui: stop retrying PDF preview conversion after it fails

A failed PDF-to-image conversion or text extraction reset its loading
flag, which re-ran the effect and retried forever. Remember the failed
item for each view and only retry when the item changes, so the error
state stays on screen.

Add a PdfConversionError story that feeds a broken PDF.

Assisted-by: Claude (Opus 5.5 Medium)

* ui: keep the text of PDFs sent as images and tidy the PDF preview

Store the extracted text of a PDF sent as images in its content, so the
Text view reads it instead of parsing the PDF again; only messages stored
with the old placeholder extract it. The model still receives the images
only. Guard retries on the error state, as the item remounts per id, skip
an empty base64 payload, and share the status markup between views.

Assisted-by: Claude (Opus 5.5 Medium)
2026-11-04 15:46:51 +02:00
Masashi Yoshimura b393d73bb0 webgpu: return maxBufferSize from get_max_size and avoid whole-buffer bindings in memset/clear (#31348)
* Fix get_max_size and improve the memset shader

* Fix buffer_memset to  handle the buffer size more than max binding size.
2026-11-04 15:46:06 +02:00
Neo ZhangandSigbjørn Skjæret 00df7c22d9 sycl : fix docker issue (#31605)
* add oneDNN in build image

* Update .devops/intel.Dockerfile

Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>

---------

Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
2026-11-04 15:45:29 +02:00
Aaron Thomas 034e2535a9 gguf-py : validate every tokenizer merge pair, not only the first (#31631)
* gguf-py : validate every tokenizer merge pair, not only the first

The transformers 4.45 pair format was detected by checking merges[0],
then every element was converted unchecked: a non-pair element raised
a bare TypeError, and pairs with the wrong arity or non-string parts
were silently mangled into invalid merges.

Validate each element (2-string list) up front and raise the existing
"Unknown tokenizer merges format" error for anything else.

Assisted-by: Devin

* gguf-py : remove test_vocab.py per review
2026-11-04 14:42:39 +01:00
Adrien Gallouët 52e53a76df common : delete common_get_path_from_env() (#31444)
Signed-off-by: Adrien Gallouët <angt@huggingface.co>
2026-11-04 14:21:38 +01:00
Georgi Gerganov cd959bae99 llama.cpp : bump version to 0.9.0 (#31646)
* llama.cpp : bump version to 0.9.0

* authors : update
2026-11-04 14:06:26 +02:00
35 changed files with 1507 additions and 272 deletions
+3 -1
View File
@@ -64,7 +64,7 @@ RUN mkdir -p /app/full \
&& cp requirements.txt /app/full \
&& cp .devops/tools.sh /app/full/tools.sh
FROM docker.io/intel/oneapi-toolkit:$ONEAPI_VERSION AS base
FROM docker.io/intel/deep-learning-essentials:$ONEAPI_VERSION AS base
ARG BUILD_DATE=N/A
ARG APP_VERSION=N/A
@@ -79,6 +79,8 @@ LABEL org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.url=$IMAGE_URL \
org.opencontainers.image.source=$IMAGE_SOURCE
COPY --from=build /opt/intel/oneapi/dnnl /opt/intel/oneapi/dnnl
#Following versions are for multiple GPUs, since 26.x has known issue:
# https://github.com/ggml-org/llama.cpp/issues/21747,
# https://github.com/intel/compute-runtime/issues/921.
+26 -1
View File
@@ -1,4 +1,4 @@
# date: Wed Oct 28 12:38:26 EET 2026
# date: Wed Nov 4 12:03:31 EET 2026
# this file is auto-generated by scripts/gen-authors.sh
Нияз Гарифзянов <112617865+garrnizon@users.noreply.github.com>
@@ -48,6 +48,7 @@ Abheek Gulati <abheekg@hotmail.com>
abhijain1204fujitsu <139222713+abhijain1204fujitsu@users.noreply.github.com>
Abhijit Ramesh <abhijitramesh2k@gmail.com>
abhijitb11 <113058133+abhijitb11@users.noreply.github.com>
Abhijith Jajam <129675815+jajamabhijith@users.noreply.github.com>
Abhilash Majumder <30946547+abhilash1910@users.noreply.github.com>
Abhinay Krishna <abhinaykrishna60@gmail.com>
Abhiram <78226909+geckguy@users.noreply.github.com>
@@ -203,14 +204,17 @@ Ankit Khandelwal <ankk98@gmail.com>
Ankur Verma <31362771+ankurvdev@users.noreply.github.com>
anon998 <131767832+anon998@users.noreply.github.com>
Anri Lombard <anri.m.lombard@gmail.com>
Anthony Maltsev <77708730+anthonymaltsev@users.noreply.github.com>
Anthony Umfer <aumfer@gmail.com>
Anthony Van de Gejuchte <anthonyvdgent@gmail.com>
Anthony Wang <anthonywang903@gmail.com>
antichristHater <142441588+antichristHater@users.noreply.github.com>
Antoine Viallon <antoine@lesviallon.fr>
Anton I <inflyer@hotmail.com>
Anton Mitkov <anton_b_mitkov@abv.bg>
Anton Mitkov <anton.mitkov@codeplay.com>
Antonis Makropoulos <benuix@gmail.com>
anubhavfujitsu <Anubhav.Srivastava@fujitsu.com>
Anudit Nagar <nagaranudit@gmail.com>
Anuj Attri <anujattri01@gmail.com>
anujj <ajalota@nvidia.com>
@@ -293,6 +297,7 @@ Bernat Vadell <hounter.caza@gmail.com>
Bernhard M. Wiedemann <githubbmwprimary@lsmod.de>
Bert Wagner <github@bertwagner.com>
Bertay Eren <39909689+bertaye@users.noreply.github.com>
Bhargav Krish <bkrishnamurt@nvidia.com>
Bhavik Sharda <10757940+BLSharda@users.noreply.github.com>
bhubbb <79117352+bhubbb@users.noreply.github.com>
Bill Sideris <bill88t@feline.gr>
@@ -533,6 +538,7 @@ Don Mahurin <dmahurin@users.noreply.github.com>
Dong Won Kim <63934649+ddwkim@users.noreply.github.com>
Donghyeon Jeong <54725479+djeong20@users.noreply.github.com>
Dongliang Wei <121270393+wdl339@users.noreply.github.com>
Dongsheng Luo <luods1995@outlook.com>
Doomsdayrs <38189170+Doomsdayrs@users.noreply.github.com>
DooWoong Lee (David) <manics99@naver.com>
DorianRudolph <dorianrudo97@googlemail.com>
@@ -612,6 +618,7 @@ Faez Shakil <faez.shakil@gmail.com>
fairydreaming <166155368+fairydreaming@users.noreply.github.com>
Faisal Zaghloul <faisal.zaghloul@gmail.com>
Faisal Zaghloul <quic_fzaghlou@quicinc.com>
FamousMonsterr <famousmonster@ya.ru>
Fan Shupei <dymarkfan@outlook.com>
FantasyGmm <16450052+FantasyGmm@users.noreply.github.com>
fanyang <fanyang89@outlook.com>
@@ -654,6 +661,7 @@ Frederik Vogel <Schaltfehler@users.noreply.github.com>
Fredrik Hultin <noname@nurd.se>
fredzillman <fzillman@gmail.com>
frob <rick+github@frob.com.au>
frogscooper <frogscooper@gmail.com>
Frost-54 <91168148+Frost-54@users.noreply.github.com>
Frosty40 <newjordan@gmail.com>
Funtowicz Morgan <mfuntowicz@users.noreply.github.com>
@@ -694,6 +702,7 @@ Gilad S. <7817232+giladgd@users.noreply.github.com>
Giovanni Rivera <rivera.giovanni271@gmail.com>
github-actions[bot] <github-actions[bot]@users.noreply.github.com>
GittyBurstein <g0534163997@gmail.com>
Giulianno <lernexbusiness@gmail.com>
Giuseppe Scrivano <giuseppe@scrivano.org>
Giuseppe Scrivano <gscrivan@redhat.com>
GiviMAD <GiviMAD@users.noreply.github.com>
@@ -762,6 +771,7 @@ Henry147147 <44851451+Henry147147@users.noreply.github.com>
Herman Semenoff <GermanAizek@yandex.ru>
Herman Semenov <GermanAizek@yandex.ru>
Hesen Peng <hesen.peng@gmail.com>
hesiru <siruhe666@gmail.com>
hey-gm <heitorgm@outlook.com>
HighDoping <highdoping@gmail.com>
HimariO <dsfhe49854@gmail.com>
@@ -919,6 +929,7 @@ Jesse Ikonen <jesse.ikonen@gmail.com>
Jesse Jojo Johnson <williamsaintgeorge@gmail.com>
Jesse LaRose <jesse@taey.ai>
Jesse Posner <jesse.posner@gmail.com>
Jesse Symons <155509533+Torchit1@users.noreply.github.com>
Jesus Gulfo <jesdga95@gmail.com>
Jesus Talavera <145992175+jesus-talavera-ibm@users.noreply.github.com>
Jetson Tan <tanzongyouyi@outlook.com>
@@ -1071,6 +1082,7 @@ Kevin Liu <4396kevinliu@gmail.com>
Kevin Lo <kevlo@kevlo.org>
Kevin Pouget <kpouget@redhat.com>
Kevin Wang <kevmo314@gmail.com>
Keyu Zhu <inuebisu@outlook.com>
Khashayar Ghafouri <43180261+khashayarghafouri@users.noreply.github.com>
khimaros <me@khimaros.com>
Kilian Hu <90606809+kilian-hu@users.noreply.github.com>
@@ -1078,6 +1090,7 @@ Kilian Krampf <kilian@krampf.de>
kiltyj <kiltyj@gmail.com>
Kim S. <polydecay@users.noreply.github.com>
kimminsu <80271594+kimminsu38oo@users.noreply.github.com>
Kirill A. Korinsky <kirill@korins.ky>
KITAITI Makoto <KitaitiMakoto@gmail.com>
kiwi <122582483+kiwi142857@users.noreply.github.com>
klosax <131523366+klosax@users.noreply.github.com>
@@ -1136,6 +1149,7 @@ leejet <leejet714@gmail.com>
Leng Yue <lengyue@lengyue.me>
Lennart Austenfeld <53152202+l-austenfeld@users.noreply.github.com>
leo-pony <nengjunma@outlook.com>
Leon <118652405+LeON-Nie-code@users.noreply.github.com>
Leon Knauer <git@leonknauer.com>
Leonard Mosescu <tlemo@users.noreply.github.com>
leonardHONG <2695316095@qq.com>
@@ -1146,8 +1160,10 @@ lexasub <lexakopp2212@gmail.com>
lgai-exaone <exaonemodels@lgresearch.ai>
lhez <lih@qti.qualcomm.com>
lhez <quic_lih@quicinc.com>
Li Chuyang <1418587903@qq.com>
Li Pengzhan <151381994+Lpzhan931@users.noreply.github.com>
Li Tan <tanliboy@gmail.com>
li-lizhe <82501526+li-lizhe@users.noreply.github.com>
liangliangchang <lichang@amd.com>
liminfei-amd <91481003+liminfei-amd@users.noreply.github.com>
limitedAtonement <limitedAtonement@users.noreply.github.com>
@@ -1166,6 +1182,7 @@ LoganDark <github@logandark.mozmail.com>
Loïc Carrère <loic.carrere@gmail.com>
lon <114724657+longregen@users.noreply.github.com>
loonerin <132926317+loonerin@users.noreply.github.com>
Lorenzo Bona <lorenzobona@tuta.com>
losewayy <1134285612@qq.com>
LostRuins <39025047+LostRuins@users.noreply.github.com>
LostRuins Concedo <39025047+LostRuins@users.noreply.github.com>
@@ -1253,6 +1270,7 @@ Masaya, Kato <62578291+msy-kato@users.noreply.github.com>
mashdragon <122402293+mashdragon@users.noreply.github.com>
Mason Milburn <masonmilby@gmail.com>
MasterYi1024 <39848311+MasterYi1024@users.noreply.github.com>
Maternion <98753158+maternion@users.noreply.github.com>
Mateusz Charytoniuk <mateusz.charytoniuk@protonmail.com>
Matheus C. França <matheus-catarino@hotmail.com>
Matheus Gabriel Alves Silva <matheusgasource@gmail.com>
@@ -1350,6 +1368,7 @@ Miwa / Ensan <63481257+ensan-hcl@users.noreply.github.com>
miyan <1138989048@qq.com>
mj-shifu <77107165+mj-shifu@users.noreply.github.com>
mkoker <132301062+mkoker@users.noreply.github.com>
mmalpe-nv <mmalpe@nvidia.com>
mmyjona <jonathan.gonse@gmail.com>
mnehete32 <33429707+mnehete32@users.noreply.github.com>
Mohamed Elashri <git@elashri.com>
@@ -1368,6 +1387,7 @@ mtmcp <141645996+mtmcp@users.noreply.github.com>
muggle-stack <promuggle@qq.com>
Muhammad Saad <saadydev@gmail.com>
Muhammad Salem <salem.ebo@gmail.com>
MuoDoo <36472129+MuoDoo@users.noreply.github.com>
Murilo Santana <mvrilo@gmail.com>
Musab Gultekin <musabgultekin@users.noreply.github.com>
musoles <135031143+musoles@users.noreply.github.com>
@@ -1416,10 +1436,12 @@ Nicolas Budyn <16596345+nbud@users.noreply.github.com>
Nicolas Mowen <nickmowen213@gmail.com>
Nicolás Pérez <nicolas_perez@brown.edu>
Nicolò Scipione <nicolo.scipione@codeplay.com>
Nidhi <nidhisharma16635@gmail.com>
Nigel Bosch <pnigelb@gmail.com>
Nigel Jones <nigel.l.jones+git@gmail.com>
Nik Bogatyrev <50734723+notforu777@users.noreply.github.com>
Nikhil Jain <nikhil.jain0987@gmail.com>
Nikhil Sharma <150424968+nikhilfujitsu@users.noreply.github.com>
Nikita Gordeev <iamfobey@gmail.com>
Nikita Sarychev <42014488+sARY77@users.noreply.github.com>
Niklas Korz <niklas@niklaskorz.de>
@@ -1592,6 +1614,7 @@ Raman Shinde <raman.shinde15@gmail.com>
Rand Xie <randxiexyy29@gmail.com>
Randall Fitzgerald <randall@dasaku.net>
Random Fly <renfei8@live.cn>
randomized <286843573@qq.com>
rankaiyx <rankaiyx@foxmail.com>
rankaiyx <rankaiyx@rankaiyx.com>
RapidMark <32768622+RapidMark@users.noreply.github.com>
@@ -2009,6 +2032,7 @@ Tungsten842 <quantmint@protonmail.com>
Tushar <ditsuke@protonmail.com>
tv1wnd <55383215+tv1wnd@users.noreply.github.com>
tyronecai <tyronecai@163.com>
Uber <20715993+YagUber@users.noreply.github.com>
ubergarm <leimgrub@gmail.com>
ubik2 <ubik2@users.noreply.github.com>
UEXTM.com <84163508+uextm@users.noreply.github.com>
@@ -2242,4 +2266,5 @@ Zijun Yu <zijun.yu@intel.com>
zql <37731799+zqlcode@users.noreply.github.com>
zrm <trustiosity.zrm@gmail.com>
Zsapi <martin1.zsapka@gmail.com>
zyang-dev <267119621+zyang-dev@users.noreply.github.com>
zzzzwc <tiddar@foxmail.com>
+1 -1
View File
@@ -4,7 +4,7 @@ include(CheckIncludeFileCXX)
### llama.cpp version
set(LLAMA_VERSION_MAJOR 0)
set(LLAMA_VERSION_MINOR 8)
set(LLAMA_VERSION_MINOR 9)
set(LLAMA_VERSION_PATCH 0)
set(LLAMA_VERSION_BASE "${LLAMA_VERSION_MAJOR}.${LLAMA_VERSION_MINOR}.${LLAMA_VERSION_PATCH}")
+1 -1
View File
@@ -710,7 +710,7 @@ static void common_params_apply_system_config(common_params & params, llama_exam
std::vector<std::filesystem::path> paths;
#if defined(_WIN32)
const std::filesystem::path program_data = common_get_path_from_env("PROGRAMDATA");
const std::filesystem::path program_data = common_get_env("PROGRAMDATA");
if (!program_data.empty()) {
paths.push_back(program_data / "llama.cpp" / "config.ini");
}
+6 -17
View File
@@ -983,20 +983,9 @@ void common_set_env(const std::string & name, const std::string & value) {
#endif
}
std::filesystem::path common_get_path_from_env(const std::string & name) {
#if defined(_WIN32)
const std::wstring wname = utf8_to_wstring(name);
const wchar_t * wvalue = _wgetenv(wname.c_str());
return wvalue ? std::filesystem::path(wvalue) : std::filesystem::path();
#else
const char * value = std::getenv(name.c_str());
return value ? std::filesystem::path(value) : std::filesystem::path();
#endif
}
#if !defined(_WIN32)
static std::filesystem::path get_home_directory() {
std::filesystem::path home = common_get_path_from_env("HOME");
std::filesystem::path home = common_get_env("HOME");
if (!home.empty()) {
return home;
}
@@ -1009,19 +998,19 @@ static std::filesystem::path get_home_directory() {
#endif
std::filesystem::path fs_get_cache_directory() {
std::filesystem::path cache_directory = common_get_path_from_env("LLAMA_CACHE");
std::filesystem::path cache_directory = common_get_env("LLAMA_CACHE");
if (!cache_directory.empty()) {
return cache_directory;
}
#if defined(_WIN32)
cache_directory = common_get_path_from_env("LOCALAPPDATA");
cache_directory = common_get_env("LOCALAPPDATA");
if (cache_directory.empty()) {
throw std::runtime_error("Failed to find %LOCALAPPDATA% directory");
}
#elif defined(__APPLE__)
cache_directory = get_home_directory() / "Library/Caches";
#else
cache_directory = common_get_path_from_env("XDG_CACHE_HOME");
cache_directory = common_get_env("XDG_CACHE_HOME");
if (cache_directory.empty()) {
cache_directory = get_home_directory() / ".cache";
}
@@ -1032,12 +1021,12 @@ std::filesystem::path fs_get_cache_directory() {
std::filesystem::path fs_get_config_directory() {
std::filesystem::path config_directory;
#if defined(_WIN32)
config_directory = common_get_path_from_env("APPDATA");
config_directory = common_get_env("APPDATA");
if (config_directory.empty()) {
throw std::runtime_error("Failed to find %APPDATA% directory");
}
#else
config_directory = common_get_path_from_env("XDG_CONFIG_HOME");
config_directory = common_get_env("XDG_CONFIG_HOME");
if (config_directory.empty()) {
config_directory = get_home_directory() / ".config";
}
-3
View File
@@ -919,9 +919,6 @@ std::string wstring_to_utf8(const std::wstring & str);
std::string common_get_env(const std::string & name);
void common_set_env(const std::string & name, const std::string & value);
// reads a path from the environment, an unset variable gives an empty path
std::filesystem::path common_get_path_from_env(const std::string & name);
//
// Filesystem utils
//
+1 -1
View File
@@ -45,7 +45,7 @@ static fs::path get_cache_directory() {
{HOME_DIR, fs::path(".cache") / "huggingface" / "hub"}
};
for (const auto & entry : entries) {
if (fs::path base = common_get_path_from_env(entry.var); !base.empty()) {
if (fs::path base = common_get_env(entry.var); !base.empty()) {
return entry.path.empty() ? base : base / entry.path;
}
}
+2
View File
@@ -95,6 +95,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
"FalconForCausalLM": "falcon",
"FalconH1ForCausalLM": "falcon_h1",
"FalconMambaForCausalLM": "mamba",
"FishOmniModel": "drama3",
"GPT2LMHeadModel": "gpt2",
"GPTBigCodeForCausalLM": "starcoder",
"GPTNeoXForCausalLM": "gptneox",
@@ -346,6 +347,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
"DotsOCRForCausalLM": "dotsocr",
"EmbeddingGemma2Model": "gemma",
"Exaone4_5_ForConditionalGeneration": "exaone",
"FishOmniModel": "drama3",
"Gemma3ForConditionalGeneration": "gemma",
"Gemma3nForConditionalGeneration": "gemma",
"Gemma4ForConditionalGeneration": "gemma",
+1 -1
View File
@@ -494,7 +494,7 @@ class ModelBase:
s = self.model_tensors[name]
self.model_tensors[weight_name] = lambda w=w, s=s: dequant_bitnet(w(), s())
tensors_to_remove.append(name)
elif quant_method == "fp8":
elif quant_method in ("fp8", "bsgemm-moe"): # bsgemm-moe: drama-3, same block layout
block_size = quant_config.get("weight_block_size")
for name in self.model_tensors.keys():
if name.endswith("_scale_inv"):
+251
View File
@@ -0,0 +1,251 @@
from __future__ import annotations
from typing import Any, Callable, Iterable, TYPE_CHECKING
import torch
if TYPE_CHECKING:
from torch import Tensor
from .base import ModelBase, MmprojModel, gguf
from .qwen import Qwen3_5MoeTextModel
# Fish Audio Drama 3 (FishOmniModel)
# text model: Qwen3.5-MoE backbone, samples the main token of each frame
# mmproj: codebook tower and Fish DAC decoder (codec.pth)
_TOWER_SHARED = "audio_decoder_mtp_shared.layers."
_TOWER_RESIDUAL = "audio_decoder_mtp.layers."
@ModelBase.register("FishOmniModel")
@ModelBase.example("fishaudio/drama-3")
class Drama3TextModel(Qwen3_5MoeTextModel):
model_arch = gguf.MODEL_ARCH.QWEN35MOE
no_mtp = True # the checkpoint has no MTP head
@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
if item[0].startswith("audio_decoder"):
return None
return super().filter_tensors(item)
@ModelBase.register("FishOmniModel")
@ModelBase.example("fishaudio/drama-3")
class Drama3MmprojModel(MmprojModel):
has_vision_encoder = False
has_audio_encoder = False
_LAYER_MAP = {
"enorm.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_ENORM, True),
"hnorm.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_HNORM, True),
"eh_proj.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_EH_PROJ, False),
"semantic_embedding.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_BLK_EMBD, False),
"codebook_embedding.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_BLK_EMBD, False),
"final_layernorm.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_BLK_OUT_NORM, True),
"transformer_layer.attention_norm.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_NORM, True),
"transformer_layer.ffn_norm.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_FFN_NORM, True),
"transformer_layer.attention.q_norm.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_Q_NORM, True),
"transformer_layer.attention.k_norm.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_K_NORM, True),
"transformer_layer.attention.wo.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_ATTN_OUT, False),
"transformer_layer.feed_forward.w1.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_FFN_GATE, False),
"transformer_layer.feed_forward.w3.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_FFN_UP, False),
"transformer_layer.feed_forward.w2.weight": (gguf.MODEL_TENSOR.A_GEN_CODE_FFN_DOWN, False),
}
_heads: dict[int, Tensor]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._heads = {}
def get_audio_config(self) -> dict[str, Any] | None:
cfg = dict(self.global_config["audio_decoder_config"])
cfg["num_hidden_layers"] = cfg["shared_enrichment_layers"] + cfg["num_codebooks"] - 1
return cfg
def set_gguf_parameters(self):
cfg = self.hparams
self.gguf_writer.add_file_type(self.ftype)
self.gguf_writer.add_clip_has_gen_audio_encoder(True)
self.gguf_writer.add_clip_gen_audio_projector_type(gguf.VisionProjectorType.DRAMA3_GEN)
self.gguf_writer.add_gen_audio_projection_dim(self.n_embd_text)
self.gguf_writer.add_gen_audio_embedding_length(cfg["dim"])
self.gguf_writer.add_gen_audio_feed_forward_length(cfg["intermediate_size"])
self.gguf_writer.add_gen_audio_block_count(cfg["num_hidden_layers"])
self.gguf_writer.add_gen_audio_head_count(cfg["n_head"])
self.gguf_writer.add_gen_audio_head_count_kv(cfg["n_local_heads"])
self.gguf_writer.add_gen_audio_attention_layernorm_eps(cfg["norm_eps"])
# other tower and codec hparams are hardcoded in clip.cpp
@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name = item[0]
if name.startswith("audio_decoder") or name == "model.language_model.embed_tokens.weight":
return item
return None
def tensor_force_quant(self, name, new_name, bid, n_dims):
# conv1d/conv1d_dw kernels must be F16, ggml_conv_1d(_dw) has no BF16 path
if new_name.endswith(".weight") and (
new_name in ("a.gen.wav.dac.entry.weight", "a.gen.wav.dac.post_conv.weight")
or (".up.blk." in new_name and new_name.endswith(".dwconv.weight"))
or (".dac.blk." in new_name and (new_name.endswith(".conv1.weight") or new_name.endswith(".conv2.weight")))
):
return gguf.GGMLQuantizationType.F16
# keep ConvTranspose1d kernels and folded codebook tables in F32
if new_name.endswith(".conv.weight") and (".up.blk." in new_name or ".dac.blk." in new_name):
return gguf.GGMLQuantizationType.F32
if new_name.startswith("a.gen.wav.quant."):
return gguf.GGMLQuantizationType.F32
return super().tensor_force_quant(name, new_name, bid, n_dims)
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
T = gguf.MODEL_TENSOR
cfg = self.hparams
if name.startswith("a.gen.wav."):
yield (name, data_torch)
return
if name == "model.language_model.embed_tokens.weight":
# semantic band of the backbone embeddings
begin = self.global_config["semantic_start_token_id"]
yield (self.format_tensor_name(T.A_GEN_CODE_OUT_EMBD), data_torch[begin:begin + cfg["vocab_size"]])
return
if name == "audio_decoder.codebook_embeddings.weight":
n_cb = cfg["num_codebooks"]
yield (self.format_tensor_name(T.A_GEN_CODE_EMBD), data_torch.reshape(n_cb, -1, data_torch.shape[-1]))
return
if name.startswith("audio_decoder_mtp.heads."):
n_heads = cfg["num_codebooks"] - 1
self._heads[int(name.split(".")[2])] = data_torch
if len(self._heads) < n_heads:
return
yield (self.format_tensor_name(T.A_GEN_CODE_HEAD), torch.stack([self._heads.pop(i) for i in range(n_heads)]))
return
for prefix, offset in ((_TOWER_SHARED, 0), (_TOWER_RESIDUAL, cfg["shared_enrichment_layers"])):
if not name.startswith(prefix):
continue
idx, key = name[len(prefix):].split(".", 1)
il = int(idx) + offset
if key == "transformer_layer.attention.wqkv.weight":
# rows are [query | gate | key | value]
n_q = cfg["n_head"] * cfg["head_dim"]
n_kv = cfg["n_local_heads"] * cfg["head_dim"]
q, gate, k, v = data_torch.split([n_q, n_q, n_kv, n_kv], dim=0)
yield (self.format_tensor_name(T.A_GEN_CODE_ATTN_Q, il), q)
yield (self.format_tensor_name(T.A_GEN_CODE_ATTN_GATE, il), gate)
yield (self.format_tensor_name(T.A_GEN_CODE_ATTN_K, il), k)
yield (self.format_tensor_name(T.A_GEN_CODE_ATTN_V, il), v)
return
tensor, zero_centered = self._LAYER_MAP[key]
if zero_centered:
data_torch = data_torch.float() + 1
yield (self.format_tensor_name(tensor, il), data_torch)
return
raise ValueError(f"unexpected tensor {name}")
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
# only the DAC decoder, the encoder is for voice cloning (not supported yet)
sd = torch.load(self.dir_model / "codec.pth", map_location="cpu", mmap=True, weights_only=True)
sd = sd.get("state_dict", sd)
T = gguf.MODEL_TENSOR
def get(name: str) -> Tensor:
return sd[name].float()
def conv(prefix: str) -> Tensor:
# fold weight norm
if prefix + ".conv.weight" in sd:
return get(prefix + ".conv.weight")
g = get(prefix + ".conv.parametrizations.weight.original0")
v = get(prefix + ".conv.parametrizations.weight.original1")
return g * v / v.norm(dim=(1, 2), keepdim=True)
def quant_table(prefix: str) -> Tensor:
# fold out_proj into the codebook, so decoding is a row lookup
g = get(prefix + ".out_proj.weight_g")
v = get(prefix + ".out_proj.weight_v")
w = (g * v / v.norm(dim=(1, 2), keepdim=True)).squeeze(-1)
return get(prefix + ".codebook.weight") @ w.T + get(prefix + ".out_proj.bias")
def snake(alpha: Tensor) -> tuple[Tensor, Tensor]:
alpha = alpha.flatten()
return alpha, 1.0 / (alpha + 1e-9)
q = "quantizer."
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_FIRST_CB), quant_table(q + "semantic_quantizer.quantizers.0"))
n_rest = sum(1 for k in sd if k.startswith(q + "quantizer.quantizers.") and k.endswith(".codebook.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_QUANT_REST_CB),
torch.stack([quant_table(f"{q}quantizer.quantizers.{i}") for i in range(n_rest)]))
# post_module uses interleaved RoPE, permute q/k rows so NEOX RoPE gives the same result
tfm = q + "post_module."
n_head, d_head = 16, 64
order = torch.cat([torch.arange(0, d_head, 2), torch.arange(1, d_head, 2)])
def neox_rows(w: Tensor) -> Tensor:
return w.reshape(n_head, d_head, -1)[:, order].reshape(w.shape)
n_tfm = sum(1 for k in sd if k.startswith(tfm + "layers.") and k.endswith(".ffn_norm.weight"))
for il in range(n_tfm):
p = f"{tfm}layers.{il}."
wq, wk, wv = get(p + "attention.wqkv.weight").chunk(3, dim=0)
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_ATTN_Q, il), neox_rows(wq))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_ATTN_K, il), neox_rows(wk))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_ATTN_V, il), wv)
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_ATTN_OUT, il), get(p + "attention.wo.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_ATTN_NORM, il), get(p + "attention_norm.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_ATTN_SCALE, il), get(p + "attention_layer_scale.gamma"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_FFN_NORM, il), get(p + "ffn_norm.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_FFN_GATE, il), get(p + "feed_forward.w1.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_FFN_UP, il), get(p + "feed_forward.w3.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_FFN_DOWN, il), get(p + "feed_forward.w2.weight"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_FFN_SCALE, il), get(p + "ffn_layer_scale.gamma"))
yield (self.format_tensor_name(T.A_GEN_WAV_TFM_OUTPUT_NORM), get(tfm + "norm.weight"))
# upsample: 2x (ConvTranspose1d + ConvNeXt)
for il in range(2):
p = f"{q}upsample.{il}."
yield (self.format_tensor_name(T.A_GEN_WAV_UP_CONV, il, suffix=".weight"), conv(p + "0"))
yield (self.format_tensor_name(T.A_GEN_WAV_UP_CONV, il, suffix=".bias"), get(p + "0.conv.bias"))
yield (self.format_tensor_name(T.A_GEN_WAV_UP_DWCONV, il, suffix=".weight"), conv(p + "1.dwconv"))
yield (self.format_tensor_name(T.A_GEN_WAV_UP_DWCONV, il, suffix=".bias"), get(p + "1.dwconv.conv.bias"))
for key, tensor in (("norm", T.A_GEN_WAV_UP_NORM), ("pwconv1", T.A_GEN_WAV_UP_PW1), ("pwconv2", T.A_GEN_WAV_UP_PW2)):
yield (self.format_tensor_name(tensor, il, suffix=".weight"), get(f"{p}1.{key}.weight"))
yield (self.format_tensor_name(tensor, il, suffix=".bias"), get(f"{p}1.{key}.bias"))
yield (self.format_tensor_name(T.A_GEN_WAV_UP_GAMMA, il, suffix=""), get(p + "1.gamma"))
# DAC: entry conv, 4x (Snake, ConvTranspose1d, 3 residual units), Snake, post conv
d = "decoder.model."
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_ENTRY, suffix=".weight"), conv(d + "0"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_ENTRY, suffix=".bias"), get(d + "0.conv.bias"))
for bid in range(4):
p = f"{d}{bid + 1}.block."
a, b = snake(get(p + "0.alpha"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_UP_SNAKE, bid, suffix=".alpha"), a)
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_UP_SNAKE, bid, suffix=".beta"), b)
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_UP_CONV, bid, suffix=".weight"), conv(p + "1"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_UP_CONV, bid, suffix=".bias"), get(p + "1.conv.bias"))
for xid in range(3):
r = f"{p}{xid + 2}.block."
for act, tensor in (("0", T.A_GEN_WAV_DAC_RES_ACT1), ("2", T.A_GEN_WAV_DAC_RES_ACT2)):
a, b = snake(get(r + act + ".alpha"))
name = gguf.TENSOR_NAMES[tensor].format(bid=bid, xid=xid)
yield (name + ".alpha", a)
yield (name + ".beta", b)
for cv, tensor in (("1", T.A_GEN_WAV_DAC_RES_CONV1), ("3", T.A_GEN_WAV_DAC_RES_CONV2)):
name = gguf.TENSOR_NAMES[tensor].format(bid=bid, xid=xid)
yield (name + ".weight", conv(r + cv))
yield (name + ".bias", get(r + cv + ".conv.bias"))
a, b = snake(get(d + "5.alpha"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_POST_SNAKE, suffix=".alpha"), a)
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_POST_SNAKE, suffix=".beta"), b)
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_POST_CONV, suffix=".weight"), conv(d + "6"))
yield (self.format_tensor_name(T.A_GEN_WAV_DAC_POST_CONV, suffix=".bias"), get(d + "6.conv.bias"))
+45 -42
View File
@@ -182,7 +182,6 @@ struct webgpu_capabilities {
uint32_t subgroup_size = 0;
uint32_t min_subgroup_size = 0;
uint32_t max_subgroup_size = 0;
size_t memset_bytes_per_thread;
};
// Stores global webgpu members
@@ -256,8 +255,6 @@ struct webgpu_context_struct {
wgpu::ComputePassEncoder active_compute_pass;
bool batch_compute_passes = true;
size_t memset_bytes_per_thread;
#ifdef GGML_WEBGPU_GPU_PROFILE
// Profiling: per-shader GPU time in ms
std::unordered_map<std::string, double> shader_gpu_time_ms;
@@ -672,38 +669,54 @@ static void ggml_backend_webgpu_buffer_memset(webgpu_global_context & ctx,
size_t size) {
std::lock_guard<std::recursive_mutex> lock(ctx->mutex);
std::vector<uint32_t> params = { (uint32_t) offset, (uint32_t) size, value };
std::vector<wgpu::BindGroupEntry> entries = { ggml_webgpu_make_bind_group_entry(0, buf, 0, buf.GetSize()) };
size_t bytes_per_wg =
ctx->capabilities.limits.maxComputeInvocationsPerWorkgroup * ctx->capabilities.memset_bytes_per_thread;
uint32_t wg_x = CEIL_DIV(size + 3, bytes_per_wg);
const size_t max_bind_size = ctx->capabilities.limits.maxStorageBufferBindingSize;
const size_t max_wg_size = ctx->capabilities.limits.maxComputeInvocationsPerWorkgroup;
ctx->queue.WriteBuffer(ctx->memset_params_buf, 0, params.data(), params.size() * sizeof(uint32_t));
while (size > 0) {
const size_t align_offset = offset & ~(ctx->capabilities.limits.minStorageBufferOffsetAlignment - 1);
const size_t lead = offset - align_offset;
const size_t chunk_size = std::min(size, max_bind_size - lead);
const size_t bind_size = ROUNDUP_POW2(chunk_size + lead, WEBGPU_STORAGE_BUF_BINDING_MULT);
wgpu::BindGroupEntry params_entry = {};
params_entry.binding = 1;
params_entry.buffer = ctx->memset_params_buf;
params_entry.offset = 0;
params_entry.size = WEBGPU_PARAMS_BUF_SIZE_BYTES;
entries.push_back(params_entry);
std::vector<uint32_t> params = { (uint32_t) lead, (uint32_t) chunk_size, value };
std::vector<wgpu::BindGroupEntry> entries = { ggml_webgpu_make_bind_group_entry(0, buf, align_offset,
bind_size) };
uint32_t wg_x;
uint32_t wg_y;
uint32_t n_values = CEIL_DIV(chunk_size + lead, sizeof(uint32_t));
uint32_t total_wg = CEIL_DIV(n_values, max_wg_size);
compute_2d_workgroups(total_wg, ctx->capabilities.limits.maxComputeWorkgroupsPerDimension, wg_x, wg_y);
wgpu::BindGroupDescriptor bind_group_desc;
bind_group_desc.layout = ctx->memset_pipeline.pipeline.GetBindGroupLayout(0);
bind_group_desc.entryCount = entries.size();
bind_group_desc.entries = entries.data();
bind_group_desc.label = ctx->memset_pipeline.name.c_str();
wgpu::BindGroup bind_group = ctx->device.CreateBindGroup(&bind_group_desc);
ctx->queue.WriteBuffer(ctx->memset_params_buf, 0, params.data(), params.size() * sizeof(uint32_t));
wgpu::CommandEncoder encoder = ctx->device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
pass.SetPipeline(ctx->memset_pipeline.pipeline);
pass.SetBindGroup(0, bind_group);
pass.DispatchWorkgroups(wg_x, 1, 1);
pass.End();
wgpu::BindGroupEntry params_entry = {};
params_entry.binding = 1;
params_entry.buffer = ctx->memset_params_buf;
params_entry.offset = 0;
params_entry.size = WEBGPU_PARAMS_BUF_SIZE_BYTES;
entries.push_back(params_entry);
wgpu::CommandBuffer command = encoder.Finish();
std::vector<wgpu::CommandBuffer> commands = { command };
ctx->queue.Submit(commands.size(), commands.data());
wgpu::BindGroupDescriptor bind_group_desc;
bind_group_desc.layout = ctx->memset_pipeline.pipeline.GetBindGroupLayout(0);
bind_group_desc.entryCount = entries.size();
bind_group_desc.entries = entries.data();
bind_group_desc.label = ctx->memset_pipeline.name.c_str();
wgpu::BindGroup bind_group = ctx->device.CreateBindGroup(&bind_group_desc);
wgpu::CommandEncoder encoder = ctx->device.CreateCommandEncoder();
wgpu::ComputePassEncoder pass = encoder.BeginComputePass();
pass.SetPipeline(ctx->memset_pipeline.pipeline);
pass.SetBindGroup(0, bind_group);
pass.DispatchWorkgroups(wg_x, wg_y, 1);
pass.End();
wgpu::CommandBuffer command = encoder.Finish();
std::vector<wgpu::CommandBuffer> commands = { command };
ctx->queue.Submit(commands.size(), commands.data());
offset += chunk_size;
size -= chunk_size;
}
}
/** End WebGPU Actions */
@@ -4006,12 +4019,10 @@ static size_t ggml_backend_webgpu_buffer_type_get_alignment(ggml_backend_buffer_
return dev_ctx->webgpu_global_ctx->capabilities.limits.minStorageBufferOffsetAlignment;
}
// maxBufferSize might be larger, but you can't bind more than
// maxStorageBufferBindingSize to a single binding.
static size_t ggml_backend_webgpu_buffer_type_get_max_size(ggml_backend_buffer_type_t buft) {
ggml_backend_webgpu_device_context * dev_ctx =
static_cast<ggml_backend_webgpu_device_context *>(buft->device->context);
return dev_ctx->webgpu_global_ctx->capabilities.limits.maxStorageBufferBindingSize;
return dev_ctx->webgpu_global_ctx->capabilities.limits.maxBufferSize;
}
static size_t ggml_backend_webgpu_buffer_type_get_alloc_size(ggml_backend_buffer_type_t buft,
@@ -4176,17 +4187,9 @@ static ggml_guid_t ggml_backend_webgpu_guid(void) {
}
static void ggml_webgpu_init_memset_pipeline(webgpu_global_context & ctx) {
// we use the maximum workgroup size for the memset pipeline
size_t max_threads = ctx->capabilities.limits.maxComputeInvocationsPerWorkgroup *
ctx->capabilities.limits.maxComputeWorkgroupsPerDimension;
// Size the bytes_per_thread so that the largest buffer size can be handled
ctx->capabilities.memset_bytes_per_thread =
CEIL_DIV(ctx->capabilities.limits.maxStorageBufferBindingSize, max_threads);
std::vector<wgpu::ConstantEntry> constants(2);
std::vector<wgpu::ConstantEntry> constants(1);
constants[0].key = "wg_size";
constants[0].value = ctx->capabilities.limits.maxComputeInvocationsPerWorkgroup;
constants[1].key = "bytes_per_thread";
constants[1].value = ctx->capabilities.memset_bytes_per_thread;
ctx->memset_pipeline = ggml_webgpu_create_pipeline(ctx->device, wgsl_memset, "memset", constants);
}
+23 -20
View File
@@ -11,30 +11,33 @@ struct Params {
var<uniform> params: Params;
override wg_size: u32;
override bytes_per_thread: u32;
@compute @workgroup_size(wg_size)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let i = gid.x * bytes_per_thread;
fn main(@builtin(workgroup_id) wid: vec3<u32>,
@builtin(num_workgroups) num_wg: vec3<u32>,
@builtin(local_invocation_id) lid: vec3<u32>) {
let i = (wid.x + wid.y * num_wg.x) * wg_size + lid.x;
let start = params.offset;
let end = params.offset + params.size;
let id = (start >> 2u) + i;
let id_u32 = id * 4;
for (var j: u32 = 0u; j < bytes_per_thread; j += 4) {
let byte_index = start + i + j;
if (byte_index + 4 <= end) {
output_buffer[byte_index >> 2] = params.value;
} else {
// Handle tail (unaligned)
for (var k: u32 = 0; k < 4; k++) {
let idx = byte_index + k;
if (idx < end) {
let word_idx = idx >> 2;
let bit_offset = (idx & 3) * 8u;
let mask = ~(0xffu << bit_offset);
let existing = output_buffer[word_idx];
output_buffer[word_idx] = (existing & mask) | (params.value & (0xffu << bit_offset));
}
}
}
if (id_u32 >= end) {
return;
}
let lo = max(start, id_u32) - id_u32;
let hi = min(end, id_u32 + 4u) - id_u32;
var mask = 0xFFFFFFFFu << (8u * lo);
if (hi < 4u) {
mask &= (1u << (8u * hi)) - 1u;
}
if (mask == 0xFFFFFFFFu) {
output_buffer[id] = params.value;
} else {
let existing = output_buffer[id];
output_buffer[id] = (existing & ~(mask)) | (params.value & mask);
}
}
+16
View File
@@ -1295,6 +1295,11 @@ class MODEL_TENSOR(IntEnum):
A_GEN_CODE_ATTN_GATE = auto() # lfm3a: per-head sigmoid gate on the attention output
A_GEN_CODE_HEAD_NORM = auto() # lfm3a: per-codebook norm before the output head, merged 2D [n_codebooks, dim]
A_GEN_CODE_MASK_EMBD = auto() # lfm3a: MaskGIT mask embedding
A_GEN_CODE_ENORM = auto() # drama3: per-layer norm of the code embedding
A_GEN_CODE_HNORM = auto() # drama3: per-layer norm of the input hidden state
A_GEN_CODE_EH_PROJ = auto() # drama3: per-layer [code embd; hidden] -> hidden projection
A_GEN_CODE_BLK_EMBD = auto() # drama3: per-layer code embedding table
A_GEN_CODE_BLK_OUT_NORM = auto() # drama3: per-layer output norm
# qwen3tts code2wav: RVQ codes -> raw PCM
A_GEN_WAV_QUANT_FIRST_IN = auto() # semantic RVQ, in_proj (1x1 conv, loaded as 2D)
A_GEN_WAV_QUANT_FIRST_OUT = auto() # semantic RVQ, out_proj
@@ -2198,6 +2203,11 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.A_GEN_CODE_ATTN_GATE: "a.gen.code.blk.{bid}.attn_gate",
MODEL_TENSOR.A_GEN_CODE_HEAD_NORM: "a.gen.code.head_norm",
MODEL_TENSOR.A_GEN_CODE_MASK_EMBD: "a.gen.code.mask_embd",
MODEL_TENSOR.A_GEN_CODE_ENORM: "a.gen.code.blk.{bid}.enorm",
MODEL_TENSOR.A_GEN_CODE_HNORM: "a.gen.code.blk.{bid}.hnorm",
MODEL_TENSOR.A_GEN_CODE_EH_PROJ: "a.gen.code.blk.{bid}.eh_proj",
MODEL_TENSOR.A_GEN_CODE_BLK_EMBD: "a.gen.code.blk.{bid}.embd",
MODEL_TENSOR.A_GEN_CODE_BLK_OUT_NORM: "a.gen.code.blk.{bid}.out_norm",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_IN: "a.gen.wav.quant.first.in_proj",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_OUT: "a.gen.wav.quant.first.out_proj",
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_CB: "a.gen.wav.quant.first.codebook",
@@ -2615,6 +2625,11 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.A_GEN_CODE_ATTN_GATE,
MODEL_TENSOR.A_GEN_CODE_HEAD_NORM,
MODEL_TENSOR.A_GEN_CODE_MASK_EMBD,
MODEL_TENSOR.A_GEN_CODE_ENORM,
MODEL_TENSOR.A_GEN_CODE_HNORM,
MODEL_TENSOR.A_GEN_CODE_EH_PROJ,
MODEL_TENSOR.A_GEN_CODE_BLK_EMBD,
MODEL_TENSOR.A_GEN_CODE_BLK_OUT_NORM,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_IN,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_OUT,
MODEL_TENSOR.A_GEN_WAV_QUANT_FIRST_CB,
@@ -6919,6 +6934,7 @@ class VisionProjectorType:
CHATTERBOX_SPKENC = "chatterbox_spkenc" # audio: CAMPPlus speaker encoder
CHATTERBOX = "chatterbox" # audio generation: s3gen flow matching + HiFT vocoder
SOPRANO = "soprano" # audio generation: Vocos decoder
DRAMA3_GEN = "drama3_gen" # audio generation: codebook tower + Fish DAC decoder
HUNYUANVL = "hunyuanvl"
PARAKEET = "parakeet" # audio
MINIMAXM3 = "minimax_m3"
+3 -1
View File
@@ -221,10 +221,12 @@ class SpecialVocab:
if isinstance(merges, list) and merges:
if isinstance(merges[0], str):
self.merges = merges
elif isinstance(merges[0], list) and len(merges[0]) == 2 and isinstance(merges[0][0], str):
elif isinstance(merges[0], list):
# New format since transformers 4.45 to support spaces in merges
# ref: https://github.com/ggml-org/llama.cpp/issues/9692
# TODO: internally store as the new format instead of converting to old
if not all(isinstance(pair, list) and len(pair) == 2 and all(isinstance(s, str) for s in pair) for pair in merges):
raise ValueError("Unknown tokenizer merges format")
if any(' ' in s for pair in merges for s in pair):
logger.warning(f'Spaces in merges detected, encoding as {chr(ord(" ") + 256)!r}')
self.merges = [
+1
View File
@@ -70,6 +70,7 @@ add_library(mtmd
models/mimo-audio.cpp
models/qwen3tts-spkenc.cpp
models/qwen3tts-gen.cpp
models/drama3-gen.cpp
models/pockettts-seanet.cpp
models/pockettts-spkenc.cpp
models/pockettts-gen.cpp
+9
View File
@@ -261,6 +261,13 @@
#define TN_A_GEN_CODE_MASK_EMBD "a.gen.code.mask_embd"
#define TN_ATTN_GATE "%s.blk.%d.attn_gate.%s"
// drama3 tower, the rest of the block uses the generic names with prefix "a.gen.code"
#define TN_A_GEN_CODE_ENORM "a.gen.code.blk.%d.enorm.%s"
#define TN_A_GEN_CODE_HNORM "a.gen.code.blk.%d.hnorm.%s"
#define TN_A_GEN_CODE_EH_PROJ "a.gen.code.blk.%d.eh_proj.%s"
#define TN_A_GEN_CODE_BLK_EMBD "a.gen.code.blk.%d.embd.%s"
#define TN_A_GEN_CODE_BLK_OUT_NORM "a.gen.code.blk.%d.out_norm.%s"
// qwen3tts code2wav (RVQ codes -> raw PCM)
// pre_transformer layers use the generic TN_ATTN_*/TN_FFN_*/TN_LN_*/TN_LS_* macros, prefix "a.gen.wav.tfm"
#define TN_A_GEN_WAV_QUANT_FIRST_IN "a.gen.wav.quant.first.in_proj.%s"
@@ -595,6 +602,7 @@ enum projector_type {
PROJECTOR_TYPE_POCKETTTS_GEN,
PROJECTOR_TYPE_SOPRANO,
PROJECTOR_TYPE_LFM3A_GEN,
PROJECTOR_TYPE_DRAMA3_GEN,
PROJECTOR_TYPE_MUSE_GLIMMER,
PROJECTOR_TYPE_SENSENOVA_U1,
PROJECTOR_TYPE_COHERE2V,
@@ -679,6 +687,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
{ PROJECTOR_TYPE_POCKETTTS_GEN, "pockettts_gen"},
{ PROJECTOR_TYPE_SOPRANO, "soprano"},
{ PROJECTOR_TYPE_LFM3A_GEN, "lfm3a_gen"},
{ PROJECTOR_TYPE_DRAMA3_GEN, "drama3_gen"},
{ PROJECTOR_TYPE_MUSE_GLIMMER, "muse-glimmer"},
{ PROJECTOR_TYPE_SENSENOVA_U1, "sensenova_u1"},
{ PROJECTOR_TYPE_COHERE2V, "cohere2v"},
+10
View File
@@ -841,6 +841,15 @@ struct clip_chatterbox {
ggml_tensor * spk_affine_b = nullptr;
};
// drama3 tower: extra tensors around the generic block
struct clip_drama3_layer {
ggml_tensor * enorm_w = nullptr;
ggml_tensor * hnorm_w = nullptr;
ggml_tensor * eh_proj_w = nullptr; // [code embd; hidden] -> hidden
ggml_tensor * embd_w = nullptr;
ggml_tensor * out_norm_w = nullptr;
};
struct clip_vocos {
ggml_tensor * input_w = nullptr;
ggml_tensor * input_b = nullptr;
@@ -1107,6 +1116,7 @@ struct clip_model {
// qwen3tts code2wav: RVQ codes -> raw PCM
clip_code2wav c2w;
std::vector<clip_drama3_layer> drama3_layers;
// pocket-tts: SEANet stack, shared by the encoder (speaker path) and the decoder (gen path)
clip_seanet seanet;
+209 -91
View File
@@ -1237,6 +1237,15 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
}
builder = std::make_unique<clip_graph_lfm3a_gen>(ctx, img, gen_process, all_known, n_frames);
} break;
case PROJECTOR_TYPE_DRAMA3_GEN:
{
const auto gen_process = params ? params->gen_process : CLIP_GEN_PROCESS_GEN_CODE;
const int64_t n_cb = ctx->model.gen_code_embd_w->ne[2];
const int n_frames = params && params->codes ? (int) (params->codes->size() / n_cb) : 1;
builder = std::make_unique<clip_graph_drama3_gen>(ctx, img, gen_process, params ? params->gen_state : nullptr, n_frames,
params ? params->top_k : 30, params ? params->top_p : 0.9f,
params ? params->temp : 1.0f);
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
const auto gen_process = params ? params->gen_process : CLIP_GEN_PROCESS_GEN_CODE;
@@ -2257,6 +2266,22 @@ struct clip_model_loader {
// matches the reference decoder's sliding_window (speech_tokenizer/config.json)
hparams.wav_tfm_swa = 72;
} break;
case PROJECTOR_TYPE_DRAMA3_GEN:
{
// Fish DAC decoder, codec.pth has no config
hparams.audio_sample_rate = 44100;
hparams.wav_tfm_n_layer = 8;
hparams.wav_tfm_n_embd = 1024;
hparams.wav_tfm_n_ff = 3072;
hparams.wav_tfm_n_head = 16;
hparams.wav_tfm_n_head_kv = 16;
hparams.wav_tfm_eps = 1e-5f;
hparams.wav_tfm_rope_theta = 10000.0f;
hparams.wav_tfm_swa = 128;
hparams.wav_upsample_n_block = 2;
hparams.wav_dac_n_block = 4;
hparams.wav_dac_n_res = 3;
} break;
case PROJECTOR_TYPE_POCKETTTS_SPKENC:
case PROJECTOR_TYPE_POCKETTTS_GEN:
{
@@ -2785,6 +2810,92 @@ struct clip_model_loader {
}
}
// code2wav: RVQ codes -> raw PCM, drama3 has no quant/pre_conv/tfm projections
auto load_code2wav = [&](bool with_proj) {
auto & c2w = model.c2w;
c2w.quant_first_in_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_IN, "weight"), with_proj);
c2w.quant_first_out_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_OUT, "weight"), with_proj);
c2w.quant_first_cb_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_CB, "weight"));
c2w.quant_rest_in_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_IN, "weight"), with_proj);
c2w.quant_rest_out_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_OUT, "weight"), with_proj);
c2w.quant_rest_cb_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_CB, "weight"));
c2w.pre_conv_w = get_tensor(string_format(TN_A_GEN_WAV_PRE_CONV, "weight"), with_proj);
c2w.pre_conv_b = get_tensor(string_format(TN_A_GEN_WAV_PRE_CONV, "bias"), with_proj);
c2w.tfm_in_proj_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_IN_PROJ, "weight"), with_proj);
c2w.tfm_in_proj_b = get_tensor(string_format(TN_A_GEN_WAV_TFM_IN_PROJ, "bias"), with_proj);
c2w.tfm_out_proj_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_PROJ, "weight"), with_proj);
c2w.tfm_out_proj_b = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_PROJ, "bias"), with_proj);
c2w.tfm_output_norm_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_NORM, "weight"));
// loaded manually, the generic model.layers loop is taken by code_predictor
c2w.tfm_layers.resize(hparams.wav_tfm_n_layer);
for (int il = 0; il < hparams.wav_tfm_n_layer; il++) {
auto & layer = c2w.tfm_layers[il];
const char * p = "a.gen.wav.tfm";
layer.q_w = get_tensor(string_format(TN_ATTN_Q, p, il, "weight"));
layer.k_w = get_tensor(string_format(TN_ATTN_K, p, il, "weight"));
layer.v_w = get_tensor(string_format(TN_ATTN_V, p, il, "weight"));
layer.o_w = get_tensor(string_format(TN_ATTN_OUTPUT, p, il, "weight"));
layer.ln_1_w = get_tensor(string_format(TN_LN_1, p, il, "weight"));
layer.ln_2_w = get_tensor(string_format(TN_LN_2, p, il, "weight"));
layer.ls_1_w = get_tensor(string_format(TN_LS_1, p, il, "weight"));
layer.ls_2_w = get_tensor(string_format(TN_LS_2, p, il, "weight"));
layer.ff_gate_w = get_tensor(string_format(TN_FFN_GATE, p, il, "weight"));
layer.ff_up_w = get_tensor(string_format(TN_FFN_UP, p, il, "weight"));
layer.ff_down_w = get_tensor(string_format(TN_FFN_DOWN, p, il, "weight"));
}
// upsample: 2x (causal ConvTranspose1d + ConvNeXt block)
c2w.upsample.resize(hparams.wav_upsample_n_block);
for (int il = 0; il < hparams.wav_upsample_n_block; il++) {
auto & up = c2w.upsample[il];
up.conv_w = get_tensor(string_format(TN_A_GEN_WAV_UP_CONV, il, "weight"));
up.conv_b = get_tensor(string_format(TN_A_GEN_WAV_UP_CONV, il, "bias"));
up.dwconv_w = get_tensor(string_format(TN_A_GEN_WAV_UP_DWCONV, il, "weight"));
up.dwconv_b = get_tensor(string_format(TN_A_GEN_WAV_UP_DWCONV, il, "bias"));
up.norm_w = get_tensor(string_format(TN_A_GEN_WAV_UP_NORM, il, "weight"));
up.norm_b = get_tensor(string_format(TN_A_GEN_WAV_UP_NORM, il, "bias"));
up.pw1_w = get_tensor(string_format(TN_A_GEN_WAV_UP_PW1, il, "weight"));
up.pw1_b = get_tensor(string_format(TN_A_GEN_WAV_UP_PW1, il, "bias"));
up.pw2_w = get_tensor(string_format(TN_A_GEN_WAV_UP_PW2, il, "weight"));
up.pw2_b = get_tensor(string_format(TN_A_GEN_WAV_UP_PW2, il, "bias"));
up.gamma = get_tensor(string_format(TN_A_GEN_WAV_UP_GAMMA, il));
}
// DAC decoder: conv_pre + n upsample blocks (each with n_res residual units) + conv_post
c2w.dac_entry_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_ENTRY, "weight"));
c2w.dac_entry_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_ENTRY, "bias"));
c2w.dac.resize(hparams.wav_dac_n_block);
for (int il = 0; il < hparams.wav_dac_n_block; il++) {
auto & blk = c2w.dac[il];
blk.snake_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_SNAKE, il, "alpha"));
blk.snake_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_SNAKE, il, "beta"));
blk.conv_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_CONV, il, "weight"));
blk.conv_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_CONV, il, "bias"));
blk.res.resize(hparams.wav_dac_n_res);
for (int ir = 0; ir < hparams.wav_dac_n_res; ir++) {
auto & res = blk.res[ir];
res.act1_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT1, il, ir, "alpha"));
res.act1_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT1, il, ir, "beta"));
res.conv1_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV1, il, ir, "weight"));
res.conv1_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV1, il, ir, "bias"));
res.act2_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT2, il, ir, "alpha"));
res.act2_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT2, il, ir, "beta"));
res.conv2_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV2, il, ir, "weight"));
res.conv2_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV2, il, ir, "bias"));
}
}
c2w.dac_post_snake_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_SNAKE, "alpha"));
c2w.dac_post_snake_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_SNAKE, "beta"));
c2w.dac_post_conv_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_CONV, "weight"));
c2w.dac_post_conv_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_CONV, "bias"));
};
switch (model.proj_type) {
case PROJECTOR_TYPE_MLP:
@@ -3656,91 +3767,25 @@ struct clip_model_loader {
model.gen_code_norm_w = get_tensor(string_format(TN_A_GEN_CODE_NORM, "weight"));
// code2wav: RVQ codes -> raw PCM, lives in the same ctx as code_predictor
{
auto & c2w = model.c2w;
c2w.quant_first_in_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_IN, "weight"));
c2w.quant_first_out_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_OUT, "weight"));
c2w.quant_first_cb_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_FIRST_CB, "weight"));
c2w.quant_rest_in_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_IN, "weight"));
c2w.quant_rest_out_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_OUT, "weight"));
c2w.quant_rest_cb_w = get_tensor(string_format(TN_A_GEN_WAV_QUANT_REST_CB, "weight"));
c2w.pre_conv_w = get_tensor(string_format(TN_A_GEN_WAV_PRE_CONV, "weight"));
c2w.pre_conv_b = get_tensor(string_format(TN_A_GEN_WAV_PRE_CONV, "bias"));
c2w.tfm_in_proj_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_IN_PROJ, "weight"));
c2w.tfm_in_proj_b = get_tensor(string_format(TN_A_GEN_WAV_TFM_IN_PROJ, "bias"));
c2w.tfm_out_proj_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_PROJ, "weight"));
c2w.tfm_out_proj_b = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_PROJ, "bias"));
c2w.tfm_output_norm_w = get_tensor(string_format(TN_A_GEN_WAV_TFM_OUT_NORM, "weight"));
// loaded manually, the generic model.layers loop is taken by code_predictor
c2w.tfm_layers.resize(hparams.wav_tfm_n_layer);
for (int il = 0; il < hparams.wav_tfm_n_layer; il++) {
auto & layer = c2w.tfm_layers[il];
const char * p = "a.gen.wav.tfm";
layer.q_w = get_tensor(string_format(TN_ATTN_Q, p, il, "weight"));
layer.k_w = get_tensor(string_format(TN_ATTN_K, p, il, "weight"));
layer.v_w = get_tensor(string_format(TN_ATTN_V, p, il, "weight"));
layer.o_w = get_tensor(string_format(TN_ATTN_OUTPUT, p, il, "weight"));
layer.ln_1_w = get_tensor(string_format(TN_LN_1, p, il, "weight"));
layer.ln_2_w = get_tensor(string_format(TN_LN_2, p, il, "weight"));
layer.ls_1_w = get_tensor(string_format(TN_LS_1, p, il, "weight"));
layer.ls_2_w = get_tensor(string_format(TN_LS_2, p, il, "weight"));
layer.ff_gate_w = get_tensor(string_format(TN_FFN_GATE, p, il, "weight"));
layer.ff_up_w = get_tensor(string_format(TN_FFN_UP, p, il, "weight"));
layer.ff_down_w = get_tensor(string_format(TN_FFN_DOWN, p, il, "weight"));
}
// upsample: 2x (causal ConvTranspose1d + ConvNeXt block)
c2w.upsample.resize(hparams.wav_upsample_n_block);
for (int il = 0; il < hparams.wav_upsample_n_block; il++) {
auto & up = c2w.upsample[il];
up.conv_w = get_tensor(string_format(TN_A_GEN_WAV_UP_CONV, il, "weight"));
up.conv_b = get_tensor(string_format(TN_A_GEN_WAV_UP_CONV, il, "bias"));
up.dwconv_w = get_tensor(string_format(TN_A_GEN_WAV_UP_DWCONV, il, "weight"));
up.dwconv_b = get_tensor(string_format(TN_A_GEN_WAV_UP_DWCONV, il, "bias"));
up.norm_w = get_tensor(string_format(TN_A_GEN_WAV_UP_NORM, il, "weight"));
up.norm_b = get_tensor(string_format(TN_A_GEN_WAV_UP_NORM, il, "bias"));
up.pw1_w = get_tensor(string_format(TN_A_GEN_WAV_UP_PW1, il, "weight"));
up.pw1_b = get_tensor(string_format(TN_A_GEN_WAV_UP_PW1, il, "bias"));
up.pw2_w = get_tensor(string_format(TN_A_GEN_WAV_UP_PW2, il, "weight"));
up.pw2_b = get_tensor(string_format(TN_A_GEN_WAV_UP_PW2, il, "bias"));
up.gamma = get_tensor(string_format(TN_A_GEN_WAV_UP_GAMMA, il));
}
// DAC decoder: conv_pre + n upsample blocks (each with n_res residual units) + conv_post
c2w.dac_entry_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_ENTRY, "weight"));
c2w.dac_entry_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_ENTRY, "bias"));
c2w.dac.resize(hparams.wav_dac_n_block);
for (int il = 0; il < hparams.wav_dac_n_block; il++) {
auto & blk = c2w.dac[il];
blk.snake_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_SNAKE, il, "alpha"));
blk.snake_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_SNAKE, il, "beta"));
blk.conv_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_CONV, il, "weight"));
blk.conv_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_CONV, il, "bias"));
blk.res.resize(hparams.wav_dac_n_res);
for (int ir = 0; ir < hparams.wav_dac_n_res; ir++) {
auto & res = blk.res[ir];
res.act1_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT1, il, ir, "alpha"));
res.act1_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT1, il, ir, "beta"));
res.conv1_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV1, il, ir, "weight"));
res.conv1_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV1, il, ir, "bias"));
res.act2_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT2, il, ir, "alpha"));
res.act2_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_ACT2, il, ir, "beta"));
res.conv2_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV2, il, ir, "weight"));
res.conv2_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_RES_CONV2, il, ir, "bias"));
}
}
c2w.dac_post_snake_alpha = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_SNAKE, "alpha"));
c2w.dac_post_snake_beta = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_SNAKE, "beta"));
c2w.dac_post_conv_w = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_CONV, "weight"));
c2w.dac_post_conv_b = get_tensor(string_format(TN_A_GEN_WAV_DAC_POST_CONV, "bias"));
load_code2wav(true);
} break;
case PROJECTOR_TYPE_DRAMA3_GEN:
{
// generic a.gen.code.blk.* tensors are loaded by the loop above
model.drama3_layers.resize(hparams.n_layer);
for (int il = 0; il < hparams.n_layer; ++il) {
auto & tl = model.drama3_layers[il];
model.layers[il].attn_gate_w = get_tensor(string_format(TN_ATTN_GATE, prefix, il, "weight"));
tl.enorm_w = get_tensor(string_format(TN_A_GEN_CODE_ENORM, il, "weight"));
tl.hnorm_w = get_tensor(string_format(TN_A_GEN_CODE_HNORM, il, "weight"));
tl.eh_proj_w = get_tensor(string_format(TN_A_GEN_CODE_EH_PROJ, il, "weight"));
tl.embd_w = get_tensor(string_format(TN_A_GEN_CODE_BLK_EMBD, il, "weight"));
tl.out_norm_w = get_tensor(string_format(TN_A_GEN_CODE_BLK_OUT_NORM, il, "weight"));
}
model.gen_code_embd_w = get_tensor(string_format(TN_A_GEN_CODE_EMBD, "weight")); // merged 3D, one table per codebook
model.gen_code_head_w = get_tensor(string_format(TN_A_GEN_CODE_HEAD, "weight")); // merged 3D, codebooks 1..
model.gen_code_out_embd_w = get_tensor(string_format(TN_A_GEN_CODE_OUT_EMBD, "weight")); // backbone rows of the semantic band
load_code2wav(false);
} break;
case PROJECTOR_TYPE_CHATTERBOX:
{
@@ -5520,6 +5565,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
case PROJECTOR_TYPE_LFM3A_GEN:
case PROJECTOR_TYPE_DRAMA3_GEN:
{
// one hidden-state vector fed back to the talker per call
n_patches = 1;
@@ -5587,6 +5633,7 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
static std::vector<c2w_state_slot> list_gen_state_slots(const clip_hparams & hparams, const clip_model & model) {
switch (model.proj_type) {
case PROJECTOR_TYPE_QWEN3TTS_GEN: return list_c2w_state_slots(hparams, model);
case PROJECTOR_TYPE_DRAMA3_GEN: return list_drama3_state_slots(hparams, model);
case PROJECTOR_TYPE_POCKETTTS_GEN: return list_pockettts_state_slots(hparams, model);
case PROJECTOR_TYPE_LFM3A_GEN: return list_lfm3a_gen_state_slots(hparams, model);
default: return {};
@@ -5772,6 +5819,14 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
LOG_ERR("%s: audio gen requires a gen state\n", __func__);
return false;
}
if (ctx->proj_type() == PROJECTOR_TYPE_DRAMA3_GEN && params->gen_process == CLIP_GEN_PROCESS_GEN_CODE) {
float pos = 0.0f;
ggml_backend_tensor_get(params->gen_state->slots[0], &pos, 0, sizeof(float));
if ((int) pos >= clip_graph_drama3_gen::n_pos_max) {
LOG_ERR("%s: the codebook tower holds at most %d rows\n", __func__, clip_graph_drama3_gen::n_pos_max);
return false;
}
}
// build the inference graph
ggml_backend_sched_reset(ctx->sched.get());
@@ -5845,6 +5900,10 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
size_t i = 0;
for (const auto & slot : list_gen_state_slots(hparams, model)) {
GGML_ASSERT(i < slots.size());
if (slot.in_place) {
i++;
continue;
}
ggml_backend_tensor_copy(slots[i++], get_inp_tensor(("state_in_" + slot.name).c_str()));
}
};
@@ -6900,6 +6959,58 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
}
}
} break;
case PROJECTOR_TYPE_DRAMA3_GEN:
{
const int64_t n_cb = model.gen_code_embd_w->ne[2];
const int64_t n_vocab = model.gen_code_embd_w->ne[1];
if (params->gen_process == CLIP_GEN_PROCESS_GEN_WAV) {
// frame-major to group-major, clamp: codec residual codebooks are smaller than the tower vocab
GGML_ASSERT(params->codes != nullptr && params->codes->size() % n_cb == 0);
const int64_t n_frames = (int64_t) params->codes->size() / n_cb;
const int64_t n_first = model.c2w.quant_first_cb_w->ne[1];
const int64_t n_rest = model.c2w.quant_rest_cb_w->ne[1];
std::vector<int32_t> codes(params->codes->size());
for (int64_t f = 0; f < n_frames; f++) {
for (int64_t g = 0; g < n_cb; g++) {
const int64_t n = g == 0 ? n_first : n_rest;
codes[g * n_frames + f] = std::clamp<int32_t>((*params->codes)[f * n_cb + g], 0, (int32_t) n - 1);
}
}
set_input_i32("inp_codes", codes);
set_gen_state_in();
} else {
// code0 < 0: next row is text (prompt), the tower only fills its KV
// codes: input frame codes for a speech row, empty for a text row
const bool next_speech = params->code0 >= 0;
const bool cur_speech = params->codes && (int64_t) params->codes->size() == n_cb;
if (params->code0 >= n_vocab) {
LOG_ERR("%s: code0 out of range (%d, vocab %lld)\n", __func__, params->code0, (long long) n_vocab);
return false;
}
std::vector<int32_t> code0 = { std::max(params->code0, 0) };
set_input_i32("inp_code0", code0);
std::vector<int32_t> extra(n_cb - 2);
for (int64_t g = 2; g < n_cb; g++) {
const int32_t c = cur_speech ? std::clamp<int32_t>((*params->codes)[g], 0, (int32_t) n_vocab - 1) : 0;
extra[g - 2] = c + (int32_t) (g * n_vocab);
}
set_input_i32("inp_extra", extra);
set_input_f32("inp_scale", { next_speech ? 1.0f : 0.0f, cur_speech ? 1.0f : 0.0f });
float pos = 0.0f;
ggml_backend_tensor_get(params->gen_state->slots[0], &pos, 0, sizeof(float));
std::vector<int32_t> inp_pos = { (int32_t) pos };
set_input_i32("inp_pos", inp_pos);
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
std::vector<float> r(model.gen_code_head_w->ne[2]);
for (auto & v : r) {
v = dist(params->gen_state->rng);
}
set_input_f32("inp_rand", r);
}
} break;
case PROJECTOR_TYPE_QWEN3TTS_GEN:
{
if (params->gen_process == CLIP_GEN_PROCESS_GEN_WAV) {
@@ -7700,13 +7811,15 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
clip_graph_soprano::decode_spectrum(model, spectrum, out_audio);
}
// drop the tail audio that comes from the code-0 rear padding
const int64_t n_codes = params->codes ? model.gen_code_head_w->ne[2] + 1 : 0;
const int64_t n_frames_w = hparams.wav_tfm_swa;
const int64_t n_frames = params->codes ? (int64_t) params->codes->size() / n_codes : n_frames_w;
if (n_frames < n_frames_w) {
const size_t hop = out_audio.size() / n_frames_w;
out_audio.resize((size_t) n_frames * hop);
// drop the tail audio that comes from the code-0 rear padding, drama3 has no padding
if (model.proj_type != PROJECTOR_TYPE_DRAMA3_GEN) {
const int64_t n_codes = params->codes ? model.gen_code_head_w->ne[2] + 1 : 0;
const int64_t n_frames_w = hparams.wav_tfm_swa;
const int64_t n_frames = params->codes ? (int64_t) params->codes->size() / n_codes : n_frames_w;
if (n_frames < n_frames_w) {
const size_t hop = out_audio.size() / n_frames_w;
out_audio.resize((size_t) n_frames * hop);
}
}
}
if (params->enc_state != nullptr && model.proj_type == PROJECTOR_TYPE_LFM3A) {
@@ -7744,6 +7857,10 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
const auto & slots = params->gen_state->slots;
size_t i = 0;
for (const auto & slot : list_gen_state_slots(hparams, model)) {
if (slot.in_place) {
i++;
continue;
}
ggml_tensor * t = ggml_graph_get_tensor(gf, ("state_out_" + slot.name).c_str());
if (t == nullptr) {
GGML_ABORT("graph has no \"state_out_%s\" tensor", slot.name.c_str());
@@ -7924,6 +8041,7 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
return ctx->model.mm_fc_w->ne[2];
case PROJECTOR_TYPE_QWEN3TTS_GEN:
case PROJECTOR_TYPE_LFM3A_GEN:
case PROJECTOR_TYPE_DRAMA3_GEN:
return ctx->model.gen_code_out_embd_w->ne[0];
case PROJECTOR_TYPE_POCKETTTS_SPKENC:
return ctx->model.spk_proj_w->ne[1];
+256
View File
@@ -0,0 +1,256 @@
#include "models.h"
#include <cmath>
#include <string>
// tower RoPE: interleaved, first 64 dims of each head
static constexpr int DRAMA3_N_ROT = 64;
static constexpr float DRAMA3_ROPE_THETA = 1e7f;
std::vector<c2w_state_slot> list_drama3_state_slots(const clip_hparams & hparams, const clip_model & model) {
std::vector<c2w_state_slot> slots;
slots.push_back({"tower_pos", 1, 1, true});
for (size_t il = 0; il < model.layers.size(); il++) {
const int64_t n_kv = model.layers[il].k_w->ne[1];
slots.push_back({"tower_k_" + std::to_string(il), n_kv, clip_graph_drama3_gen::n_pos_max, true});
slots.push_back({"tower_v_" + std::to_string(il), n_kv, clip_graph_drama3_gen::n_pos_max, true});
}
for (auto & slot : list_c2w_state_slots(hparams, model)) {
slots.push_back(slot);
}
return slots;
}
// h_out = out_norm(Block(eh_proj([enorm(mask * E[code] + extra); hnorm(h_in)])))
ggml_tensor * clip_graph_drama3_gen::tower_layer(ggml_tensor * x, int il, ggml_tensor * code, ggml_tensor * mask, ggml_tensor * extra,
ggml_tensor * inp_pos, ggml_tensor * k_slot, ggml_tensor * v_slot, int pos) {
const auto & layer = model.layers[il];
const auto & tl = model.drama3_layers[il];
const int n_head = hparams.n_head;
const int n_head_kv = hparams.n_head_kv;
const int64_t d_head = layer.q_w->ne[1] / n_head;
const int64_t n_kv = d_head * n_head_kv;
const float eps = hparams.eps;
ggml_tensor * e = ggml_mul(ctx0, ggml_get_rows(ctx0, tl.embd_w, code), mask);
if (extra) {
e = ggml_add(ctx0, e, extra);
}
e = ggml_mul(ctx0, ggml_rms_norm(ctx0, e, eps), tl.enorm_w);
ggml_tensor * h = ggml_mul(ctx0, ggml_rms_norm(ctx0, x, eps), tl.hnorm_w);
ggml_tensor * mixed = ggml_mul_mat(ctx0, tl.eh_proj_w, ggml_concat(ctx0, e, h, 0));
ggml_tensor * cur = ggml_mul(ctx0, ggml_rms_norm(ctx0, mixed, eps), layer.ln_1_w);
ggml_tensor * q = ggml_reshape_3d(ctx0, ggml_mul_mat(ctx0, layer.q_w, cur), d_head, n_head, 1);
ggml_tensor * k = ggml_reshape_3d(ctx0, ggml_mul_mat(ctx0, layer.k_w, cur), d_head, n_head_kv, 1);
ggml_tensor * v = ggml_mul_mat(ctx0, layer.v_w, cur);
ggml_tensor * gate = ggml_mul_mat(ctx0, layer.attn_gate_w, cur);
q = ggml_mul(ctx0, ggml_rms_norm(ctx0, q, eps), layer.q_norm);
k = ggml_mul(ctx0, ggml_rms_norm(ctx0, k, eps), layer.k_norm);
q = ggml_rope_ext(ctx0, q, inp_pos, nullptr, DRAMA3_N_ROT, GGML_ROPE_TYPE_NORMAL, 0, DRAMA3_ROPE_THETA, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
k = ggml_rope_ext(ctx0, k, inp_pos, nullptr, DRAMA3_N_ROT, GGML_ROPE_TYPE_NORMAL, 0, DRAMA3_ROPE_THETA, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f);
// write K/V of this row to the slot, attention uses the cached rows + this row
ggml_tensor * k_new = ggml_reshape_2d(ctx0, k, n_kv, 1);
ggml_build_forward_expand(gf, ggml_cpy(ctx0, k_new, ggml_view_2d(ctx0, k_slot, n_kv, 1, k_slot->nb[1], (size_t) pos * k_slot->nb[1])));
ggml_build_forward_expand(gf, ggml_cpy(ctx0, v, ggml_view_2d(ctx0, v_slot, n_kv, 1, v_slot->nb[1], (size_t) pos * v_slot->nb[1])));
ggml_tensor * k_all = k_new;
ggml_tensor * v_all = v;
if (pos > 0) {
k_all = ggml_concat(ctx0, ggml_view_2d(ctx0, k_slot, n_kv, pos, k_slot->nb[1], 0), k_new, 1);
v_all = ggml_concat(ctx0, ggml_view_2d(ctx0, v_slot, n_kv, pos, v_slot->nb[1], 0), v, 1);
}
ggml_tensor * attn = build_attn(nullptr, nullptr,
ggml_reshape_4d(ctx0, q, d_head, n_head, 1, 1),
ggml_reshape_4d(ctx0, k_all, d_head, n_head_kv, pos + 1, 1),
ggml_reshape_4d(ctx0, v_all, d_head, n_head_kv, pos + 1, 1),
nullptr, 1.0f / sqrtf((float) d_head), il);
attn = ggml_mul(ctx0, attn, ggml_sigmoid(ctx0, gate));
cur = ggml_add(ctx0, mixed, ggml_mul_mat(ctx0, layer.o_w, attn));
ggml_tensor * ff = ggml_mul(ctx0, ggml_rms_norm(ctx0, cur, eps), layer.ln_2_w);
ff = ggml_swiglu_split(ctx0, ggml_mul_mat(ctx0, layer.ff_gate_w, ff), ggml_mul_mat(ctx0, layer.ff_up_w, ff));
cur = ggml_add(ctx0, cur, ggml_mul_mat(ctx0, layer.ff_down_w, ff));
cur = ggml_mul(ctx0, ggml_rms_norm(ctx0, cur, eps), tl.out_norm_w);
cb(cur, "tower_out", il);
return cur;
}
// one row: backbone hidden + next main code -> other codes of that frame + next backbone input
ggml_cgraph * clip_graph_drama3_gen::build_tower() {
const int n_layer = (int) model.layers.size();
const int n_heads = (int) model.gen_code_head_w->ne[2]; // one per residual codebook
const int n_shared = n_layer - n_heads;
const int64_t n_cb = model.gen_code_embd_w->ne[2];
const int64_t n_embd_cb = model.gen_code_embd_w->ne[0];
int pos = 0;
std::vector<ggml_tensor *> slots;
const auto slot_list = list_drama3_state_slots(hparams, model);
if (gen_state) {
float pos_f = 0.0f;
ggml_backend_tensor_get(gen_state->slots[0], &pos_f, 0, sizeof(float));
pos = (int) pos_f;
slots = gen_state->slots;
} else {
// buffer reserve: no gen state, use stand-in slots
for (const auto & slot : slot_list) {
ggml_tensor * t = nullptr;
if (slot.in_place) {
t = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, slot.ne0, slot.ne1);
ggml_set_input(t);
}
slots.push_back(t);
}
}
GGML_ASSERT(pos < n_pos_max);
ggml_tensor * h = ggml_new_tensor_1d(ctx0, GGML_TYPE_F32, n_mmproj_embd);
ggml_set_name(h, "inp_raw");
ggml_set_input(h);
ggml_tensor * inp_code0 = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, 1);
ggml_set_name(inp_code0, "inp_code0");
ggml_set_input(inp_code0);
// codes 2.. of the input frame, as row ids of the merged codebook table
ggml_tensor * inp_extra = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_cb - 2);
ggml_set_name(inp_extra, "inp_extra");
ggml_set_input(inp_extra);
// [next frame is speech, input frame is speech]
ggml_tensor * inp_scale = ggml_new_tensor_1d(ctx0, GGML_TYPE_F32, 2);
ggml_set_name(inp_scale, "inp_scale");
ggml_set_input(inp_scale);
ggml_tensor * inp_pos = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, 1);
ggml_set_name(inp_pos, "inp_pos");
ggml_set_input(inp_pos);
ggml_tensor * inp_rand = ggml_new_tensor_1d(ctx0, GGML_TYPE_F32, n_heads);
ggml_set_name(inp_rand, "inp_rand");
ggml_set_input(inp_rand);
ggml_tensor * mask = ggml_view_1d(ctx0, inp_scale, 1, 0);
ggml_tensor * extra_on = ggml_view_1d(ctx0, inp_scale, 1, sizeof(float));
ggml_tensor * cb_all = ggml_reshape_2d(ctx0, model.gen_code_embd_w, n_embd_cb, model.gen_code_embd_w->ne[1] * n_cb);
ggml_tensor * extra = ggml_cont(ctx0, ggml_transpose(ctx0, ggml_get_rows(ctx0, cb_all, inp_extra)));
extra = ggml_mul(ctx0, ggml_reshape_2d(ctx0, ggml_sum_rows(ctx0, extra), n_embd_cb, 1), extra_on);
clip_graph_qwen3tts_gen::code_gen cg(*this, top_k, top_p);
ggml_tensor * out_codes = cg.cache_set(ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, 1, n_cb), 0, inp_code0);
ggml_tensor * code = inp_code0;
ggml_tensor * x = ggml_reshape_2d(ctx0, h, n_mmproj_embd, 1);
for (int il = 0; il < n_layer; il++) {
x = tower_layer(x, il, code, mask, il == 0 ? extra : nullptr, inp_pos, slots[1 + 2 * il], slots[2 + 2 * il], pos);
if (il < n_shared) {
continue;
}
const int ih = il - n_shared;
ggml_tensor * head = ggml_view_2d(ctx0, model.gen_code_head_w, model.gen_code_head_w->ne[0], model.gen_code_head_w->ne[1],
model.gen_code_head_w->nb[1], (size_t) ih * model.gen_code_head_w->nb[2]);
ggml_tensor * logits = ggml_mul_mat(ctx0, head, x);
if (temp > 0.0f && temp != 1.0f) {
logits = ggml_scale(ctx0, logits, 1.0f / temp);
}
code = cg.do_sampling(logits, ggml_view_1d(ctx0, inp_rand, 1, (size_t) ih * sizeof(float)));
out_codes = cg.cache_set(out_codes, ih + 1, code);
}
ggml_tensor * pos_slot = slots[0];
ggml_build_forward_expand(gf, ggml_cpy(ctx0, ggml_scale_bias(ctx0, pos_slot, 1.0f, 1.0f), pos_slot));
out_codes = ggml_cont(ctx0, out_codes);
ggml_set_name(out_codes, "out_codes");
ggml_set_output(out_codes);
ggml_build_forward_expand(gf, out_codes);
// backbone input of the next row: (E_lm[main] + E_cb1[code 1]) / sqrt(2)
ggml_tensor * code1 = ggml_view_1d(ctx0, out_codes, 1, out_codes->nb[1]);
ggml_tensor * cb1 = ggml_view_2d(ctx0, model.gen_code_embd_w, n_embd_cb, model.gen_code_embd_w->ne[1],
model.gen_code_embd_w->nb[1], model.gen_code_embd_w->nb[2]);
ggml_tensor * out_embd = ggml_add(ctx0, ggml_get_rows(ctx0, model.gen_code_out_embd_w, inp_code0), ggml_get_rows(ctx0, cb1, code1));
out_embd = ggml_scale(ctx0, out_embd, 1.0f / sqrtf(2.0f));
cb(out_embd, "gen_audio_out", -1);
ggml_build_forward_expand(gf, out_embd);
return gf;
}
// frames -> PCM, qwen3tts code2wav blocks without pre_conv and tfm projections
ggml_cgraph * clip_graph_drama3_gen::build_codec() {
const auto & c2w = model.c2w;
const int64_t n_cb = model.gen_code_embd_w->ne[2];
ggml_tensor * inp_codes = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, n_frames, n_cb);
ggml_set_name(inp_codes, "inp_codes");
ggml_set_input(inp_codes);
clip_graph_qwen3tts_gen::code2wav dec(*this);
dec.gelu_erf = true;
for (const auto & slot : list_c2w_state_slots(hparams, model)) {
ggml_tensor * t = ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, slot.ne0, slot.ne1);
ggml_set_name(t, ("state_in_" + slot.name).c_str());
ggml_set_input(t);
dec.state_in[slot.name] = t;
}
// out_proj is folded into the codebook tables at conversion
auto group_ids = [&](int g) {
return ggml_view_1d(ctx0, inp_codes, n_frames, (size_t) g * inp_codes->nb[1]);
};
ggml_tensor * cur = ggml_get_rows(ctx0, c2w.quant_first_cb_w, group_ids(0)); // [dim, T]
for (int g = 1; g < n_cb; g++) {
ggml_tensor * table = ggml_view_2d(ctx0, c2w.quant_rest_cb_w, c2w.quant_rest_cb_w->ne[0], c2w.quant_rest_cb_w->ne[1],
c2w.quant_rest_cb_w->nb[1], (size_t) (g - 1) * c2w.quant_rest_cb_w->nb[2]);
cur = ggml_add(ctx0, cur, ggml_get_rows(ctx0, table, group_ids(g)));
}
for (int il = 0; il < hparams.wav_tfm_n_layer; il++) {
cur = dec.tfm_layer_forward(cur, c2w.tfm_layers[il], il);
}
cur = ggml_mul(ctx0, ggml_rms_norm(ctx0, cur, hparams.wav_tfm_eps), c2w.tfm_output_norm_w);
cb(cur, "wav_tfm_out", -1);
ggml_tensor * x = ggml_cont(ctx0, ggml_transpose(ctx0, cur)); // [T, dim]
for (size_t il = 0; il < c2w.upsample.size(); il++) {
const auto & up = c2w.upsample[il];
x = dec.causal_conv_transpose1d(x, up.conv_w, up.conv_b, 2, "");
x = dec.convnext_block(x, up, "up" + std::to_string(il));
}
static constexpr int DAC_DILATIONS[3] = { 1, 3, 9 };
x = dec.causal_conv1d(x, c2w.dac_entry_w, c2w.dac_entry_b, 1, "dac_entry");
for (size_t il = 0; il < c2w.dac.size(); il++) {
const auto & blk = c2w.dac[il];
const int stride = (int) (blk.conv_w->ne[0] / 2);
const std::string blk_name = "dac" + std::to_string(il);
x = dec.snake(x, blk.snake_alpha, blk.snake_beta);
x = dec.causal_conv_transpose1d(x, blk.conv_w, blk.conv_b, stride, blk_name + "_tail");
for (size_t ir = 0; ir < blk.res.size(); ir++) {
x = dec.dac_res_unit(x, blk.res[ir], DAC_DILATIONS[ir], blk_name + "_res" + std::to_string(ir));
}
}
x = dec.snake(x, c2w.dac_post_snake_alpha, c2w.dac_post_snake_beta);
x = dec.causal_conv1d(x, c2w.dac_post_conv_w, c2w.dac_post_conv_b, 1, "dac_post_conv");
x = ggml_tanh(ctx0, ggml_reshape_1d(ctx0, x, x->ne[0]));
ggml_set_name(x, "out_audio");
ggml_set_output(x);
ggml_build_forward_expand(gf, x);
for (auto & slot : dec.state_out) {
ggml_set_name(slot.second, ("state_out_" + slot.first).c_str());
ggml_set_output(slot.second);
ggml_build_forward_expand(gf, slot.second);
}
return gf;
}
ggml_cgraph * clip_graph_drama3_gen::build() {
return gen_process == CLIP_GEN_PROCESS_GEN_WAV ? build_codec() : build_tower();
}
+34
View File
@@ -424,6 +424,9 @@ struct clip_graph_qwen3tts_gen : clip_graph {
ggml_tensor * convnext_block(ggml_tensor * x, const clip_code2wav::upsample_block & blk, const std::string & state_prefix) const;
ggml_tensor * dac_res_unit(ggml_tensor * x, const clip_code2wav::dac_res & res, int dilation, const std::string & state_name) const;
// drama3: erf GELU in ConvNeXt, not the tanh approximation
bool gelu_erf = false;
// inp_codes [1, n_codes] I32 -> this frame's audio samples [n_samples] F32, clamped to [-1, 1]
ggml_tensor * decode(ggml_tensor * inp_codes) const;
};
@@ -487,12 +490,43 @@ struct c2w_state_slot {
std::string name;
int64_t ne0;
int64_t ne1;
bool in_place = false; // graph writes the slot directly, no state_in/state_out copy
};
std::vector<c2w_state_slot> list_c2w_state_slots(const clip_hparams & hparams, const clip_model & model);
// drama3: tower KV slots, then list_c2w_state_slots()
std::vector<c2w_state_slot> list_drama3_state_slots(const clip_hparams & hparams, const clip_model & model);
// same, for the streaming mimi decoder (pocket-tts GEN_WAV)
std::vector<c2w_state_slot> list_pockettts_state_slots(const clip_hparams & hparams, const clip_model & model);
//
// drama3 generation:
// GEN_CODE = one codebook tower row, each layer attends over the whole sequence (KV in gen state)
// GEN_WAV = Fish DAC decoder, frames -> PCM
//
struct clip_graph_drama3_gen : clip_graph {
clip_graph_drama3_gen(clip_ctx * ctx, const clip_image_f32 & img, clip_gen_process_type gen_process, clip_gen_state * gen_state,
int n_frames, int top_k, float top_p, float temp)
: clip_graph(ctx, img), gen_process(gen_process), gen_state(gen_state), n_frames(n_frames), top_k(top_k), top_p(top_p), temp(temp) {}
ggml_cgraph * build() override;
// tower KV rows, shared by the prompt and the generated frames
static constexpr int n_pos_max = 4096;
clip_gen_process_type gen_process;
clip_gen_state * gen_state; // nullptr on buffer reserve
int n_frames; // GEN_WAV only
int top_k;
float top_p;
float temp;
ggml_cgraph * build_tower();
ggml_cgraph * build_codec();
ggml_tensor * tower_layer(ggml_tensor * x, int il, ggml_tensor * code, ggml_tensor * mask, ggml_tensor * extra,
ggml_tensor * inp_pos, ggml_tensor * k_slot, ggml_tensor * v_slot, int pos);
};
struct clip_graph_soprano : clip_graph {
clip_graph_soprano(clip_ctx * ctx, const clip_image_f32 & img, int n_frames) : clip_graph(ctx, img), n_frames(n_frames) {}
ggml_cgraph * build() override;
+5 -3
View File
@@ -527,7 +527,7 @@ ggml_tensor * clip_graph_qwen3tts_gen::code2wav::convnext_block(ggml_tensor * x,
ggml_tensor * g = ggml_mul_mat(ctx0, blk.pw1_w, hc);
g = ggml_add(ctx0, g, blk.pw1_b);
g = ggml_gelu(ctx0, g);
g = gelu_erf ? ggml_gelu_erf(ctx0, g) : ggml_gelu(ctx0, g);
g = ggml_mul_mat(ctx0, blk.pw2_w, g);
g = ggml_add(ctx0, g, blk.pw2_b);
g = ggml_mul(ctx0, g, blk.gamma);
@@ -617,7 +617,7 @@ std::vector<c2w_state_slot> list_c2w_state_slots(const clip_hparams & hparams, c
const auto & c2w = model.c2w;
std::vector<c2w_state_slot> slots;
if (c2w.pre_conv_w == nullptr) {
if (c2w.dac_entry_w == nullptr) {
return slots; // not a code2wav model, it keeps no state between calls
}
@@ -632,7 +632,9 @@ std::vector<c2w_state_slot> list_c2w_state_slots(const clip_hparams & hparams, c
slots.push_back({"tfm_v_" + std::to_string(il), kv_ch, prefix});
}
slots.push_back({"pre_conv", c2w.pre_conv_w->ne[0] - 1, c2w.pre_conv_w->ne[1]});
if (c2w.pre_conv_w) {
slots.push_back({"pre_conv", c2w.pre_conv_w->ne[0] - 1, c2w.pre_conv_w->ne[1]});
}
for (size_t il = 0; il < c2w.upsample.size(); il++) {
const auto & up = c2w.upsample[il];
+341
View File
@@ -2077,6 +2077,345 @@ private:
bool wav_header_sent = false;
};
// Drama 3: backbone samples the main token of each frame, codebook tower samples the other codes, Fish DAC makes the PCM
// the tower attends over the whole sequence, so it also runs on every prompt row
class drama3_gen_audio_pipeline : public mtmd_gen_audio_pipeline {
public:
using mtmd_gen_audio_pipeline::mtmd_gen_audio_pipeline;
void reset() override {
seq_id = 0;
pos = 0;
prompt_tokens.clear();
prompt_pos = 0;
frame.clear();
hist.clear();
codes_buf.clear();
n_decoded = 0;
gen_state.reset();
pcm_sent = 0;
wav_header_sent = false;
audio_pcm.clear();
h_state_buf.clear();
out_buf.clear();
}
int32_t set_input(const mtmd_helper_gen_audio_inp * inp) override {
reset();
seq_id = inp->seq_id;
out_type = inp->out_type;
stream = inp->stream;
if (inp->speaker_ref) {
LOG_WRN("mtmd_helper_gen_audio: voice cloning is not supported yet, the speaker file is ignored\n");
}
sem_begin = find_special_token(vocab, "<|semantic:0|>");
eos = find_special_token(vocab, "<|im_end|>");
if (sem_begin == LLAMA_TOKEN_NULL || eos == LLAMA_TOKEN_NULL) {
LOG_ERR("mtmd_helper_gen_audio: missing Drama 3 tokens in vocab\n");
return 1;
}
// reference-bracket-v1 prompt without references, tokenize each segment alone as the reference does
std::string text(inp->prompt, inp->prompt_len);
if (text.rfind("<|speaker:", 0) != 0) {
text = "<|speaker:0|> " + text;
}
const char * segments[] = {
"<|im_start|>system\n", "convert the provided text to speech", "<|im_end|>\n",
"<|im_start|>user\n", text.c_str(), "<|im_end|>\n", "<|im_start|>assistant\n<|voice|>",
};
for (const char * seg : segments) {
const int32_t len = (int32_t) strlen(seg);
std::vector<llama_token> ids(len + 16);
const int32_t n = llama_tokenize(vocab, seg, len, ids.data(), (int32_t) ids.size(), false, true);
if (n < 0) {
LOG_ERR("mtmd_helper_gen_audio: tokenization failed\n");
return 1;
}
prompt_tokens.insert(prompt_tokens.end(), ids.begin(), ids.begin() + n);
}
if (prompt_tokens.size() >= llama_n_ctx_seq(lctx) || prompt_tokens.size() > n_tower_rows) {
LOG_ERR("mtmd_helper_gen_audio: prompt does not fit in the context\n");
return 1;
}
if (!llama_memory_seq_rm(llama_get_memory(lctx), seq_id, 0, -1)) {
LOG_ERR("mtmd_helper_gen_audio: cannot reset the sequence\n");
return 1;
}
rng.seed(inp->seed == UINT32_MAX ? std::random_device{}() : inp->seed);
return init_gen_state(inp->seed) ? 0 : 1;
}
// run the tower on every prompt row to fill its KV
int32_t step_prompt(int32_t n_batch) override {
GGML_ASSERT(n_batch > 0);
const int n_prompt = (int) prompt_tokens.size();
if (prompt_pos >= n_prompt) {
return 0;
}
// small batches: every row also outputs logits
const int n = std::min({ n_batch, n_prompt - prompt_pos, 64 });
llama_batch batch = llama_batch_init(n, 0, 1);
for (int i = 0; i < n; i++) {
batch.token [i] = prompt_tokens[(size_t) (prompt_pos + i)];
batch.pos [i] = pos + i;
batch.n_seq_id[i] = 1;
batch.seq_id [i][0] = seq_id;
batch.logits [i] = 1;
}
batch.n_tokens = n;
const int ret = llama_decode(lctx, batch);
llama_batch_free(batch);
if (ret != 0) {
LOG_ERR("mtmd_helper_gen_audio: prompt decode failed\n");
return -1;
}
for (int i = 0; i < n; i++) {
const float * h = llama_get_embeddings_ith(lctx, i);
if (prompt_pos + i == n_prompt - 1) {
h_state_buf.assign(h, h + n_embd); // first frame comes from the last prompt row
break;
}
if (!run_tower(-1, h)) {
return -1;
}
}
pos += n;
prompt_pos += n;
return n_prompt - prompt_pos;
}
// sample the main token here, ignore the token of the caller
int32_t step_gen(llama_token sampled, const float * h_state_in, const float ** h_state_out, bool * out_stop) override {
GGML_UNUSED(sampled);
GGML_UNUSED(h_state_in);
*h_state_out = nullptr;
*out_stop = false;
const llama_token main = sample_main(llama_get_logits_ith(lctx, -1));
const size_t n_rows = prompt_tokens.size() - 1 + codes_buf.size() / n_cb;
if (main == eos || (uint32_t) pos >= llama_n_ctx_seq(lctx) || n_rows >= n_tower_rows) {
*out_stop = true;
return 0;
}
hist.push_back(main);
if (hist.size() > ras_window) {
hist.erase(hist.begin());
}
const float * next_embd = run_tower(main - sem_begin, h_state_buf.data());
if (!next_embd) {
return 1;
}
codes_buf.insert(codes_buf.end(), frame.begin(), frame.end());
// text/speech stream: all mrope sections use the same position
const bool mrope = llama_model_rope_type(model) == LLAMA_ROPE_TYPE_MROPE ||
llama_model_rope_type(model) == LLAMA_ROPE_TYPE_IMROPE;
decode_embd_batch batch(next_embd, 1, mrope ? 4 : 1, n_embd);
if (mrope) {
batch.set_position_mrope_1d(pos, seq_id);
} else {
batch.set_position_normal(pos, seq_id);
}
batch.logits[0] = 1;
if (llama_process(lctx, LLAMA_PROCESS_TYPE_DECODE, batch.render(lctx, 0, 1)) != 0) {
LOG_ERR("mtmd_helper_gen_audio: decode failed\n");
return 1;
}
pos++;
const float * h = llama_get_embeddings_ith(lctx, -1);
h_state_buf.assign(h, h + n_embd);
*h_state_out = h_state_buf.data();
if (stream && codes_buf.size() / n_cb - n_decoded >= stream_frames) {
return decode_wav() ? 0 : 1;
}
return 0;
}
int32_t get_output(int32_t * out_sample_rate, const char ** out_data, size_t * out_data_len, int64_t * out_n_samples) override {
*out_sample_rate = info.sample_rate;
if (!stream) {
if (flush() != 0) {
return 1;
}
if (out_n_samples) {
*out_n_samples = (int64_t) audio_pcm.size();
}
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
*out_data = (const char *) audio_pcm.data();
*out_data_len = audio_pcm.size() * sizeof(float);
return 0;
}
out_buf.clear();
if (!write_wav16(out_buf, audio_pcm, info.sample_rate)) {
LOG_ERR("mtmd_helper_gen_audio: output too large for WAV\n");
return 1;
}
*out_data = out_buf.data();
*out_data_len = out_buf.size();
return 0;
}
// streaming: only the audio since the previous call
const size_t n_new = audio_pcm.size() - pcm_sent;
if (out_n_samples) {
*out_n_samples = (int64_t) n_new;
}
if (out_type == MTMD_HELPER_GEN_AUDIO_OUTTYPE_PCM) {
*out_data = (const char *) (audio_pcm.data() + pcm_sent);
*out_data_len = n_new * sizeof(float);
pcm_sent = audio_pcm.size();
return 0;
}
out_buf.clear();
if (!wav_header_sent) {
write_wav16_header(out_buf, UINT32_MAX, info.sample_rate);
wav_header_sent = true;
}
append_wav16_pcm(out_buf, audio_pcm.data() + pcm_sent, n_new);
pcm_sent = audio_pcm.size();
*out_data = out_buf.data();
*out_data_len = out_buf.size();
return 0;
}
int32_t flush() override {
return decode_wav() ? 0 : 1;
}
private:
// repetition-aware sampling: if a semantic draw repeats one of the last ras_window main tokens, draw again with fixed settings
static constexpr size_t ras_window = 16;
static constexpr size_t n_cb = 10;
// codebook tower KV rows (clip_graph_drama3_gen::n_pos_max): one per prompt row but the last, one per frame
static constexpr size_t n_tower_rows = 4096;
// frames per codec call when streaming (0.33 s)
static constexpr size_t stream_frames = 7;
// top-p keeps the token that crosses the threshold, as the reference does
llama_token draw(const std::vector<float> & cand_logits, float temp, int32_t top_k, float top_p) {
std::vector<std::pair<float, int>> cand(cand_logits.size());
for (size_t i = 0; i < cand.size(); i++) {
cand[i] = { cand_logits[i] / temp, (int) i };
}
std::sort(cand.begin(), cand.end(), [](const auto & a, const auto & b) { return a.first > b.first; });
const float lmax = cand[0].first;
double sum = 0.0;
std::vector<double> prob(cand.size());
for (size_t i = 0; i < cand.size(); i++) {
prob[i] = std::exp((double) (cand[i].first - lmax));
sum += prob[i];
}
size_t n_keep = cand.size();
if (top_k > 0) {
n_keep = std::min(n_keep, (size_t) top_k);
}
double cum = 0.0;
for (size_t i = 0; i < n_keep; i++) {
if (cum > top_p * sum) {
n_keep = i;
break;
}
cum += prob[i];
}
double kept = 0.0;
for (size_t i = 0; i < n_keep; i++) {
kept += prob[i];
}
double r = std::uniform_real_distribution<double>(0.0, 1.0)(rng) * kept;
for (size_t i = 0; i < n_keep; i++) {
r -= prob[i];
if (r <= 0.0) {
return cand[i].second;
}
}
return cand[n_keep - 1].second;
}
// candidates: eos, then semantic codes 1.. (the reference never samples code 0)
llama_token sample_main(const float * logits) {
std::vector<float> cand(n_cb_vocab);
cand[0] = logits[eos];
for (int i = 1; i < n_cb_vocab; i++) {
cand[i] = logits[sem_begin + i];
}
auto to_token = [&](int i) { return i == 0 ? eos : sem_begin + i; };
const llama_token first = to_token(draw(cand, 1.0f, 0, 0.9f));
if (first != eos && std::find(hist.begin(), hist.end(), first) != hist.end()) {
return to_token(draw(cand, 1.0f, 30, 0.95f));
}
return first;
}
// code0 < 0 for a prompt row, else the main code of the next frame
// returns the next backbone input, the codes of the next frame go to frame
const float * run_tower(int32_t code0, const float * h) {
mtmd_gen_inp inp = mtmd_gen_inp_default(mctx);
inp.type = MTMD_GEN_PROCESS_TYPE_GEN_CODE;
inp.code0 = code0;
inp.embd = const_cast<float *>(h);
inp.codes = frame.empty() ? nullptr : frame.data();
inp.n_codes = frame.size();
mtmd_gen_out out{};
if (mtmd_gen_audio_process(mctx, gen_state.get(), &inp, &out) != 0 || (code0 >= 0 && out.n_codes != n_cb)) {
LOG_ERR("mtmd_helper_gen_audio: codebook tower failed\n");
return nullptr;
}
if (code0 >= 0) {
frame.assign(out.codes, out.codes + out.n_codes);
}
return out.embd;
}
// the codec is stateful, so frames can be decoded in pieces
bool decode_wav() {
const size_t n_frames = codes_buf.size() / n_cb;
while (n_decoded < n_frames) {
const size_t n = std::min(n_frames - n_decoded, (size_t) 64);
mtmd_gen_inp inp = mtmd_gen_inp_default(mctx);
inp.type = MTMD_GEN_PROCESS_TYPE_GEN_WAV;
inp.codes = codes_buf.data() + n_decoded * n_cb;
inp.n_codes = n * n_cb;
mtmd_gen_out out{};
if (mtmd_gen_audio_process(mctx, gen_state.get(), &inp, &out) != 0) {
LOG_ERR("mtmd_helper_gen_audio: codec decode failed\n");
return false;
}
audio_pcm.insert(audio_pcm.end(), out.audio, out.audio + out.n_samples);
n_decoded += n;
}
return true;
}
static constexpr int n_cb_vocab = 4096; // <|semantic:0..4095|>
llama_token sem_begin = LLAMA_TOKEN_NULL;
llama_token eos = LLAMA_TOKEN_NULL;
llama_seq_id seq_id = 0;
llama_pos pos = 0;
std::vector<llama_token> prompt_tokens;
int prompt_pos = 0;
std::mt19937 rng;
std::vector<int32_t> frame; // codes of the last frame, input of the next tower row
std::vector<llama_token> hist; // last main tokens, for repetition-aware sampling
std::vector<int32_t> codes_buf; // frame-major
size_t n_decoded = 0; // frames already through the codec
std::vector<float> audio_pcm;
std::vector<float> h_state_buf;
mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
std::vector<char> out_buf;
bool stream = false;
size_t pcm_sent = 0; // samples already returned by get_output()
bool wav_header_sent = false;
};
static std::unique_ptr<mtmd_gen_audio_pipeline> make_pipeline(llama_context * lctx, mtmd_context * mctx) {
switch (mtmd_gen_audio_get_info(mctx).type) {
case MTMD_GEN_AUDIO_TYPE_QWEN3TTS:
@@ -2089,6 +2428,8 @@ static std::unique_ptr<mtmd_gen_audio_pipeline> make_pipeline(llama_context * lc
return std::unique_ptr<mtmd_gen_audio_pipeline>(new lfm3a_gen_audio_pipeline(lctx, mctx));
case MTMD_GEN_AUDIO_TYPE_CHATTERBOX:
return std::unique_ptr<mtmd_gen_audio_pipeline>(new chatterbox_gen_audio_pipeline(lctx, mctx));
case MTMD_GEN_AUDIO_TYPE_DRAMA3:
return std::unique_ptr<mtmd_gen_audio_pipeline>(new drama3_gen_audio_pipeline(lctx, mctx));
default:
return nullptr;
}
+10
View File
@@ -2373,6 +2373,10 @@ mtmd_gen_audio_info mtmd_gen_audio_get_info(const mtmd_context * ctx) {
info.type = MTMD_GEN_AUDIO_TYPE_CHATTERBOX;
info.sample_rate = 24000;
break;
case PROJECTOR_TYPE_DRAMA3_GEN:
info.type = MTMD_GEN_AUDIO_TYPE_DRAMA3;
info.sample_rate = 44100;
break;
default:
info.type = MTMD_GEN_AUDIO_TYPE_NONE;
break;
@@ -2776,6 +2780,12 @@ mtmd_gen_inp mtmd_gen_inp_default(const mtmd_context * ctx) {
case PROJECTOR_TYPE_SOPRANO:
inp.type = MTMD_GEN_PROCESS_TYPE_GEN_WAV;
break;
case PROJECTOR_TYPE_DRAMA3_GEN:
// codebook heads, same as the reference (sglang-omni fishaudio_drama3)
inp.top_k = 30;
inp.top_p = 0.9f;
inp.temp = 1.0f;
break;
case PROJECTOR_TYPE_LFM3A_GEN:
{
// MaskGIT sampling runs in the helper, with the settings of the checkpoint
+1
View File
@@ -467,6 +467,7 @@ enum mtmd_gen_audio_type {
MTMD_GEN_AUDIO_TYPE_SOPRANO,
MTMD_GEN_AUDIO_TYPE_LFM3A,
MTMD_GEN_AUDIO_TYPE_CHATTERBOX,
MTMD_GEN_AUDIO_TYPE_DRAMA3,
};
struct mtmd_gen_audio_info {
+3 -6
View File
@@ -128,13 +128,10 @@ static std::string path_to_utf8(const fs::path & p) {
static const std::string & home_dir() {
static const std::string home = [] {
#ifdef _WIN32
// the narrow getenv would return the profile path in the active code page
const wchar_t * w = _wgetenv(L"HOME");
if (w == nullptr) w = _wgetenv(L"USERPROFILE");
return w ? path_to_utf8(fs::path(w)) : std::string();
const std::string h = common_get_env("HOME");
return h.empty() ? common_get_env("USERPROFILE") : h;
#else
const char * h = getenv("HOME");
return h ? std::string(h) : std::string();
return common_get_env("HOME");
#endif
}();
return home;
+31
View File
@@ -154,3 +154,34 @@ The upstream repositories ship raw checkpoints without `config.json`. Download t
python convert_hf_to_gguf.py path/to/chatterbox --outfile chatterbox.gguf
python convert_hf_to_gguf.py path/to/chatterbox --mmproj --outfile mmproj-chatterbox.gguf
```
## Drama 3
[Fish Audio Drama 3](https://huggingface.co/fishaudio/drama-3): a Qwen3.5-MoE backbone (35B, 3B active) samples each frame's main token, a codebook tower samples the 9 other codes, and the Fish DAC decodes them to 44.1 kHz audio.
Available params:
- Speaker tags `<|speaker:0|>` to `<|speaker:15|>` in the text give each character its own voice, a text without a tag starts with `<|speaker:0|>`
- The language is the one of the text, `--tts-lang` is not used
- Note: `--tts-speaker-file` is not supported yet (voice cloning)
- Note: `--top-k` and `--top-p` are not used, frames are sampled with the settings of the reference (main token: temperature 1.0, top-p 0.9, repetition-aware redraw over the last 16 frames; other codes: temperature 1.0, top-k 30, top-p 0.9)
- Each frame is 2048 samples (about 46 ms), generation stops at the end-of-speech token
- The codebook tower keeps the whole sequence in its KV cache, the prompt and the generated frames are limited to 4096 positions
Example usage:
```sh
llama-tts -m drama3.gguf \
-mm mmproj-drama3.gguf \
-p "<|speaker:0|> Did you remember to buy the bread? <|speaker:1|> Of course I did, it is on the kitchen table." \
-n 1000 \
--output out.wav
```
**Note for GGUF conversion:**
The routed experts of the checkpoint are FP8, they are dequantized at conversion. The mmproj holds the codebook tower and the decoder half of `codec.pth`, it is best kept in BF16:
```sh
python convert_hf_to_gguf.py path/to/drama-3 --outtype q8_0 --outfile drama3.gguf
python convert_hf_to_gguf.py path/to/drama-3 --mmproj --outtype bf16 --outfile mmproj-drama3.gguf
```
@@ -106,18 +106,22 @@
{readonly}
width={imageWidth}
/>
{:else if isPdfFile(item.attachment, item.uploadedFile)}
<ChatAttachmentsListItemThumbnailFile
attachment={item.attachment}
class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
{:else if isPdfFile(item.attachment, item.uploadedFile) && item.preview}
{@const pageCount =
item.attachment?.type === AttachmentType.PDF ? (item.attachment.images?.length ?? 0) : 0}
<ChatAttachmentsListItemThumbnailImage
class="{layoutClasses} cursor-pointer {className} {scrollClasses}"
height={imageHeight}
id={item.id}
{imageClass}
label={pageCount > 0 ? `PDF, ${pageCount} ${pageCount === 1 ? 'page' : 'pages'}` : 'PDF'}
name={item.name}
onRemove={onFileRemove}
onclick={() => onPreview?.(item)}
preview={item.preview}
{readonly}
size={item.size}
textContent={item.textContent}
uploadedFile={item.uploadedFile}
width={imageWidth}
/>
{:else}
<ChatAttachmentsListItemThumbnailFile
@@ -7,6 +7,7 @@
height?: string;
id: string;
imageClass?: string;
label?: string;
onclick?: (event?: MouseEvent) => void;
onRemove?: (id: string) => void;
name: string;
@@ -20,6 +21,7 @@
height = 'h-16',
id,
imageClass = '',
label,
name,
onclick,
onRemove,
@@ -49,6 +51,14 @@
{@render image()}
{/if}
{#if label}
<span
class="pointer-events-none absolute bottom-1 left-1 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-white"
>
{label}
</span>
{/if}
{#if !readonly}
<div
class="absolute top-1 right-1 flex items-center justify-center opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
@@ -32,6 +32,7 @@
isImage: boolean;
isAudio: boolean;
isVideo: boolean;
isPdf: boolean;
}
interface Props {
@@ -58,6 +59,7 @@
...item,
isAudio: isAudioFile(item.attachment, item.uploadedFile),
isImage: isImageFile(item.attachment, item.uploadedFile),
isPdf: isPdfFile(item.attachment, item.uploadedFile),
isVideo: isVideoFile(item.attachment, item.uploadedFile)
})
)
@@ -3,11 +3,11 @@
import { SyntaxHighlightedCode } from '$lib/components/app';
import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { PdfViewMode } from '$lib/enums';
import { ICON_CLASS_DEFAULT, PDF_AS_IMAGES_LEGACY_CONTENT_REGEX } from '$lib/constants';
import { AttachmentType, MimeTypeApplication, PdfViewMode } from '$lib/enums';
import type { ChatAttachmentDisplayItem } from '$lib/types';
import { getLanguageFromFilename } from '$lib/utils';
import { convertPDFToImage } from '$lib/utils/browser-only';
import { convertPDFToImage, convertPDFToText } from '$lib/utils/browser-only';
interface Props {
currentItem: ChatAttachmentDisplayItem | null;
@@ -23,54 +23,70 @@
let pdfViewMode = $state<PdfViewMode>(PdfViewMode.PAGES);
let pdfImages = $state<string[]>([]);
let pdfImagesLoading = $state(false);
// a failure stays on screen: the effect does not retry while an error is set
let pdfImagesError = $state<string | null>(null);
let pdfText = $state<string | null>(null);
let pdfTextLoading = $state(false);
let pdfTextError = $state<string | null>(null);
let language = $derived(getLanguageFromFilename(displayName));
// older PDFs sent as images stored a placeholder in `content`, so their text is extracted again
let hasStoredText = $derived(
currentItem?.attachment?.type === AttachmentType.PDF
? !(
currentItem.attachment.processedAsImages &&
PDF_AS_IMAGES_LEGACY_CONTENT_REGEX.test(currentItem.attachment.content)
)
: !!displayTextContent
);
let isLoading = $derived(pdfImagesLoading || pdfTextLoading);
function getPdfFile(): File | null {
if (currentItem?.uploadedFile?.file) {
return currentItem.uploadedFile.file;
}
if (
currentItem?.attachment &&
'base64Data' in currentItem.attachment &&
currentItem.attachment.base64Data
) {
const byteArray = Uint8Array.from(atob(currentItem.attachment.base64Data), (char) =>
char.charCodeAt(0)
);
return new File([byteArray], displayName, { type: MimeTypeApplication.PDF });
}
return null;
}
async function loadPdfImages() {
if (pdfImages.length > 0 || pdfImagesLoading || !currentItem) return;
if (
currentItem.attachment?.type === AttachmentType.PDF &&
currentItem.attachment.images?.length
) {
pdfImages = currentItem.attachment.images;
return;
}
if (pdfImagesError) return;
pdfImagesLoading = true;
pdfImagesError = null;
try {
let file: File | null = null;
const file = getPdfFile();
if (currentItem.uploadedFile?.file) {
file = currentItem.uploadedFile.file;
} else if (currentItem.attachment) {
// Check if we have pre-processed images
if (
'images' in currentItem.attachment &&
currentItem.attachment.images &&
Array.isArray(currentItem.attachment.images) &&
currentItem.attachment.images.length > 0
) {
pdfImages = currentItem.attachment.images;
return;
}
// Convert base64 back to File for processing
if ('base64Data' in currentItem.attachment && currentItem.attachment.base64Data) {
const base64Data = currentItem.attachment.base64Data;
const byteCharacters = atob(base64Data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
file = new File([byteArray], displayName, { type: 'application/pdf' });
}
}
if (file) {
pdfImages = await convertPDFToImage(file);
} else {
if (!file) {
throw new Error('No PDF file available for conversion');
}
pdfImages = await convertPDFToImage(file);
} catch (error) {
pdfImagesError = error instanceof Error ? error.message : 'Failed to load PDF images';
} finally {
@@ -78,26 +94,62 @@
}
}
async function loadPdfText() {
if (pdfText !== null || pdfTextLoading || !currentItem) return;
if (hasStoredText) {
pdfText = displayTextContent ?? '';
return;
}
if (pdfTextError) return;
pdfTextLoading = true;
try {
const file = getPdfFile();
if (!file) {
throw new Error('No PDF file available for text extraction');
}
pdfText = await convertPDFToText(file);
} catch (error) {
pdfTextError = error instanceof Error ? error.message : 'Failed to extract PDF text';
} finally {
pdfTextLoading = false;
}
}
$effect(() => {
if (pdfViewMode === PdfViewMode.PAGES) {
loadPdfImages();
} else {
loadPdfText();
}
});
</script>
<div class="mb-4 flex items-center justify-end gap-2">
<Button
disabled={pdfImagesLoading}
disabled={isLoading}
onclick={() => (pdfViewMode = PdfViewMode.TEXT)}
size="sm"
variant={pdfViewMode === PdfViewMode.TEXT ? 'default' : 'outline'}
>
<FileText class="mr-1 {ICON_CLASS_DEFAULT}" />
{#if pdfTextLoading}
<div
class="mr-1 {ICON_CLASS_DEFAULT} animate-spin rounded-full border-2 border-current border-t-transparent"
></div>
{:else}
<FileText class="mr-1 {ICON_CLASS_DEFAULT}" />
{/if}
Text
</Button>
<Button
disabled={pdfImagesLoading}
disabled={isLoading}
onclick={() => (pdfViewMode = PdfViewMode.PAGES)}
size="sm"
variant={pdfViewMode === PdfViewMode.PAGES ? 'default' : 'outline'}
@@ -136,28 +188,24 @@
</Alert.Root>
{/if}
{#if pdfImagesLoading}
<div class="flex flex-1 items-center justify-center p-8">
<div class="text-center">
<div
class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-white border-t-transparent"
></div>
<p class="text-white/70">Converting PDF to images...</p>
{#if pdfViewMode === PdfViewMode.TEXT}
{#if pdfTextLoading}
{@render loadingStatus('Extracting PDF text...')}
{:else if pdfTextError}
{@render status('Failed to extract PDF text', pdfTextError)}
{:else if pdfText}
<div class="px-4 pb-4">
<SyntaxHighlightedCode class="max-w-4xl" code={pdfText} {language} maxHeight="none" />
</div>
</div>
{:else}
{@render status('No text found in this PDF')}
{/if}
{:else if pdfImagesLoading}
{@render loadingStatus('Converting PDF to images...')}
{:else if pdfImagesError}
<div class="flex flex-1 items-center justify-center p-8">
<div class="text-center">
<FileText class="mx-auto mb-4 h-16 w-16 text-white/50" />
<p class="mb-4 text-white/70">Failed to load PDF images</p>
<p class="text-sm text-white/50">{pdfImagesError}</p>
</div>
</div>
{@render status('Failed to load PDF images', pdfImagesError)}
{:else if pdfImages.length > 0}
{#each pdfImages as image, index (image)}
{#each pdfImages as image, index (index)}
<p class="mb-2 text-sm text-white/50">Page {index + 1}</p>
<img alt="PDF Page {index + 1}" class="mx-auto max-w-[85vw] rounded-lg shadow-lg" src={image} />
@@ -165,22 +213,31 @@
<div class="h-4"></div>
{/each}
{:else}
{@render status('No PDF pages available')}
{/if}
{#snippet loadingStatus(message: string)}
<div class="flex flex-1 items-center justify-center p-8">
<div class="text-center">
<div
class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-white border-t-transparent"
></div>
<p class="text-white/70">{message}</p>
</div>
</div>
{/snippet}
{#snippet status(message: string, detail?: string)}
<div class="flex flex-1 items-center justify-center p-8">
<div class="text-center">
<FileText class="mx-auto mb-4 h-16 w-16 text-white/50" />
<p class="text-white/70">No PDF pages available</p>
<p class={['text-white/70', detail && 'mb-4']}>{message}</p>
{#if detail}
<p class="text-sm text-white/50">{detail}</p>
{/if}
</div>
</div>
{/if}
{#if pdfViewMode === PdfViewMode.TEXT && displayTextContent}
<div class="px-4 pb-4">
<SyntaxHighlightedCode
class="max-w-4xl"
code={displayTextContent}
{language}
maxHeight="none"
/>
</div>
{/if}
{/snippet}
@@ -10,6 +10,7 @@
isImage: boolean;
isAudio: boolean;
isVideo: boolean;
isPdf: boolean;
preview?: string;
}
@@ -46,7 +47,7 @@
]}
onclick={() => onNavigate(index)}
>
{#if item.isImage && item.preview}
{#if (item.isImage || item.isPdf) && item.preview}
<img alt={item.name} class="h-12 w-12 object-cover" src={item.preview} />
{:else}
<div
@@ -74,6 +74,9 @@ export const IMAGE_FILE_TYPES = {
}
} as const;
/** `content` that messages stored before the text was kept carry for a PDF sent as images. */
export const PDF_AS_IMAGES_LEGACY_CONTENT_REGEX = /^PDF file with \d+ pages$/;
export const PDF_FILE_TYPES = {
[FileTypePdf.PDF]: {
extensions: [FileExtensionPdf.PDF],
+10 -1
View File
@@ -71,13 +71,22 @@ export function getAttachmentDisplayItems(
for (const [index, attachment] of attachments.entries()) {
const isImage = isImageFile(attachment);
let preview: string | undefined;
if (isImage && 'base64Url' in attachment) {
preview = attachment.base64Url;
} else if (attachment.type === AttachmentType.PDF && attachment.processedAsImages) {
// first rendered page is the thumbnail of a PDF sent as images
preview = attachment.images?.[0];
}
items.push({
attachment,
attachmentIndex: index,
id: `attachment-${index}`,
isImage,
name: attachment.name,
preview: isImage && 'base64Url' in attachment ? attachment.base64Url : undefined,
preview,
size: 'size' in attachment ? attachment.size : undefined,
textContent: 'content' in attachment ? attachment.content : undefined
});
@@ -140,6 +140,8 @@ export async function parseFilesToMessageExtras(
// Process PDF as images (only for vision models)
try {
const images = await convertPDFToImage(file.file);
// kept for the preview's Text view; the model only receives the images
const content = file.textContent ?? (await convertPDFToText(file.file).catch(() => ''));
// Show success toast for PDF image processing
toast.success(
@@ -151,7 +153,7 @@ export async function parseFilesToMessageExtras(
extras.push({
base64Data: base64Data,
content: `PDF file with ${images.length} pages`,
content,
images: images,
name: file.name,
processedAsImages: true,
@@ -0,0 +1,47 @@
<script lang="ts" module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import ChatAttachmentsPreview from '$lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreview.svelte';
import { AttachmentType } from '$lib/enums';
import { expect, waitFor } from 'storybook/test';
const { Story } = defineMeta({
component: ChatAttachmentsPreview,
parameters: {
layout: 'fullscreen'
},
title: 'Components/ChatScreen/ChatAttachments/ChatAttachmentsPreview'
});
// same backdrop as DialogChatAttachmentsPreview
const frameClass = 'flex h-screen flex-col bg-neutral-950';
// valid base64 that is not a PDF: pdf.js throws InvalidPDFException
const brokenPdfAttachment: DatabaseMessageExtraPdfFile = {
base64Data: btoa('this is not a pdf'),
content: 'Extracted text of the broken PDF.',
name: 'broken.pdf',
processedAsImages: false,
size: 17,
type: AttachmentType.PDF
};
</script>
<!-- conversion fails once: shows the error and does not retry -->
<Story
asChild
name="PdfConversionError"
play={async ({ canvas }) => {
await waitFor(() => expect(canvas.getByText('Failed to load PDF images')).toBeInTheDocument(), {
timeout: 5000
});
// a retry loop would flip back to the loading state
await new Promise((resolve) => setTimeout(resolve, 500));
await expect(canvas.queryByText('Converting PDF to images...')).not.toBeInTheDocument();
await expect(canvas.getByText('Failed to load PDF images')).toBeInTheDocument();
}}
>
<div class={frameClass}>
<ChatAttachmentsPreview attachments={[brokenPdfAttachment]} class="min-h-0 flex-1" />
</div>
</Story>