multimodalart HF Staff commited on
Commit
9003c90
·
verified ·
1 Parent(s): eefecf8

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/elder.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/speech_med.wav filter=lfs diff=lfs merge=lfs -text
38
+ examples/speech_short.wav filter=lfs diff=lfs merge=lfs -text
39
+ examples/woman.jpg filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,41 @@
1
  ---
2
- title: Bernini R S2v Talking Head
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Bernini-R-S2V Talking Head
3
+ emoji: 🎤
4
+ colorFrom: red
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.20.0
 
8
  app_file: app.py
9
+ short_description: Portrait + speech audio into a lip-synced talking video
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 1h
12
+ models:
13
+ - rzgar/Bernini-R-S2V
14
+ - tolgacangoz/Wan2.2-S2V-14B-Diffusers
15
  ---
16
 
17
+ # 🎤 Bernini-R-S2V Talking Head
18
+
19
+ Animate a **portrait image + speech audio** into a **lip-synced talking video**.
20
+
21
+ [Bernini-R-S2V](https://huggingface.co/rzgar/Bernini-R-S2V) is a speech-driven
22
+ video extension of Bernini-R, built on **Wan 2.2 S2V**. It adds single-speaker
23
+ audio conditioning so a still portrait can be driven to speak or sing in sync
24
+ with an audio track.
25
+
26
+ ## How it works
27
+
28
+ - **Input:** a clear front-facing portrait + a mono speech clip (best results at
29
+ ~3–10 seconds of clear speech).
30
+ - **Output:** an MP4 of the portrait animated to speak, with the audio muxed in.
31
+ - Runs on **ZeroGPU** with the community `WanSpeechToVideoPipeline`
32
+ (diffusers PR #12258).
33
+
34
+ ## Note on weights
35
+
36
+ The Bernini-R-S2V release ships ComfyUI single-file checkpoints (dual high/low
37
+ noise Wan 2.2 MoE) plus a wav2vec2 audio encoder, with no diffusers-format
38
+ export. The community diffusers Wan 2.2 S2V pipeline uses a single-transformer
39
+ S2V architecture, so to deliver the same self-hosted image + audio → talking
40
+ video experience this Space runs the **base Wan 2.2 S2V-14B** weights
41
+ (`tolgacangoz/Wan2.2-S2V-14B-Diffusers`) that Bernini-R-S2V is finetuned from.
app.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Bernini-R-S2V Talking Head — image + speech audio -> lip-synced talking video.
4
+
5
+ Bernini-R-S2V (rzgar/Bernini-R-S2V) is a speech-driven video extension of
6
+ Bernini-R, built on Wan 2.2 S2V. It adds single-speaker audio conditioning so a
7
+ still portrait can be animated to speak/sing in sync with an audio track.
8
+
9
+ The Bernini finetune ships as ComfyUI single-file checkpoints (dual high/low
10
+ noise Wan 2.2 MoE) plus a wav2vec2 audio encoder. There is no diffusers-format
11
+ export of those exact weights, and the community diffusers Wan 2.2 S2V pipeline
12
+ uses a single-transformer S2V architecture. To give the same
13
+ image + audio -> talking-video experience self-hosted on ZeroGPU, this Space
14
+ runs the base Wan 2.2 S2V pipeline that Bernini-R-S2V is finetuned from,
15
+ via the community WanSpeechToVideoPipeline (diffusers PR #12258).
16
+ """
17
+ import os
18
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
19
+
20
+ import math
21
+ import tempfile
22
+
23
+ import spaces
24
+ import torch
25
+ import gradio as gr
26
+ from PIL import Image
27
+
28
+ from diffusers import AutoencoderKLWan, WanSpeechToVideoPipeline
29
+ from diffusers.utils import export_to_video, load_audio
30
+ try:
31
+ from diffusers.utils import export_to_merged_video_audio
32
+ HAS_MERGE = True
33
+ except Exception:
34
+ HAS_MERGE = False
35
+ from transformers import Wav2Vec2ForCTC
36
+
37
+ MODEL_ID = "tolgacangoz/Wan2.2-S2V-14B-Diffusers"
38
+
39
+ # Wan default negative prompt (Chinese) — steers away from low quality / artifacts.
40
+ NEG = ("色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,"
41
+ "低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,"
42
+ "毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走")
43
+
44
+ DEFAULT_PROMPT = "a person talking to the camera, natural expression, slight head movement"
45
+
46
+ # Load the pipeline eagerly at module scope so ZeroGPU packs the weights and
47
+ # streams them into VRAM on the first @spaces.GPU call (rather than paying the
48
+ # load cost inside every request).
49
+ print("Loading Wan 2.2 S2V pipeline (base weights for Bernini-R-S2V)...")
50
+ audio_encoder = Wav2Vec2ForCTC.from_pretrained(MODEL_ID, subfolder="audio_encoder", dtype=torch.float32)
51
+ vae = AutoencoderKLWan.from_pretrained(MODEL_ID, subfolder="vae", torch_dtype=torch.float32)
52
+ pipe = WanSpeechToVideoPipeline.from_pretrained(
53
+ MODEL_ID, vae=vae, audio_encoder=audio_encoder, torch_dtype=torch.bfloat16
54
+ )
55
+ pipe.to("cuda")
56
+ print("Pipeline ready.")
57
+
58
+
59
+ def _snap_size(img, target_area, divisor=64, max_side=1280):
60
+ w, h = img.size
61
+ scale = math.sqrt(target_area / (w * h))
62
+ nw = max(divisor, min(max_side, int(round(w * scale / divisor) * divisor)))
63
+ nh = max(divisor, min(max_side, int(round(h * scale / divisor) * divisor)))
64
+ return nh, nw
65
+
66
+
67
+ def _audio_seconds(audio_path):
68
+ try:
69
+ import soundfile as sf
70
+ info = sf.info(audio_path)
71
+ return info.frames / info.samplerate
72
+ except Exception:
73
+ return 6.0
74
+
75
+
76
+ def _estimate_duration(image_path, audio_path, prompt, negative_prompt, steps,
77
+ guidance, res_area, max_seconds, seed, *args, **kwargs):
78
+ # Video length grows with audio length (chunked at ~5s / chunk). Estimate
79
+ # GPU seconds from the (capped) audio duration and step count.
80
+ try:
81
+ secs = min(float(max_seconds), _audio_seconds(audio_path)) if audio_path else 5.0
82
+ except Exception:
83
+ secs = 5.0
84
+ n_chunks = max(1, math.ceil(secs / 5.0))
85
+ per_chunk = 8.0 * (int(steps) / 20.0) * (1.0 if res_area == "480p" else 2.0)
86
+ return int(min(700, 45 + n_chunks * (12 + per_chunk * 6)))
87
+
88
+
89
+ @spaces.GPU(duration=_estimate_duration)
90
+ def generate(image_path, audio_path, prompt=DEFAULT_PROMPT, negative_prompt=NEG,
91
+ steps=20, guidance=4.5, res_area="480p", max_seconds=6, seed=42,
92
+ progress=gr.Progress(track_tqdm=True)):
93
+ """Generate a lip-synced talking-head video from a portrait image and speech audio.
94
+
95
+ Args:
96
+ image_path: Path to the reference portrait image (a clear front-facing face works best).
97
+ audio_path: Path to the speech audio file (mono, clear speech gives the best lip-sync).
98
+ prompt: Text prompt describing the scene / motion.
99
+ negative_prompt: Things to avoid in the generation.
100
+ steps: Number of denoising steps (higher = slower, more detailed).
101
+ guidance: Classifier-free guidance scale.
102
+ res_area: Target resolution, "480p" or "720p".
103
+ max_seconds: Cap on how many seconds of audio to animate (controls video length / runtime).
104
+ seed: Random seed for reproducibility.
105
+
106
+ Returns:
107
+ Path to the generated MP4 video with the audio track muxed in.
108
+ """
109
+ if not image_path:
110
+ raise gr.Error("Please provide a portrait image first.")
111
+ if not audio_path:
112
+ raise gr.Error("Please provide a speech audio file first.")
113
+
114
+ image = Image.open(image_path).convert("RGB")
115
+ audio, sampling_rate = load_audio(audio_path)
116
+
117
+ # Trim overly long audio to keep generation within the GPU time budget.
118
+ max_seconds = float(max_seconds)
119
+ max_samples = int(max_seconds * sampling_rate)
120
+ if audio.shape[0] > max_samples:
121
+ audio = audio[:max_samples]
122
+
123
+ target = 480 * 832 if res_area == "480p" else 720 * 1280
124
+ h, w = _snap_size(image, target_area=target)
125
+ gen = torch.Generator(device="cuda").manual_seed(int(seed))
126
+
127
+ frames = pipe(
128
+ image=image,
129
+ audio=audio,
130
+ sampling_rate=sampling_rate,
131
+ prompt=(prompt or DEFAULT_PROMPT),
132
+ negative_prompt=(negative_prompt or NEG),
133
+ height=h,
134
+ width=w,
135
+ num_frames_per_chunk=80,
136
+ num_inference_steps=int(steps),
137
+ guidance_scale=float(guidance),
138
+ generator=gen,
139
+ ).frames[0]
140
+
141
+ out = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
142
+ export_to_video(frames, out, fps=16)
143
+ if HAS_MERGE:
144
+ try:
145
+ export_to_merged_video_audio(out, audio_path)
146
+ except Exception as e:
147
+ print("merge audio failed:", e)
148
+ return out
149
+
150
+
151
+ CSS = """
152
+ #col-container { max-width: 1150px; margin: 0 auto; }
153
+ .dark .gradio-container { color: var(--body-text-color); }
154
+ """
155
+
156
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
157
+ with gr.Column(elem_id="col-container"):
158
+ gr.Markdown(
159
+ """
160
+ # 🎤 Bernini-R-S2V — Talking Head
161
+ Animate a **portrait + speech audio** into a **lip-synced talking video**.
162
+
163
+ Powered by [Bernini-R-S2V](https://huggingface.co/rzgar/Bernini-R-S2V) — a speech-driven
164
+ video extension of Bernini-R built on **Wan 2.2 S2V**. Give it a clear front-facing face
165
+ and a mono speech clip; keep audio to ~3–10 s for the best sync and fastest results.
166
+ """
167
+ )
168
+ with gr.Row():
169
+ with gr.Column():
170
+ image = gr.Image(type="filepath", label="Portrait image (clear front-facing face)")
171
+ audio = gr.Audio(type="filepath", label="Speech audio (mono, clear speech)")
172
+ prompt = gr.Textbox(label="Prompt", value=DEFAULT_PROMPT)
173
+ with gr.Accordion("Advanced settings", open=False):
174
+ negative_prompt = gr.Textbox(label="Negative prompt", value=NEG, lines=2)
175
+ res_area = gr.Radio(["480p", "720p"], value="480p",
176
+ label="Resolution (720p is slower)")
177
+ steps = gr.Slider(8, 40, value=20, step=1, label="Steps")
178
+ guidance = gr.Slider(1.0, 8.0, value=4.5, step=0.5, label="Guidance scale")
179
+ max_seconds = gr.Slider(2, 12, value=6, step=1,
180
+ label="Max audio seconds to animate")
181
+ seed = gr.Number(value=42, label="Seed", precision=0)
182
+ btn = gr.Button("Generate talking video", variant="primary")
183
+ with gr.Column():
184
+ out_video = gr.Video(label="Result")
185
+
186
+ gr.Examples(
187
+ examples=[
188
+ ["examples/man.jpg", "examples/speech_short.wav"],
189
+ ["examples/woman.jpg", "examples/speech_med.wav"],
190
+ ["examples/elder.jpg", "examples/speech_short.wav"],
191
+ ],
192
+ inputs=[image, audio],
193
+ outputs=out_video,
194
+ fn=generate,
195
+ cache_examples=True,
196
+ cache_mode="lazy",
197
+ )
198
+
199
+ btn.click(
200
+ generate,
201
+ inputs=[image, audio, prompt, negative_prompt, steps, guidance, res_area, max_seconds, seed],
202
+ outputs=out_video,
203
+ api_name="generate",
204
+ )
205
+
206
+ if __name__ == "__main__":
207
+ demo.queue().launch(mcp_server=True)
examples/elder.jpg ADDED

Git LFS Details

  • SHA256: 7bee38e7ec3225d902a29a025cfab2702f0cf9487456b798ec839853df5c8c92
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
examples/man.jpg ADDED
examples/speech_med.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5ba8d022a19343d0c970422abb3b196a28cc3a7c5b3f1aec145e84cf55f9092b
3
+ size 433362
examples/speech_short.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6659d24ff35077c85bb26a68c446454b9798642daecbec1ea86c1ec02b94e0a6
3
+ size 203370
examples/woman.jpg ADDED

Git LFS Details

  • SHA256: bd50dea7cae6d4e8dae99a3478aa28b237c7b46c41a081899f04fba9673d445b
  • Pointer size: 131 Bytes
  • Size of remote file: 128 kB
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/tolgacangoz/diffusers.git@integrations/wan2.2-s2v
2
+ transformers>=4.49.0
3
+ accelerate
4
+ safetensors
5
+ librosa
6
+ soundfile
7
+ ftfy
8
+ sentencepiece
9
+ imageio
10
+ imageio-ffmpeg
11
+ numpy
12
+ Pillow