diff --git a/docs/wan.md b/docs/wan.md
index 02b46d62a..ebe4ebe0a 100644
--- a/docs/wan.md
+++ b/docs/wan.md
@@ -34,6 +34,10 @@
- Wan2.2 I2V A14B
- safetensors: https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/tree/main/split_files/diffusion_models
- gguf: https://huggingface.co/QuantStack/Wan2.2-I2V-A14B-GGUF/tree/main
+ - Wan2.2 S2V 14B
+ - safetensors: https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/tree/main/split_files/diffusion_models
+ - gguf: https://huggingface.co/QuantStack/Wan2.2-S2V-14B-GGUF/tree/main
+ - int8_convrot safetensors: https://huggingface.co/noctrex/Wan2.2-S2V-14B-int8_convrot
- Download vae
- wan_2.1_vae (for all the wan model except Wan2.2 TI2V 5B)
- safetensors: https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/blob/main/split_files/vae/wan_2.1_vae.safetensors
@@ -49,6 +53,9 @@
- Download clip_vison_h (for Wan2.1 I2V/FLF2V only)
- safetensors: https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/blob/main/split_files/clip_vision/clip_vision_h.safetensors
+- Download audio_encoder (for Wan2.2 S2V only)
+ - safetensors: https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/blob/main/split_files/audio_encoders/wav2vec2_large_english_fp16.safetensors
+
## Examples
@@ -94,6 +101,66 @@
+### Wan2.2 S2V 14B
+
+Audio-driven video (speech-to-video). The reference image (`-i`) is the speaker
+portrait, `--audio` is the driving audio track and `--audio-encoder` is the
+wav2vec2 audio encoder. Wan2.2 S2V requires the wan_2.1 vae (16 channel), not
+the wan2.2 vae.
+
+```
+.\bin\Release\sd-cli.exe -M vid_gen --diffusion-model ..\models\diffusion_models\wan2.2_s2v-14B-Q8_0.gguf --audio-encoder ..\models\audio_encoders\wav2vec2_large_english_fp16.safetensors --vae ..\models\vae\wan_2.1_vae.safetensors --t5xxl ..\models\text_encoders\umt5-xxl-encoder-Q8_0.gguf -p "a person is talking" --cfg-scale 6.0 --steps 20 --sampling-method euler -v -n "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" -W 832 -H 480 --diffusion-fa --offload-to-cpu --vae-tiling --video-frames 81 -i ..\assets\cat_with_sd_cpp_42.png --audio .\input\speech.wav --flow-shift 3.0
+```
+
+Notes:
+
+- Recommended settings: `--sampling-method euler --steps 20 --cfg-scale 6.0`.
+ `dpm++2m` produces heavy artifacts on S2V. 4 steps with the lightning LoRA
+ (below) is the fast option.
+- Resolutions: width and height must be multiples of 16; the examples use
+ multiples of 64. 832x480 is a fast starting point; generation cost scales
+ with pixel area.
+- Use `--diffusion-fa` together with `--offload-to-cpu --vae-tiling`. Without
+ flash attention the S2V attention compute buffer alone needs ~40 GB VRAM at
+ 480x832; `--offload-to-cpu` keeps the model weights in system RAM, and
+ `--vae-tiling` keeps the video VAE decode (77-81 frames) within VRAM.
+- `--audio` accepts a WAV file; it is downmixed to mono and resampled to 16 kHz
+ internally. Audio longer than the video is truncated, video longer than the
+ audio is padded with silence. Pick `--video-frames` to match the audio:
+ roughly `audio_seconds * 16` frames, capped at one chunk (77-81 frames,
+ ~5 s at the model's 16 fps). 33, 77 and 81 map to clean latent frame counts.
+- The output video carries the driving audio track: it is muxed into `.avi`
+ / `.webm` outputs, truncated to the video duration. Other container types
+ fall back to a `.wav` sidecar file next to the video.
+- Approximate VRAM use at 640x368, 13 frames:
+
+ | checkpoint | VRAM |
+ |------------|------|
+ | wan2.2_s2v_14B_int8_convrot.safetensors | 17.6 GB |
+ | wan2.2_s2v-14B-Q8_0.gguf | 18.4 GB |
+ | wan2.2_s2v-14B-Q4_K_M.gguf | 13.4 GB |
+
+ Q4_K_M is the lowest-memory option; bf16 and fp8_scaled need more than
+ 24 GB VRAM.
+- One generation covers the first S2V chunk window (`--video-frames` frames).
+ Long-video chunked extend mode is not implemented yet.
+- Speed: the lightx2v lightning LoRA works with S2V at 4 steps and
+ `--cfg-scale 1.0`. Use the **low_noise** variant;
+ the high_noise variant produces artifacts on S2V:
+
+ ```
+ --lora-model-dir ..\models\loras
+ -p "..."
+ --cfg-scale 1.0 --steps 4
+ ```
+
+ Expect some quality/dynamics loss compared to the full 20-step run.
+- fp8_scaled and GGUF checkpoints are supported. The int8_convrot checkpoint
+ is available at
+ https://huggingface.co/noctrex/Wan2.2-S2V-14B-int8_convrot
+ ([int8_convrot](int8_convrot.md)); int8 convrot is supported on the CUDA,
+ Vulkan and ROCm backends.
+
### Wan2.2 T2V A14B T2I
```
diff --git a/examples/common/common.cpp b/examples/common/common.cpp
index 35812157c..fb790bb3e 100644
--- a/examples/common/common.cpp
+++ b/examples/common/common.cpp
@@ -412,6 +412,11 @@ ArgOptions SDContextParams::get_options() {
"path to standalone LTX audio vae model",
0,
&audio_vae_path},
+ {"",
+ "--audio-encoder",
+ "path to wav2vec2 audio encoder model (Wan2.2 S2V)",
+ 0,
+ &audio_encoder_path},
{"",
"--taesd",
"path to taesd. Using Tiny AutoEncoder for fast decoding (low quality)",
@@ -817,6 +822,7 @@ std::string SDContextParams::to_string() const {
<< " vae_path: \"" << vae_path << "\",\n"
<< " vae_format: \"" << vae_format << "\",\n"
<< " audio_vae_path: \"" << audio_vae_path << "\",\n"
+ << " audio_encoder_path: \"" << audio_encoder_path << "\",\n"
<< " taesd_path: \"" << taesd_path << "\",\n"
<< " esrgan_path: \"" << esrgan_path << "\",\n"
<< " control_net_path: \"" << control_net_path << "\",\n"
@@ -879,6 +885,7 @@ sd_ctx_params_t SDContextParams::to_sd_ctx_params_t(bool taesd_preview) {
sd_ctx_params.embeddings_connectors_path = embeddings_connectors_path.c_str();
sd_ctx_params.vae_path = vae_path.c_str();
sd_ctx_params.audio_vae_path = audio_vae_path.c_str();
+ sd_ctx_params.audio_encoder_path = audio_encoder_path.c_str();
sd_ctx_params.taesd_path = taesd_path.c_str();
sd_ctx_params.control_net_path = control_net_path.c_str();
sd_ctx_params.ip_adapter_path = ip_adapter_path.c_str();
@@ -1428,6 +1435,16 @@ ArgOptions SDGenerationParams::get_options() {
return 1;
};
+ auto on_audio_arg = [&](int argc, const char** argv, int index) {
+ if (++index >= argc) {
+ return -1;
+ }
+ // S2V driving track shares the ref-audio transport (Wan2.2 S2V reads
+ // sd_vid_gen_params.ref_audios[0]).
+ ref_audio_paths.push_back(argv[index]);
+ return 1;
+ };
+
auto on_cache_mode_arg = [&](int argc, const char** argv, int index) {
if (++index >= argc) {
return -1;
@@ -1617,6 +1634,10 @@ ArgOptions SDGenerationParams::get_options() {
"--ref-audio",
"standalone WAV reference for MiniMax-H3 Ref2VA (can be used multiple times)",
on_ref_audio_arg},
+ {"",
+ "--audio",
+ "driving audio track (Wan2.2 S2V; can be used once)",
+ on_audio_arg},
{"",
"--cache-mode",
"caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level), 'spectrum' (UNET/DiT Chebyshev+Taylor forecasting)",
diff --git a/examples/common/common.h b/examples/common/common.h
index 34b4a013b..47a7ec5c0 100644
--- a/examples/common/common.h
+++ b/examples/common/common.h
@@ -130,6 +130,7 @@ struct SDContextParams {
std::string vae_path;
std::string vae_format = "auto";
std::string audio_vae_path;
+ std::string audio_encoder_path;
std::string taesd_path;
std::string esrgan_path;
std::string control_net_path;
diff --git a/examples/common/media_io.cpp b/examples/common/media_io.cpp
index 9ebd0b5a1..62d58ffba 100644
--- a/examples/common/media_io.cpp
+++ b/examples/common/media_io.cpp
@@ -1,4 +1,5 @@
#include "media_io.h"
+#include "conditioning/audio_processing.hpp"
#include "log.h"
#include "resource_owners.hpp"
@@ -10,6 +11,7 @@
#include
#include
#include
+#include
#include
#include
@@ -1506,3 +1508,35 @@ bool load_wav_from_file(const std::string& path,
}
return true;
}
+
+// DSP core lives in src/conditioning/audio_processing.hpp (shared with
+// stable-diffusion.cpp); these wrappers keep the examples-side API.
+std::vector downmix_to_mono(const float* interleaved_samples,
+ uint64_t sample_count,
+ uint32_t channels) {
+ return AudioProcessing::downmix_to_mono(interleaved_samples, sample_count, channels);
+}
+
+std::vector resample_audio(const float* samples,
+ uint64_t sample_count,
+ uint32_t orig_sample_rate,
+ uint32_t target_sample_rate) {
+ return AudioProcessing::resample_audio(samples, sample_count, orig_sample_rate, target_sample_rate);
+}
+
+bool load_wav_from_file_mono(const std::string& path,
+ std::vector& mono_samples,
+ uint32_t target_sample_rate) {
+ std::vector interleaved;
+ uint32_t sample_rate = 0;
+ uint32_t channels = 0;
+ if (!load_wav_from_file(path, interleaved, sample_rate, channels)) {
+ return false;
+ }
+ const uint64_t frames = channels > 0 ? interleaved.size() / channels : 0;
+ mono_samples = downmix_to_mono(interleaved.data(), frames, channels);
+ if (sample_rate != target_sample_rate && !mono_samples.empty()) {
+ mono_samples = resample_audio(mono_samples.data(), mono_samples.size(), sample_rate, target_sample_rate);
+ }
+ return true;
+}
diff --git a/examples/common/media_io.h b/examples/common/media_io.h
index 52b9221a2..0c2230674 100644
--- a/examples/common/media_io.h
+++ b/examples/common/media_io.h
@@ -115,4 +115,22 @@ bool load_wav_from_file(const std::string& path,
uint32_t& sample_rate,
uint32_t& channels);
+// Downmix interleaved samples to mono by averaging channels. Returns empty vector on invalid input.
+std::vector downmix_to_mono(const float* interleaved_samples,
+ uint64_t sample_count,
+ uint32_t channels);
+
+// Band-limited sinc resampler matching torchaudio.functional.resample
+// (sinc_interp_hann, lowpass_filter_width 6, rolloff 0.99). Returns the input unchanged
+// when sample rates are equal, and an empty vector on invalid input.
+std::vector resample_audio(const float* samples,
+ uint64_t sample_count,
+ uint32_t orig_sample_rate,
+ uint32_t target_sample_rate);
+
+// Convenience: load WAV, downmix to mono and resample to the target sample rate.
+bool load_wav_from_file_mono(const std::string& path,
+ std::vector& mono_samples,
+ uint32_t target_sample_rate = 16000);
+
#endif // __MEDIA_IO_H__
diff --git a/include/stable-diffusion.h b/include/stable-diffusion.h
index d5cda6d9a..98c7704b7 100644
--- a/include/stable-diffusion.h
+++ b/include/stable-diffusion.h
@@ -206,6 +206,7 @@ typedef struct {
const char* embeddings_connectors_path;
const char* vae_path;
const char* audio_vae_path;
+ const char* audio_encoder_path;
const char* taesd_path;
const char* control_net_path;
const char* ip_adapter_path;
diff --git a/src/conditioning/audio_processing.hpp b/src/conditioning/audio_processing.hpp
new file mode 100644
index 000000000..88eb7a731
--- /dev/null
+++ b/src/conditioning/audio_processing.hpp
@@ -0,0 +1,228 @@
+#ifndef __SD_CONDITIONING_AUDIO_PROCESSING_HPP__
+#define __SD_CONDITIONING_AUDIO_PROCESSING_HPP__
+
+// Wan2.2-S2V audio windowing, ported from ComfyUI comfy_extras/nodes_wan.py
+// (linear_interpolation + get_audio_embed_bucket_fps, m=0).
+//
+// Input is the wav2vec2 hidden states stacked per layer [num_layers, in_frames, dim]
+// at the encoder frame rate (50 Hz). The frames are interpolated to video_rate
+// (30 Hz), bucketed to fps (16) frames with zero padding past the audio end, and
+// split into chunks of batch_frames = latent_t * 4 frames (one per diffusion chunk).
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace AudioProcessing {
+
+ // Chunk/padding math of get_audio_embed_bucket_fps (m=0).
+ struct BucketPlan {
+ int audio_frames; // input frames at video_rate (30 Hz)
+ int batch_frames; // pixel frames per chunk (latent_t * 4)
+ int video_rate; // timeline rate of audio_frames (30 Hz)
+ int fps; // bucket frame rate (16 fps)
+ int num_chunks; // ComfyUI num_repeat
+ int bucket_frames; // total bucket frames = num_chunks * batch_frames
+ int padded_audio_frames; // audio_frames plus zero padding applied
+ };
+
+ inline BucketPlan plan_buckets(int audio_frames, int batch_frames, int video_rate = 30, int fps = 16) {
+ BucketPlan plan;
+ plan.audio_frames = audio_frames;
+ plan.batch_frames = batch_frames;
+ plan.video_rate = video_rate;
+ plan.fps = fps;
+ const double scale = static_cast(video_rate) / fps;
+ // min_batch_num = int(audio_frame_num / (batch_frames * scale)) + 1
+ plan.num_chunks = static_cast(audio_frames / (batch_frames * scale)) + 1;
+ plan.bucket_frames = plan.num_chunks * batch_frames;
+ // padd_audio_num = ceil(bucket_frames / fps * video_rate) - audio_frame_num
+ plan.padded_audio_frames = static_cast(
+ std::ceil(plan.bucket_frames / static_cast(fps) * video_rate));
+ return plan;
+ }
+
+ // Bucket frame index (fps timeline) -> source frame index (video_rate timeline).
+ // get_sample_indices with fixed_start=0 reduces to round-half-even(i * video_rate / fps),
+ // matching numpy's default rounding.
+ inline int bucket_source_frame(int bucket_frame, int video_rate = 30, int fps = 16) {
+ return static_cast(std::nearbyint(static_cast(bucket_frame) * video_rate / fps));
+ }
+
+ // torch.nn.functional.interpolate size computation: output_len = int(in_len / input_fps * output_fps)
+ inline int interpolated_frame_count(int in_frames, int input_fps = 50, int output_fps = 30) {
+ return static_cast(in_frames / static_cast(input_fps) * output_fps);
+ }
+
+ // torch.nn.functional.interpolate(mode='linear', align_corners=True) along the frame
+ // dimension. in: [num_layers, in_frames, dim], out: [num_layers, out_frames, dim].
+ inline std::vector linear_interpolate_frames(const std::vector& in,
+ int num_layers,
+ int in_frames,
+ int dim,
+ int out_frames) {
+ std::vector out(static_cast(num_layers) * out_frames * dim, 0.0f);
+ if (in.empty() || in_frames <= 0 || out_frames <= 0 || num_layers <= 0 || dim <= 0) {
+ return out;
+ }
+ const double scale = out_frames > 1 ? static_cast(in_frames - 1) / (out_frames - 1) : 0.0;
+ for (int layer = 0; layer < num_layers; ++layer) {
+ for (int out_i = 0; out_i < out_frames; ++out_i) {
+ const double pos = out_i * scale;
+ const int src0 = static_cast(pos);
+ const int src1 = std::min(src0 + 1, in_frames - 1);
+ const float frac = static_cast(pos - src0);
+ const float* in_row = &in[(static_cast(layer) * in_frames + src0) * dim];
+ const float* in_next = &in[(static_cast(layer) * in_frames + src1) * dim];
+ float* out_row = &out[(static_cast(layer) * out_frames + out_i) * dim];
+ for (int d = 0; d < dim; ++d) {
+ out_row[d] = in_row[d] * (1.0f - frac) + in_next[d] * frac;
+ }
+ }
+ }
+ return out;
+ }
+
+ // Polyphase FIR from torchaudio.functional.resample: sinc interpolated at output
+ // phases, hann-windowed over lowpass_filter_width zero crossings, anti-aliased by
+ // clamping the sinc argument to +-lowpass_filter_width after scaling by min(rate)*rolloff.
+ // Returns the input unchanged when sample rates are equal, and an empty vector on
+ // invalid input.
+ inline std::vector resample_audio(const float* samples,
+ uint64_t sample_count,
+ uint32_t orig_sample_rate,
+ uint32_t target_sample_rate) {
+ if (samples == nullptr || sample_count == 0 || orig_sample_rate == 0 || target_sample_rate == 0) {
+ return {};
+ }
+ if (orig_sample_rate == target_sample_rate) {
+ return std::vector(samples, samples + sample_count);
+ }
+
+ constexpr int kLowpassFilterWidth = 6;
+ constexpr double kRolloff = 0.99;
+ constexpr double kPi = 3.14159265358979323846;
+
+ const uint64_t gcd = std::gcd(static_cast(orig_sample_rate),
+ static_cast(target_sample_rate));
+ const int64_t orig_freq = static_cast(orig_sample_rate / gcd);
+ const int64_t new_freq = static_cast(target_sample_rate / gcd);
+ const double base_freq = static_cast(std::min(orig_freq, new_freq)) * kRolloff;
+ const int64_t width = static_cast(std::ceil(kLowpassFilterWidth * orig_freq / base_freq));
+ const int64_t kernel_size = 2 * width + orig_freq;
+
+ std::vector kernel(static_cast(new_freq) * kernel_size);
+ for (int64_t j = 0; j < new_freq; ++j) {
+ for (int64_t i = 0; i < kernel_size; ++i) {
+ double t = -static_cast(j) / new_freq + static_cast(i - width) / orig_freq;
+ t *= base_freq;
+ t = std::clamp(t, -static_cast(kLowpassFilterWidth), static_cast(kLowpassFilterWidth));
+ const double cos_arg = std::cos(t * kPi / kLowpassFilterWidth / 2);
+ const double window = cos_arg * cos_arg;
+ double s = t * kPi;
+ const double sinc = (s == 0.0) ? 1.0 : std::sin(s) / s;
+ kernel[j * kernel_size + i] = sinc * window * (base_freq / orig_freq);
+ }
+ }
+
+ const uint64_t num_phases = static_cast(sample_count / orig_freq) + 1;
+ const uint64_t target_length = (static_cast(new_freq) * sample_count +
+ static_cast(orig_freq) - 1) /
+ static_cast(orig_freq);
+ std::vector out(target_length);
+ for (uint64_t phase = 0; phase < num_phases; ++phase) {
+ const int64_t src_base = static_cast(phase * orig_freq) - width;
+ for (int64_t j = 0; j < new_freq; ++j) {
+ const uint64_t out_index = phase * new_freq + j;
+ if (out_index >= target_length) {
+ break;
+ }
+ const double* k = &kernel[j * kernel_size];
+ double acc = 0.0;
+ for (int64_t i = 0; i < kernel_size; ++i) {
+ const int64_t src = src_base + i;
+ if (src >= 0 && src < static_cast(sample_count)) {
+ acc += samples[src] * k[i];
+ }
+ }
+ out[out_index] = static_cast(acc);
+ }
+ }
+ return out;
+ }
+
+ // Downmix interleaved samples to mono by averaging channels. Returns an empty
+ // vector on invalid input.
+ inline std::vector downmix_to_mono(const float* interleaved_samples,
+ uint64_t sample_count,
+ uint32_t channels) {
+ std::vector mono;
+ if (interleaved_samples == nullptr || sample_count == 0 || channels == 0) {
+ return mono;
+ }
+ mono.resize(static_cast(sample_count));
+ if (channels == 1) {
+ std::memcpy(mono.data(), interleaved_samples, static_cast(sample_count) * sizeof(float));
+ return mono;
+ }
+ const float scale = 1.0f / static_cast(channels);
+ for (uint64_t i = 0; i < sample_count; ++i) {
+ float sum = 0.0f;
+ for (uint32_t c = 0; c < channels; ++c) {
+ sum += interleaved_samples[i * channels + c];
+ }
+ mono[static_cast(i)] = sum * scale;
+ }
+ return mono;
+ }
+
+ // Full bucketing: stacked encoder states [num_layers, in_frames, dim] at input_fps ->
+ // bucket frames [bucket_frames, num_layers, dim] at fps, with zero frames past the
+ // audio end. Chunk c occupies rows [c * batch_frames, (c + 1) * batch_frames).
+ // Returns an empty vector on invalid input; the applied plan is stored in *plan_out.
+ inline std::vector build_audio_buckets(const float* stacked_states,
+ int num_layers,
+ int in_frames,
+ int dim,
+ int batch_frames,
+ BucketPlan* plan_out = nullptr,
+ int input_fps = 50,
+ int video_rate = 30,
+ int fps = 16) {
+ if (stacked_states == nullptr || num_layers <= 0 || in_frames <= 0 || dim <= 0 || batch_frames <= 0) {
+ return {};
+ }
+ const int audio_frames = interpolated_frame_count(in_frames, input_fps, video_rate);
+ if (audio_frames <= 0) {
+ return {};
+ }
+ const std::vector interpolated =
+ linear_interpolate_frames(std::vector(stacked_states,
+ stacked_states + static_cast(num_layers) * in_frames * dim),
+ num_layers,
+ in_frames,
+ dim,
+ audio_frames);
+ const BucketPlan plan = plan_buckets(audio_frames, batch_frames, video_rate, fps);
+ if (plan_out != nullptr) {
+ *plan_out = plan;
+ }
+ std::vector buckets(static_cast(plan.bucket_frames) * num_layers * dim, 0.0f);
+ for (int frame = 0; frame < plan.bucket_frames; ++frame) {
+ const int src = bucket_source_frame(frame, video_rate, fps);
+ if (src >= plan.audio_frames) {
+ continue; // zero padding past the audio end
+ }
+ std::copy_n(interpolated.data() + static_cast(src) * num_layers * dim,
+ static_cast(num_layers) * dim,
+ buckets.data() + static_cast(frame) * num_layers * dim);
+ }
+ return buckets;
+ }
+
+} // namespace AudioProcessing
+
+#endif // __SD_CONDITIONING_AUDIO_PROCESSING_HPP__
diff --git a/src/core/ggml_extend_backend.cpp b/src/core/ggml_extend_backend.cpp
index a83166438..74ac1c425 100644
--- a/src/core/ggml_extend_backend.cpp
+++ b/src/core/ggml_extend_backend.cpp
@@ -87,6 +87,10 @@ static bool parse_backend_module(const std::string& raw_name, SDBackendModule* m
*module = SDBackendModule::DETECTOR;
return true;
}
+ if (name == "audioencoder" || name == "audio") {
+ *module = SDBackendModule::AUDIO_ENCODER;
+ return true;
+ }
return false;
}
@@ -962,6 +966,8 @@ const char* sd_backend_module_name(SDBackendModule module) {
return "upscaler";
case SDBackendModule::DETECTOR:
return "detector";
+ case SDBackendModule::AUDIO_ENCODER:
+ return "audio_encoder";
}
return "unknown";
}
diff --git a/src/core/ggml_extend_backend.h b/src/core/ggml_extend_backend.h
index d5498e8f4..e3cda0efc 100644
--- a/src/core/ggml_extend_backend.h
+++ b/src/core/ggml_extend_backend.h
@@ -21,6 +21,7 @@ enum class SDBackendModule {
PHOTOMAKER,
UPSCALER,
DETECTOR,
+ AUDIO_ENCODER,
};
struct SDBackendAssignment {
diff --git a/src/model.h b/src/model.h
index f59f1de7d..ae4319732 100644
--- a/src/model.h
+++ b/src/model.h
@@ -35,6 +35,7 @@ enum SDVersion {
VERSION_WAN2,
VERSION_WAN2_2_I2V,
VERSION_WAN2_2_TI2V,
+ VERSION_WAN2_2_S2V,
VERSION_LINGBOT_VIDEO,
VERSION_QWEN_IMAGE,
VERSION_QWEN_IMAGE_LAYERED,
@@ -129,7 +130,7 @@ static inline bool sd_version_is_minimax_h3(SDVersion version) {
}
static inline bool sd_version_is_wan(SDVersion version) {
- if (version == VERSION_WAN2 || version == VERSION_WAN2_2_I2V || version == VERSION_WAN2_2_TI2V) {
+ if (version == VERSION_WAN2 || version == VERSION_WAN2_2_I2V || version == VERSION_WAN2_2_TI2V || version == VERSION_WAN2_2_S2V) {
return true;
}
return false;
diff --git a/src/model/audio/wav2vec2.hpp b/src/model/audio/wav2vec2.hpp
new file mode 100644
index 000000000..fbe5b8da1
--- /dev/null
+++ b/src/model/audio/wav2vec2.hpp
@@ -0,0 +1,742 @@
+#ifndef __SD_MODEL_AUDIO_WAV2VEC2_HPP__
+#define __SD_MODEL_AUDIO_WAV2VEC2_HPP__
+
+#include "core/ggml_extend.hpp"
+#include "model.h"
+
+/*================================================ Wav2Vec2 audio encoder ================================================*/
+
+// Port of ComfyUI comfy/audio_encoders/wav2vec2.py (HF wav2vec2 inference subset).
+// Checkpoint: wav2vec2-large-english (embed_dim 1024, 24 layers, 16 heads,
+// conv_norm/conv_bias true, stable layer norm). Keys carry a "wav2vec2." prefix;
+// load the storage map with that prefix so names line up with the block tree.
+
+struct Wav2Vec2Config {
+ int64_t embed_dim = 1024;
+ int64_t conv_dim = 512;
+ int num_heads = 16;
+ int num_layers = 24;
+ bool conv_norm = true;
+ bool conv_bias = true;
+ bool do_normalize = true;
+ bool do_stable_layer_norm = true;
+
+ // Mirrors comfy/audio_encoders/audio_encoders.py:49-76: embed_dim inferred from
+ // the shape of encoder.layer_norm.bias.
+ static Wav2Vec2Config detect_from_weights(const String2TensorStorage& tensor_storage_map, const std::string& prefix) {
+ Wav2Vec2Config config;
+ auto it = tensor_storage_map.find(prefix + "encoder.layer_norm.bias");
+ if (it == tensor_storage_map.end()) {
+ LOG_WARN("wav2vec2: %sencoder.layer_norm.bias not found, using large defaults", prefix.c_str());
+ return config;
+ }
+ config.embed_dim = it->second.ne[0];
+ if (config.embed_dim == 1024) { // large
+ config.embed_dim = 1024;
+ config.num_heads = 16;
+ config.num_layers = 24;
+ config.conv_norm = true;
+ config.conv_bias = true;
+ config.do_normalize = true;
+ config.do_stable_layer_norm = true;
+ } else if (config.embed_dim == 768) { // base
+ config.embed_dim = 768;
+ config.num_heads = 12;
+ config.num_layers = 12;
+ config.conv_norm = false;
+ config.conv_bias = false;
+ config.do_normalize = false;
+ config.do_stable_layer_norm = false;
+ } else {
+ LOG_WARN("wav2vec2: unsupported embed_dim %" PRId64 ", using large defaults", config.embed_dim);
+ config.embed_dim = 1024;
+ }
+ return config;
+ }
+};
+
+// Conv1d with optional groups (weight [kernel, in_channels, out_channels]). For
+// groups == channels == out_channels this stays depthwise-capable via ggml; general
+// groups run per-group ggml_conv_1d on channel views and concat.
+struct Wav2Vec2Conv1d : public UnaryBlock {
+ int64_t in_channels;
+ int64_t out_channels;
+ int64_t groups;
+ int kernel_size;
+ int stride;
+ int padding;
+ int dilation;
+ bool bias;
+ std::string prefix;
+
+ Wav2Vec2Conv1d(int64_t in_channels,
+ int64_t out_channels,
+ int kernel_size,
+ int stride = 1,
+ int padding = 0,
+ int dilation = 1,
+ int64_t groups = 1,
+ bool bias = true)
+ : in_channels(in_channels),
+ out_channels(out_channels),
+ groups(groups),
+ kernel_size(kernel_size),
+ stride(stride),
+ padding(padding),
+ dilation(dilation),
+ bias(bias) {}
+
+ void init_params(ggml_context* ctx, const String2TensorStorage& tensor_storage_map = {}, const std::string prefix = "") override {
+ this->prefix = prefix;
+ ggml_type wtype = get_type(prefix + "weight", tensor_storage_map, GGML_TYPE_F16);
+ params["weight"] = ggml_new_tensor_3d(ctx, wtype, kernel_size, in_channels / groups, out_channels);
+ if (bias) {
+ params["bias"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, out_channels);
+ }
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) override {
+ // x: [L, in_channels, N]
+ ggml_tensor* w = params["weight"];
+ ggml_tensor* b = bias ? params["bias"] : nullptr;
+
+ GGML_ASSERT(x->ne[1] == in_channels);
+
+ if (groups == 1) {
+ // F32 im2col + F32 weight keeps the conv core in full precision (see
+ // conv_1d_f32_im2col); mul_mat's mixed-type path requires an F32
+ // right-hand side, so cast F16 checkpoint weights once.
+ ggml_tensor* w32 = w->type == GGML_TYPE_F32 ? w : ggml_cast(ctx->ggml_ctx, w, GGML_TYPE_F32);
+ ggml_tensor* out = conv_1d_f32_im2col(ctx->ggml_ctx, w32, x, stride, padding, dilation);
+ if (b != nullptr) {
+ b = ggml_reshape_3d(ctx->ggml_ctx, b, 1, out_channels, 1);
+ out = ggml_add_inplace(ctx->ggml_ctx, out, b);
+ }
+ return out;
+ }
+ return grouped_conv_1d(ctx, x, w, groups, stride, padding, b);
+ }
+
+ // Grouped conv via per-group channel views; output channels concatenated in order.
+ // ggml_conv_1d hardcodes an F16 im2col destination, which rounds the input
+ // patches even for F32 weights; wav2vec2 carries large activation outliers
+ // that this rounding amplifies through 24 layers, so the grouped path uses
+ // ggml_conv_1d's exact composition with an F32 im2col instead (the pos_conv
+ // weight is always F32 here - it is recomputed in-graph from weight_norm).
+ static ggml_tensor* conv_1d_f32_im2col(ggml_context* ctx, ggml_tensor* w, ggml_tensor* x, int s0, int p0, int d0) {
+ ggml_tensor* im2col = ggml_im2col(ctx, w, x, s0, 0, p0, 0, d0, 0, false, GGML_TYPE_F32);
+ ggml_tensor* result = ggml_mul_mat(ctx,
+ ggml_reshape_2d(ctx, im2col, im2col->ne[0], im2col->ne[2] * im2col->ne[1]),
+ ggml_reshape_2d(ctx, w, w->ne[0] * w->ne[1], w->ne[2]));
+ return ggml_reshape_3d(ctx, result, im2col->ne[1], w->ne[2], im2col->ne[2]);
+ }
+
+ static ggml_tensor* grouped_conv_1d(GGMLRunnerContext* ctx,
+ ggml_tensor* x,
+ ggml_tensor* w,
+ int64_t groups,
+ int stride,
+ int padding,
+ ggml_tensor* b = nullptr) {
+ const int64_t ic_g = x->ne[1] / groups;
+ const int64_t oc_g = w->ne[2] / groups;
+ // ggml's im2col kernels (the backend of ggml_conv_1d) read input planes
+ // with flat indexing and only honor nb[1] for the channel offset. A
+ // non-contiguous x (e.g. a permuted view with time stride 4*C) therefore
+ // reads wrong elements through the per-group views below, because
+ // ggml_view_3d forces nb[0] to the type size and hides the true stride.
+ // Materialize once; contiguous inputs pass through untouched.
+ if (x->nb[0] != ggml_type_size(x->type) || x->nb[1] != (size_t)x->ne[0] * x->nb[0]) {
+ x = ggml_cont(ctx->ggml_ctx, x);
+ }
+ if (getenv("WAV2VEC2_DEBUG_CONV") != nullptr) {
+ printf("[dbg] x: ne=[%lld,%lld,%lld] nb=[%zu,%zu,%zu]\n",
+ (long long)x->ne[0], (long long)x->ne[1], (long long)x->ne[2],
+ x->nb[0], x->nb[1], x->nb[2]);
+ printf("[dbg] w: ne=[%lld,%lld,%lld] nb=[%zu,%zu,%zu] type=%d\n",
+ (long long)w->ne[0], (long long)w->ne[1], (long long)w->ne[2],
+ w->nb[0], w->nb[1], w->nb[2], (int)w->type);
+ }
+ ggml_tensor* acc = nullptr;
+ for (int64_t i = 0; i < groups; ++i) {
+ ggml_tensor* x_i = ggml_view_3d(ctx->ggml_ctx, x,
+ x->ne[0], ic_g, x->ne[2],
+ x->nb[1], x->nb[2],
+ i * ic_g * x->nb[1]);
+ ggml_tensor* w_i = ggml_view_3d(ctx->ggml_ctx, w,
+ w->ne[0], ic_g, oc_g,
+ w->nb[1], w->nb[2],
+ i * oc_g * w->nb[2]);
+ ggml_tensor* out_i = conv_1d_f32_im2col(ctx->ggml_ctx, w_i, x_i, stride, padding, 1);
+ if (b != nullptr) {
+ ggml_tensor* b_i = ggml_view_1d(ctx->ggml_ctx, b, oc_g, i * oc_g * b->nb[0]);
+ b_i = ggml_reshape_3d(ctx->ggml_ctx, b_i, 1, oc_g, 1);
+ out_i = ggml_add_inplace(ctx->ggml_ctx, out_i, b_i);
+ }
+ acc = (acc == nullptr) ? out_i : ggml_concat(ctx->ggml_ctx, acc, out_i, 1);
+ }
+ return acc;
+ }
+};
+
+// Conv + (optional) per-channel norm + GELU. ComfyUI LayerNormConv applies the
+// LayerNorm over channels for every frame; equivalent to normalizing over the
+// channel axis after transposing. GroupNorm variant mirrors LayerGroupNormConv.
+struct Wav2Vec2ConvLayer : public UnaryBlock {
+ Wav2Vec2ConvLayer(int64_t in_channels,
+ int64_t out_channels,
+ int kernel_size,
+ int stride,
+ bool bias,
+ bool use_layer_norm) {
+ blocks["conv"] = std::shared_ptr(new Wav2Vec2Conv1d(in_channels, out_channels, kernel_size, stride, 0, 1, 1, bias));
+ if (use_layer_norm) {
+ blocks["layer_norm"] = std::shared_ptr(new LayerNorm(out_channels));
+ }
+ use_layer_norm_ = use_layer_norm;
+ channels_ = out_channels;
+ }
+
+ void init_params(ggml_context* ctx, const String2TensorStorage& tensor_storage_map = {}, const std::string prefix = "") override {
+ GGMLBlock::init_params(ctx, tensor_storage_map, prefix);
+ if (!use_layer_norm_) {
+ // LayerGroupNormConv: GroupNorm(num_groups=out_channels, affine=true)
+ params["layer_norm.weight"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, channels_);
+ params["layer_norm.bias"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, channels_);
+ }
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) override {
+ // x: [L, C, N]
+ x = std::dynamic_pointer_cast(blocks["conv"])->forward(ctx, x);
+ if (use_layer_norm_) {
+ auto ln = std::dynamic_pointer_cast(blocks["layer_norm"]);
+ // [L, C, N] -> [C, L, N] -> LayerNorm over C -> back
+ x = ggml_permute(ctx->ggml_ctx, x, 1, 0, 2, 3);
+ x = ln->forward(ctx, x);
+ x = ggml_permute(ctx->ggml_ctx, x, 1, 0, 2, 3);
+ } else {
+ x = ggml_cont(ctx->ggml_ctx, x);
+ x = ggml_group_norm(ctx->ggml_ctx, x, (int)channels_, 1e-05f);
+ x = ggml_add(ctx->ggml_ctx, x, ggml_reshape_3d(ctx->ggml_ctx, params["layer_norm.bias"], 1, channels_, 1));
+ x = ggml_mul(ctx->ggml_ctx, x, ggml_reshape_3d(ctx->ggml_ctx, params["layer_norm.weight"], 1, channels_, 1));
+ }
+ return ggml_ext_gelu(ctx->ggml_ctx, x, true);
+ }
+
+private:
+ bool use_layer_norm_;
+ int64_t channels_;
+};
+
+struct Wav2Vec2ConvFeatureEncoder : public GGMLBlock {
+ Wav2Vec2ConvFeatureEncoder(const Wav2Vec2Config& config) {
+ // kernel sizes (10,3,3,3,3,2,2), strides (5,2,2,2,2,2,2); conv0 maps the
+ // single waveform channel to conv_dim and always has a bias.
+ const int kernels[7] = {10, 3, 3, 3, 3, 2, 2};
+ const int strides[7] = {5, 2, 2, 2, 2, 2, 2};
+ int64_t in_channels = 1;
+ for (int i = 0; i < 7; ++i) {
+ bool bias = (i == 0) ? true : config.conv_bias;
+ blocks["conv_layers." + std::to_string(i)] = std::shared_ptr(
+ new Wav2Vec2ConvLayer(in_channels, config.conv_dim, kernels[i], strides[i], bias, config.conv_norm));
+ in_channels = config.conv_dim;
+ }
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ // x: [L, 1, N] waveform; returns [conv_dim, L', N]
+ for (int i = 0; i < 7; ++i) {
+ auto conv = std::dynamic_pointer_cast(blocks["conv_layers." + std::to_string(i)]);
+ x = conv->forward(ctx, x);
+ }
+ return ggml_permute(ctx->ggml_ctx, x, 1, 0, 2, 3); // [conv_dim, L', N]
+ }
+};
+
+struct Wav2Vec2FeatureProjection : public UnaryBlock {
+ Wav2Vec2FeatureProjection(const Wav2Vec2Config& config) {
+ blocks["layer_norm"] = std::shared_ptr(new LayerNorm(config.conv_dim));
+ blocks["projection"] = std::shared_ptr(new Linear(config.conv_dim, config.embed_dim));
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ // x: [conv_dim, L', N] -> [embed_dim, L', N]
+ auto ln = std::dynamic_pointer_cast(blocks["layer_norm"]);
+ auto projection = std::dynamic_pointer_cast(blocks["projection"]);
+ x = ln->forward(ctx, x);
+ x = projection->forward(ctx, x);
+ return x;
+ }
+};
+
+struct Wav2Vec2PositionalConvEmbedding : public UnaryBlock {
+ int64_t embed_dim;
+ int64_t groups;
+ int kernel_size;
+ enum NormConvention {
+ NORM_PER_KERNEL_TAP, // g [1, 1, k]: norm over all channels per kernel tap
+ NORM_PER_OUT_KERNEL, // g [out, 1, k]: norm over in/g per (out, kernel tap)
+ NORM_PER_CHANNEL_PAIR, // g [out, ic_g, 1]: norm over kernel per channel pair
+ };
+ NormConvention norm_convention;
+ bool legacy_key_names; // weight_g/weight_v vs parametrizations.weight.original0/1
+
+ Wav2Vec2PositionalConvEmbedding(const Wav2Vec2Config& config)
+ : embed_dim(config.embed_dim), groups(16), kernel_size(128), norm_convention(NORM_PER_KERNEL_TAP), legacy_key_names(true) {}
+
+ void init_params(ggml_context* ctx, const String2TensorStorage& tensor_storage_map = {}, const std::string prefix = "") override {
+ // prefix arrives with a trailing dot, e.g. "...encoder.pos_conv_embed.";
+ // the weight-norm params live under the inner "conv" module.
+ const std::string base = prefix + "conv.";
+ const int64_t ic_g = embed_dim / groups;
+ ggml_type wtype = GGML_TYPE_F16;
+ auto key_g = base + "weight_g";
+ auto key_v = base + "weight_v";
+ legacy_key_names = tensor_storage_map.find(key_g) != tensor_storage_map.end();
+ if (!legacy_key_names) {
+ key_g = base + "parametrizations.weight.original0";
+ key_v = base + "parametrizations.weight.original1";
+ }
+ // The stored g shape decides the norm convention (files exist with
+ // parametrizations-style [1, 1, k] g under legacy key names).
+ auto it_g = tensor_storage_map.find(key_g);
+ GGML_ASSERT(it_g != tensor_storage_map.end() && it_g->second.ne[1] == 1);
+ if (it_g->second.ne[2] == 1) {
+ norm_convention = NORM_PER_KERNEL_TAP;
+ } else if (it_g->second.ne[1] == 1 && it_g->second.ne[2] == embed_dim) {
+ norm_convention = NORM_PER_OUT_KERNEL;
+ } else if (it_g->second.ne[0] == 1 && it_g->second.ne[1] == ic_g && it_g->second.ne[2] == embed_dim) {
+ norm_convention = NORM_PER_CHANNEL_PAIR;
+ } else {
+ GGML_ABORT("wav2vec2: unsupported weight-norm g shape");
+ }
+ // Param keys must include the inner "conv." segment: get_param_tensors
+ // joins the block prefix with the param key verbatim.
+ const std::string rel_g = key_g.substr(prefix.size()); // "conv.weight_g" / "conv.parametrizations..."
+ const std::string rel_v = key_v.substr(prefix.size());
+ params[rel_g] = ggml_new_tensor_3d(ctx, GGML_TYPE_F32,
+ it_g->second.ne[0], it_g->second.ne[1], it_g->second.ne[2]);
+ params[rel_v] = ggml_new_tensor_3d(ctx, get_type(key_v, tensor_storage_map, GGML_TYPE_F16),
+ kernel_size, ic_g, embed_dim);
+ if (tensor_storage_map.find(base + "bias") != tensor_storage_map.end()) {
+ params["conv.bias"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, embed_dim);
+ }
+ }
+
+ // Per-tap (or per-convention) norm of v, in-graph.
+ ggml_tensor* norm(GGMLRunnerContext* ctx) {
+ const int64_t ic_g = embed_dim / groups;
+ const char* key_v = legacy_key_names ? "conv.weight_v" : "conv.parametrizations.weight.original1";
+ ggml_tensor* v = params[key_v];
+ v = ggml_cast(ctx->ggml_ctx, v, GGML_TYPE_F32);
+ ggml_tensor* sq = ggml_mul(ctx->ggml_ctx, v, v);
+ ggml_tensor* norm;
+ if (norm_convention == NORM_PER_KERNEL_TAP) {
+ // sq [k, ic_g, out]: reduce channels per kernel tap -> [k, 1, 1]
+ // ggml permute axes are destinations of the source dims (inverse of
+ // torch's permute), so (2, 0, 1) yields [ic_g, out, k].
+ ggml_tensor* pt = ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, sq, 2, 0, 1, 3)); // [ic_g, out, k]
+ ggml_tensor* s = ggml_sum_rows(ctx->ggml_ctx, ggml_reshape_2d(ctx->ggml_ctx, pt, ic_g * embed_dim, kernel_size));
+ norm = ggml_sqrt(ctx->ggml_ctx, ggml_reshape_3d(ctx->ggml_ctx, s, kernel_size, 1, 1));
+ } else if (norm_convention == NORM_PER_OUT_KERNEL) {
+ // sq [k, ic_g, out] -> [ic_g, k, out]: reduce ic_g per (k, out) -> [k, 1, out]
+ ggml_tensor* pt = ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, sq, 1, 0, 2, 3));
+ norm = ggml_sqrt(ctx->ggml_ctx,
+ ggml_reshape_3d(ctx->ggml_ctx,
+ ggml_sum_rows(ctx->ggml_ctx, ggml_reshape_2d(ctx->ggml_ctx, pt, ic_g, kernel_size * embed_dim)),
+ kernel_size, 1, embed_dim));
+ } else {
+ // sq [k, ic_g, out]: reduce kernel per channel pair -> [1, ic_g, out]
+ norm = ggml_sqrt(ctx->ggml_ctx, ggml_sum_rows(ctx->ggml_ctx, sq));
+ }
+ return norm;
+ }
+
+ // Recombines the weight-normalized conv weight: weight = v * g / norm(v).
+ ggml_tensor* weight(GGMLRunnerContext* ctx) {
+ const char* key_g = legacy_key_names ? "conv.weight_g" : "conv.parametrizations.weight.original0";
+ const char* key_v = legacy_key_names ? "conv.weight_v" : "conv.parametrizations.weight.original1";
+ ggml_tensor* g = params[key_g];
+ ggml_tensor* v = params[key_v];
+ v = ggml_cast(ctx->ggml_ctx, v, GGML_TYPE_F32);
+ return ggml_mul(ctx->ggml_ctx, v, ggml_div(ctx->ggml_ctx, g, norm(ctx)));
+ }
+
+ // Debug: raw v^2 (pre-reduction) and the transposed copy feeding the reduction.
+ ggml_tensor* sq_dump(GGMLRunnerContext* ctx) {
+ const char* key_v = legacy_key_names ? "conv.weight_v" : "conv.parametrizations.weight.original1";
+ ggml_tensor* v = params[key_v];
+ v = ggml_cast(ctx->ggml_ctx, v, GGML_TYPE_F32);
+ return ggml_mul(ctx->ggml_ctx, v, v);
+ }
+
+ ggml_tensor* pt_dump(GGMLRunnerContext* ctx) {
+ return ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, sq_dump(ctx), 2, 0, 1, 3));
+ }
+
+ // Debug: grouped conv output after drop-last, before GELU (with_bias=false
+ // also skips the bias add).
+ ggml_tensor* conv_raw(GGMLRunnerContext* ctx, ggml_tensor* x, bool with_bias) {
+ ggml_tensor* b = with_bias && params.count("conv.bias") > 0 ? params["conv.bias"] : nullptr;
+ ggml_tensor* t = conv_input(ctx, x);
+ t = Wav2Vec2Conv1d::grouped_conv_1d(ctx, t, weight(ctx), groups, 1, kernel_size / 2, b);
+ t = ggml_view_3d(ctx->ggml_ctx, t, t->ne[0] - 1, t->ne[1], t->ne[2], t->nb[1], t->nb[2], 0);
+ return t; // [L, embed_dim, N]
+ }
+
+ // The materialized [L, embed_dim, N] plane-contiguous tensor that
+ // grouped_conv_1d actually consumes (cont of the permuted projection
+ // output; im2col requires this layout).
+ ggml_tensor* conv_input(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ return ggml_cont(ctx->ggml_ctx, ggml_permute(ctx->ggml_ctx, x, 1, 0, 2, 3));
+ }
+
+ // Debug: group-0-only conv (no bias), post drop-last.
+ ggml_tensor* conv_group0(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ const int64_t ic_g = embed_dim / groups;
+ ggml_tensor* t = conv_input(ctx, x);
+ ggml_tensor* w = weight(ctx);
+ ggml_tensor* x_0 = ggml_view_3d(ctx->ggml_ctx, t, t->ne[0], ic_g, t->ne[2], t->nb[1], t->nb[2], 0);
+ ggml_tensor* w_0 = ggml_view_3d(ctx->ggml_ctx, w, w->ne[0], ic_g, w->ne[2] / groups, w->nb[1], w->nb[2], 0);
+ ggml_tensor* out = Wav2Vec2Conv1d::conv_1d_f32_im2col(ctx->ggml_ctx, w_0, x_0, 1, kernel_size / 2, 1);
+ out = ggml_view_3d(ctx->ggml_ctx, out, out->ne[0] - 1, out->ne[1], out->ne[2], out->nb[1], out->nb[2], 0);
+ return out; // [L, ic_g, N]
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ // x: [embed_dim, L, N]; returns [embed_dim, L, N] (last sample dropped)
+ ggml_tensor* w = weight(ctx);
+ ggml_tensor* b = params.count("conv.bias") > 0 ? params["conv.bias"] : nullptr;
+ ggml_tensor* t = conv_input(ctx, x);
+ t = Wav2Vec2Conv1d::grouped_conv_1d(ctx, t, w, groups, 1, kernel_size / 2, b);
+ // GELU must be out-of-place and precede the drop-last view: an in-place
+ // op on a view writes through a buffer the graph allocator does not
+ // reserve for it, so reuse of the conv output's storage silently
+ // corrupts the result depending on the surrounding graph shape.
+ t = ggml_ext_gelu(ctx->ggml_ctx, t, false);
+ t = ggml_view_3d(ctx->ggml_ctx, t, t->ne[0] - 1, t->ne[1], t->ne[2], t->nb[1], t->nb[2], 0);
+ return ggml_permute(ctx->ggml_ctx, t, 1, 0, 2, 3); // [embed_dim, L, N]
+ }
+};
+
+struct Wav2Vec2FeedForward : public UnaryBlock {
+ Wav2Vec2FeedForward(const Wav2Vec2Config& config) {
+ blocks["intermediate_dense"] = std::shared_ptr(new Linear(config.embed_dim, config.embed_dim * 4));
+ blocks["output_dense"] = std::shared_ptr(new Linear(config.embed_dim * 4, config.embed_dim));
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ auto intermediate_dense = std::dynamic_pointer_cast(blocks["intermediate_dense"]);
+ auto output_dense = std::dynamic_pointer_cast(blocks["output_dense"]);
+ x = intermediate_dense->forward(ctx, x);
+ x = ggml_ext_gelu(ctx->ggml_ctx, x, true);
+ x = output_dense->forward(ctx, x);
+ return x;
+ }
+};
+
+struct Wav2Vec2TransformerEncoderLayer : public UnaryBlock {
+ bool do_stable_layer_norm;
+
+ Wav2Vec2TransformerEncoderLayer(const Wav2Vec2Config& config)
+ : do_stable_layer_norm(config.do_stable_layer_norm) {
+ blocks["attention"] = std::shared_ptr(new MultiheadAttention(config.embed_dim, config.num_heads, true, true));
+ blocks["layer_norm"] = std::shared_ptr(new LayerNorm(config.embed_dim));
+ blocks["feed_forward"] = std::shared_ptr(new Wav2Vec2FeedForward(config));
+ blocks["final_layer_norm"] = std::shared_ptr(new LayerNorm(config.embed_dim));
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ // x: [embed_dim, L, N]
+ auto attention = std::dynamic_pointer_cast(blocks["attention"]);
+ auto layer_norm = std::dynamic_pointer_cast(blocks["layer_norm"]);
+ auto feed_forward = std::dynamic_pointer_cast(blocks["feed_forward"]);
+ auto final_layer_norm = std::dynamic_pointer_cast(blocks["final_layer_norm"]);
+
+ ggml_tensor* residual = x;
+ if (do_stable_layer_norm) {
+ x = layer_norm->forward(ctx, x);
+ x = attention->forward(ctx, x);
+ x = ggml_add(ctx->ggml_ctx, residual, x);
+ x = ggml_add(ctx->ggml_ctx, x, feed_forward->forward(ctx, final_layer_norm->forward(ctx, x)));
+ } else {
+ x = attention->forward(ctx, x);
+ x = ggml_add(ctx->ggml_ctx, residual, x);
+ x = layer_norm->forward(ctx, x);
+ x = final_layer_norm->forward(ctx, ggml_add(ctx->ggml_ctx, x, feed_forward->forward(ctx, x)));
+ }
+ return x;
+ }
+};
+
+struct Wav2Vec2TransformerEncoder : public GGMLBlock {
+ int num_layers;
+ bool do_stable_layer_norm;
+
+ Wav2Vec2TransformerEncoder(const Wav2Vec2Config& config)
+ : num_layers(config.num_layers), do_stable_layer_norm(config.do_stable_layer_norm) {
+ blocks["pos_conv_embed"] = std::shared_ptr(new Wav2Vec2PositionalConvEmbedding(config));
+ for (int i = 0; i < config.num_layers; ++i) {
+ blocks["layers." + std::to_string(i)] = std::shared_ptr(new Wav2Vec2TransformerEncoderLayer(config));
+ }
+ blocks["layer_norm"] = std::shared_ptr(new LayerNorm(config.embed_dim));
+ }
+
+ ggml_tensor* pos_conv(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ auto pc = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ return pc->forward(ctx, x);
+ }
+
+ ggml_tensor* pos_conv_weight(GGMLRunnerContext* ctx) {
+ auto pc = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ return pc->weight(ctx);
+ }
+
+ ggml_tensor* pos_conv_norm(GGMLRunnerContext* ctx) {
+ auto pc = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ return pc->norm(ctx);
+ }
+
+ ggml_tensor* pos_conv_sq(GGMLRunnerContext* ctx) {
+ auto pc = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ return pc->sq_dump(ctx);
+ }
+
+ ggml_tensor* pos_conv_pt(GGMLRunnerContext* ctx) {
+ auto pc = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ return pc->pt_dump(ctx);
+ }
+
+ ggml_tensor* pos_conv_raw(GGMLRunnerContext* ctx, ggml_tensor* x, bool with_bias) {
+ auto pc = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ return pc->conv_raw(ctx, x, with_bias);
+ }
+
+ ggml_tensor* pos_conv_input(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ auto pc = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ return pc->conv_input(ctx, x);
+ }
+
+ ggml_tensor* pos_conv_group0(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ auto pc = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ return pc->conv_group0(ctx, x);
+ }
+
+ // Returns the final hidden state [embed_dim, L, N]. When all_layers != nullptr,
+ // it is filled with the 24 pre-layer states plus the final state, concatenated
+ // along a new trailing axis: [embed_dim, L, num_layers + 1].
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor** all_layers = nullptr) {
+ auto pos_conv_embed = std::dynamic_pointer_cast(blocks["pos_conv_embed"]);
+ auto layer_norm = std::dynamic_pointer_cast(blocks["layer_norm"]);
+
+ std::vector collected;
+ if (all_layers != nullptr) {
+ collected.reserve(num_layers + 1);
+ }
+
+ x = ggml_add(ctx->ggml_ctx, x, pos_conv_embed->forward(ctx, x));
+ if (!do_stable_layer_norm) {
+ x = layer_norm->forward(ctx, x);
+ }
+ for (int i = 0; i < num_layers; ++i) {
+ if (all_layers != nullptr) {
+ collected.push_back(x);
+ }
+ auto layer = std::dynamic_pointer_cast(blocks["layers." + std::to_string(i)]);
+ x = layer->forward(ctx, x);
+ }
+ if (do_stable_layer_norm) {
+ x = layer_norm->forward(ctx, x);
+ }
+ if (all_layers != nullptr) {
+ collected.push_back(x);
+ ggml_tensor* stack = collected[0];
+ for (size_t i = 1; i < collected.size(); ++i) {
+ stack = ggml_concat(ctx->ggml_ctx, stack, collected[i], 2);
+ }
+ *all_layers = stack;
+ }
+ return x;
+ }
+};
+
+struct Wav2Vec2Model : public GGMLBlock {
+ Wav2Vec2Config config;
+
+ Wav2Vec2Model() = default;
+ Wav2Vec2Model(const Wav2Vec2Config& config_) : config(config_) {
+ blocks["feature_extractor"] = std::shared_ptr(new Wav2Vec2ConvFeatureEncoder(config));
+ blocks["feature_projection"] = std::shared_ptr(new Wav2Vec2FeatureProjection(config));
+ blocks["encoder"] = std::shared_ptr(new Wav2Vec2TransformerEncoder(config));
+ }
+
+ // waveform: [L, 1, N] (already channel-mixed). Returns the final hidden state;
+ // all_layers receives the per-layer stack when non-null.
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor** all_layers = nullptr) {
+ auto feature_extractor = std::dynamic_pointer_cast(blocks["feature_extractor"]);
+ auto feature_projection = std::dynamic_pointer_cast(blocks["feature_projection"]);
+ auto encoder = std::dynamic_pointer_cast(blocks["encoder"]);
+
+ x = feature_extractor->forward(ctx, x);
+ x = feature_projection->forward(ctx, x);
+ x = encoder->forward(ctx, x, all_layers);
+ return x;
+ }
+
+ // Debug front-end stages (see Wav2Vec2ModelRunner::build_graph):
+ // 1 = feature extractor, 2 = + projection, 3 = + pos_conv and residual add,
+ // 4 = the recomputed pos_conv weight tensor, 5 = the pos_conv weight norm.
+ ggml_tensor* forward_front(GGMLRunnerContext* ctx, ggml_tensor* x, int stage) {
+ auto feature_extractor = std::dynamic_pointer_cast(blocks["feature_extractor"]);
+ auto feature_projection = std::dynamic_pointer_cast(blocks["feature_projection"]);
+ auto encoder = std::dynamic_pointer_cast(blocks["encoder"]);
+
+ if (stage == 11) {
+ x = feature_extractor->forward(ctx, x);
+ x = feature_projection->forward(ctx, x);
+ return encoder->pos_conv_group0(ctx, x);
+ }
+ if (stage == 10) {
+ x = feature_extractor->forward(ctx, x);
+ x = feature_projection->forward(ctx, x);
+ return encoder->pos_conv_input(ctx, x);
+ }
+ if (stage == 9 || stage == 8) {
+ x = feature_extractor->forward(ctx, x);
+ x = feature_projection->forward(ctx, x);
+ return encoder->pos_conv_raw(ctx, x, stage == 8);
+ }
+ if (stage == 7) {
+ return encoder->pos_conv_pt(ctx);
+ }
+ if (stage == 6) {
+ return encoder->pos_conv_sq(ctx);
+ }
+ if (stage == 5) {
+ return encoder->pos_conv_norm(ctx);
+ }
+ if (stage == 4) {
+ return encoder->pos_conv_weight(ctx);
+ }
+ x = feature_extractor->forward(ctx, x);
+ if (stage == 1) {
+ return x;
+ }
+ x = feature_projection->forward(ctx, x);
+ if (stage == 2) {
+ return x;
+ }
+ return ggml_add(ctx->ggml_ctx, x, encoder->pos_conv(ctx, x));
+ }
+};
+
+class Wav2Vec2ModelRunner : public GGMLRunner {
+public:
+ Wav2Vec2Model model;
+ std::string weight_prefix;
+
+ Wav2Vec2ModelRunner(ggml_backend_t backend,
+ const String2TensorStorage& tensor_storage_map = {},
+ const std::string prefix = "wav2vec2.",
+ std::shared_ptr weight_manager = nullptr)
+ : GGMLRunner(backend, weight_manager), weight_prefix(prefix) {
+ config = Wav2Vec2Config::detect_from_weights(tensor_storage_map, prefix);
+ model = Wav2Vec2Model(config);
+ // GGMLBlock::init/get_param_tensors append their own '.'; the loader-facing
+ // prefix convention carries one.
+ std::string block_prefix = weight_prefix;
+ if (!block_prefix.empty() && block_prefix.back() == '.') {
+ block_prefix.pop_back();
+ }
+ model.init(params_ctx, tensor_storage_map, block_prefix);
+ LOG_INFO("%s", get_desc().c_str());
+ }
+
+ std::string get_desc() override {
+ char buf[256];
+ snprintf(buf, sizeof(buf), "wav2vec2: embed_dim %" PRId64 ", %d layers, %d heads%s",
+ config.embed_dim, config.num_layers, config.num_heads,
+ config.do_stable_layer_norm ? ", stable-ln" : "");
+ return std::string(buf);
+ }
+
+ void get_param_tensors(std::map& tensors) {
+ std::string block_prefix = weight_prefix;
+ if (!block_prefix.empty() && block_prefix.back() == '.') {
+ block_prefix.pop_back();
+ }
+ model.get_param_tensors(tensors, block_prefix);
+ }
+
+ // Allocates all param tensors on the backend's default buffer. Standalone
+ // harnesses need this before loading weights; the full pipeline routes
+ // allocation through ModelManager instead.
+ bool alloc_params_on_backend(ggml_backend_t backend) {
+ params_buffer = ggml_backend_alloc_ctx_tensors_from_buft(params_ctx, ggml_backend_get_default_buffer_type(backend));
+ return params_buffer != nullptr;
+ }
+
+ // waveform: [L, 1, 1] mono; already host-normalized. stage 0 (default) returns
+ // [embed_dim, L', num_layers + 1] with the per-layer stack (last slice = final
+ // hidden state). Stages 1..3 expose front-end intermediates for parity debugging:
+ // 1 = feature extractor [conv_dim, L', 1], 2 = + projection [embed_dim, L', 1],
+ // 3 = + pos_conv and residual add [embed_dim, L', 1].
+ ggml_cgraph* build_graph(const sd::Tensor& waveform_tensor, int stage = 0) {
+ ggml_cgraph* gf = ggml_new_graph(compute_ctx);
+ ggml_tensor* waveform = make_input(waveform_tensor);
+
+ auto runner_ctx = get_context();
+
+ if (stage == 0) {
+ ggml_tensor* all_layers = nullptr;
+ model.forward(&runner_ctx, waveform, &all_layers);
+ GGML_ASSERT(all_layers != nullptr);
+ ggml_build_forward_expand(gf, all_layers);
+ } else {
+ // stages produce views (permutes); materialize them so the flat dump
+ // reflects logical [C, T, N] order
+ ggml_tensor* out = ggml_cont(runner_ctx.ggml_ctx, model.forward_front(&runner_ctx, waveform, stage));
+ ggml_build_forward_expand(gf, out);
+ }
+ return gf;
+ }
+
+ sd::Tensor compute(const int n_threads, const std::vector& mono_waveform, int stage = 0) {
+ GGML_ASSERT(!mono_waveform.empty());
+ const int64_t num_samples = (int64_t)mono_waveform.size();
+ sd::Tensor waveform({num_samples, 1, 1});
+ std::copy(mono_waveform.begin(), mono_waveform.end(), waveform.data());
+ normalize(waveform.data(), num_samples);
+
+ auto get_graph = [&]() -> ggml_cgraph* {
+ return build_graph(waveform, stage);
+ };
+ return take_or_empty(GGMLRunner::compute(get_graph, n_threads, true, true, true));
+ }
+
+private:
+ Wav2Vec2Config config;
+ ggml_backend_buffer_t params_buffer = nullptr;
+
+ // torch: (x - x.mean()) / torch.sqrt(x.var() + 1e-7); var is population variance.
+ static void normalize(float* x, int64_t n) {
+ double mean = 0.0;
+ for (int64_t i = 0; i < n; ++i) {
+ mean += x[i];
+ }
+ mean /= n;
+ double var = 0.0;
+ for (int64_t i = 0; i < n; ++i) {
+ const double d = x[i] - mean;
+ var += d * d;
+ }
+ var /= n;
+ const float scale = (float)(1.0 / std::sqrt(var + 1e-7));
+ for (int64_t i = 0; i < n; ++i) {
+ x[i] = (float)((x[i] - mean) * scale);
+ }
+ }
+};
+
+#endif // __SD_MODEL_AUDIO_WAV2VEC2_HPP__
diff --git a/src/model/common/rope.hpp b/src/model/common/rope.hpp
index c36cc7e9c..15a6963f0 100644
--- a/src/model/common/rope.hpp
+++ b/src/model/common/rope.hpp
@@ -805,7 +805,8 @@ namespace Rope {
return embed_nd(ids, bs, static_cast(theta), axes_dim, wrap_dims, EmbedNDLayout::ErnieImage);
}
- // Generate wan positional embeddings
+ // Generate wan positional embeddings; t_offset shifts the temporal ids
+ // (S2V reference latent uses t_start = max(30, t + 9)).
__STATIC_INLINE__ std::vector gen_wan_pe(int t,
int h,
int w,
@@ -814,8 +815,9 @@ namespace Rope {
int pw,
int bs,
int theta,
- const std::vector& axes_dim) {
- std::vector> ids = gen_vid_ids(t, h, w, pt, ph, pw, bs);
+ const std::vector& axes_dim,
+ int t_offset = 0) {
+ std::vector> ids = gen_vid_ids(t, h, w, pt, ph, pw, bs, t_offset);
return embed_nd(ids, bs, static_cast(theta), axes_dim);
}
diff --git a/src/model/diffusion/model.hpp b/src/model/diffusion/model.hpp
index 070ca53d4..7d37d1b7b 100644
--- a/src/model/diffusion/model.hpp
+++ b/src/model/diffusion/model.hpp
@@ -69,6 +69,8 @@ struct AnimaDiffusionExtra {
struct WanDiffusionExtra {
const sd::Tensor* vace_context = nullptr;
float vace_strength = 1.f;
+ // S2V: stacked wav2vec2 hidden states, [num_layers=25, frames=T_latent*4, dim=1024]
+ const sd::Tensor* audio_embed = nullptr;
};
struct HiDreamO1DiffusionExtra {
diff --git a/src/model/diffusion/wan.hpp b/src/model/diffusion/wan.hpp
index 9a907dcfa..8de841ccc 100644
--- a/src/model/diffusion/wan.hpp
+++ b/src/model/diffusion/wan.hpp
@@ -1,10 +1,12 @@
#ifndef __SD_MODEL_DIFFUSION_WAN_HPP__
#define __SD_MODEL_DIFFUSION_WAN_HPP__
+#include
#include