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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/wan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -94,6 +101,48 @@

<video src=../assets/wan/Wan2.2_14B_i2v.mp4 controls="controls" muted="muted" type="video/mp4"></video>

### 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.
- `--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.
- S2V always uses 16 fps. Other requested frame rates are automatically
changed to 16 with a warning, including the CLI and server video output.
`generate_video()` returns the actual frame rate through `fps_out`; C API
callers should use that value when encoding the output video.
- 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 "...<lora:lightx2v-Wan2.2-T2V-A14B-4steps-lora-rank64-Seko-V2.0-low_noise:1.0>"
--cfg-scale 1.0 --steps 4
```

Expect some quality/dynamics loss compared to the full 20-step run.

### Wan2.2 T2V A14B T2I

```
Expand Down
8 changes: 4 additions & 4 deletions examples/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@ void step_callback(int step, int frame_count, sd_image_t* image, bool is_noisy,
LOG_ERROR("save preview image to '%s' failed", path.string().c_str());
}
} else {
if (create_video_from_sd_images(cli_params->preview_path.c_str(), image, frame_count, cli_params->preview_fps, cli_params->compression_quality) != 0) {
int fps = cli_params->preview_method == PREVIEW_PROJ ? cli_params->preview_fps / 4 : cli_params->preview_fps;
if (create_video_from_sd_images(cli_params->preview_path.c_str(), image, frame_count, fps, cli_params->compression_quality) != 0) {
LOG_ERROR("save preview video to '%s' failed", cli_params->preview_path.c_str());
}
}
Expand Down Expand Up @@ -687,8 +688,6 @@ int main(int argc, const char* argv[]) {
}
}
cli_params.preview_fps = gen_params.fps;
if (cli_params.preview_method == PREVIEW_PROJ)
cli_params.preview_fps /= 4;

sd_set_preview_callback(step_callback,
cli_params.preview_method,
Expand Down Expand Up @@ -951,9 +950,10 @@ int main(int argc, const char* argv[]) {
} else if (cli_params.mode == VID_GEN) {
sd_vid_gen_params_t vid_gen_params = gen_params.to_sd_vid_gen_params_t();
sd_image_t* generated_video = nullptr;
if (!generate_video(sd_ctx.get(), &vid_gen_params, &generated_video, &num_results, &generated_audio)) {
if (!generate_video(sd_ctx.get(), &vid_gen_params, &generated_video, &num_results, &generated_audio, &cli_params.preview_fps)) {
generated_video = nullptr;
}
gen_params.fps = cli_params.preview_fps;
results.adopt(generated_video, num_results);
}

Expand Down
19 changes: 19 additions & 0 deletions examples/common/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,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)",
Expand Down Expand Up @@ -898,6 +903,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"
Expand Down Expand Up @@ -963,6 +969,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();
Expand Down Expand Up @@ -1515,6 +1522,14 @@ ArgOptions SDGenerationParams::get_options() {
return 1;
};

auto on_audio_arg = [&](int argc, const char** argv, int index) {
if (++index >= argc) {
return -1;
}
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;
Expand Down Expand Up @@ -1704,6 +1719,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)",
Expand Down
1 change: 1 addition & 0 deletions examples/common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,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;
Expand Down
5 changes: 2 additions & 3 deletions examples/server/async_jobs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
{
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
sd_image_t* raw_results = nullptr;
if (!generate_video(runtime.sd_ctx, &params, &raw_results, &num_results, &generated_audio)) {
if (!generate_video(runtime.sd_ctx, &params, &raw_results, &num_results, &generated_audio, &output_fps)) {
raw_results = nullptr;
}
results.adopt(raw_results, num_results);
Expand All @@ -261,7 +261,7 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
std::vector<uint8_t> video_bytes = create_video_from_sd_images_to_vector(job.vid_gen.output_format,
results.data(),
num_results,
job.vid_gen.gen_params.fps,
output_fps,
job.vid_gen.output_compression,
generated_audio);
free_sd_audio(generated_audio);
Expand All @@ -273,7 +273,6 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
output_media_b64 = base64_encode(video_bytes);
output_media_mime_type = video_mime_type(job.vid_gen.output_format);
output_frame_count = num_results;
output_fps = job.vid_gen.gen_params.fps;
return true;
}

Expand Down
5 changes: 4 additions & 1 deletion include/stable-diffusion.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,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;
Expand Down Expand Up @@ -521,11 +522,13 @@ enum sd_cancel_mode_t {
SD_API void sd_cancel_generation(sd_ctx_t* sd_ctx, enum sd_cancel_mode_t mode);

SD_API void sd_vid_gen_params_init(sd_vid_gen_params_t* sd_vid_gen_params);
// If non-NULL, fps_out receives the effective encoding frame rate before preview callbacks.
SD_API bool generate_video(sd_ctx_t* sd_ctx,
const sd_vid_gen_params_t* sd_vid_gen_params,
sd_image_t** frames_out,
int* num_frames_out,
sd_audio_t** audio_out);
sd_audio_t** audio_out,
int* fps_out);

typedef struct upscaler_ctx_t upscaler_ctx_t;

Expand Down
103 changes: 103 additions & 0 deletions src/conditioning/wan_audio.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#include "wan_audio.h"

#include <algorithm>
#include <cmath>
#include <cstddef>

namespace sd::wan_audio {

static BucketPlan plan_buckets(int audio_frames, int batch_frames, int video_rate, int fps) {
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<double>(video_rate) / fps;
// Keep a trailing chunk even when audio ends on a chunk boundary.
plan.num_chunks = static_cast<int>(audio_frames / (batch_frames * scale)) + 1;
plan.bucket_frames = plan.num_chunks * batch_frames;
plan.padded_audio_frames = static_cast<int>(
std::ceil(plan.bucket_frames / static_cast<double>(fps) * video_rate));
return plan;
}

// Match NumPy's round-half-even sampling.
static int bucket_source_frame(int bucket_frame, int video_rate, int fps) {
return static_cast<int>(std::nearbyint(static_cast<double>(bucket_frame) * video_rate / fps));
}

static int interpolated_frame_count(int in_frames, int input_fps, int output_fps) {
return static_cast<int>(in_frames / static_cast<double>(input_fps) * output_fps);
}

// Match PyTorch linear interpolation with align_corners=True.
static std::vector<float> linear_interpolate_frames(const std::vector<float>& in,
int num_layers,
int in_frames,
int dim,
int out_frames) {
std::vector<float> out(static_cast<size_t>(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<double>(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<int>(pos);
const int src1 = std::min(src0 + 1, in_frames - 1);
const float frac = static_cast<float>(pos - src0);
const float* in_row = &in[(static_cast<size_t>(layer) * in_frames + src0) * dim];
const float* in_next = &in[(static_cast<size_t>(layer) * in_frames + src1) * dim];
float* out_row = &out[(static_cast<size_t>(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;
}

std::vector<float> build_audio_buckets(const float* stacked_states,
int num_layers,
int in_frames,
int dim,
int batch_frames,
BucketPlan* plan_out,
int input_fps,
int video_rate,
int fps) {
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<float> interpolated =
linear_interpolate_frames(std::vector<float>(stacked_states,
stacked_states + static_cast<size_t>(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<float> buckets(static_cast<size_t>(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;
}
for (int layer = 0; layer < num_layers; ++layer) {
std::copy_n(interpolated.data() + (static_cast<size_t>(layer) * audio_frames + src) * dim,
static_cast<size_t>(dim),
buckets.data() + (static_cast<size_t>(frame) * num_layers + layer) * dim);
}
}
return buckets;
}

} // namespace sd::wan_audio
32 changes: 32 additions & 0 deletions src/conditioning/wan_audio.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#ifndef __SD_CONDITIONING_WAN_AUDIO_H__
#define __SD_CONDITIONING_WAN_AUDIO_H__

#include <vector>

namespace sd::wan_audio {

struct BucketPlan {
int audio_frames; // frames at video_rate
int batch_frames; // latent_t * 4
int video_rate;
int fps; // bucket frame rate
int num_chunks; // includes trailing padding
int bucket_frames;
int padded_audio_frames;
};

// [layers, frames, dim] at input_fps -> [bucket_frames, layers, dim] at fps.
// Pads past the audio end; returns an empty vector on invalid input.
std::vector<float> 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);

} // namespace sd::wan_audio

#endif // __SD_CONDITIONING_WAN_AUDIO_H__
Loading
Loading