yasserrmd commited on
Commit
0526b4c
·
verified ·
1 Parent(s): 51b6cc2

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +83 -7
  2. app.py +369 -0
  3. packages.txt +3 -0
  4. requirements.txt +42 -0
README.md CHANGED
@@ -1,13 +1,89 @@
1
  ---
2
- title: ControlFoley
3
- emoji: 🏆
4
- colorFrom: blue
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.25.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: ControlFoley Video to Audio
3
+ emoji: 🎬
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.22.0
 
8
  app_file: app.py
9
  pinned: false
10
+ python_version: 3.10.13
11
+ license: apache-2.0
12
  ---
13
 
14
+ # ControlFoley Video Foley Audio — ZeroGPU Space
15
+
16
+ A Gradio/Hugging Face ZeroGPU wrapper around the official
17
+ [Xiaomi Research ControlFoley](https://github.com/xiaomi-research/controlfoley)
18
+ inference code.
19
+
20
+ ## What this Space does
21
+
22
+ - Upload a video.
23
+ - Generate synchronized Foley/audio from the video.
24
+ - Optionally guide generation with a text prompt.
25
+ - Preview and download the generated FLAC.
26
+ - Preview and download the video with the generated soundtrack.
27
+ - Includes the official `assets/001.mp4` skateboard example.
28
+
29
+ ## Hardware
30
+
31
+ After creating the Space, open **Settings → Hardware** and select **ZeroGPU**.
32
+
33
+ The app uses:
34
+
35
+ ```python
36
+ @spaces.GPU(size="xlarge", duration=...)
37
+ ```
38
+
39
+ `xlarge` is intentionally selected because ControlFoley loads a large main
40
+ checkpoint plus multiple visual/audio feature models. If your testing confirms
41
+ that the complete model stack fits comfortably in 48 GB VRAM, change
42
+ `size="xlarge"` to `size="large"` to reduce ZeroGPU quota consumption.
43
+
44
+ ## Model downloads
45
+
46
+ On first startup the app downloads:
47
+
48
+ - Official ControlFoley source from GitHub.
49
+ - ControlFoley `weights/*`.
50
+ - ControlFoley `ext_weights/*`.
51
+ - Additional upstream pretrained components requested by ControlFoley
52
+ (for example CLIP/MusicGen) through their normal loaders.
53
+
54
+ The official ControlFoley model repository is large, so the first cold start
55
+ will take significantly longer than later cached starts.
56
+
57
+ ## Test
58
+
59
+ Use the bundled sample and this prompt:
60
+
61
+ ```text
62
+ the skateboard wheels scraping and grinding on the ground.
63
+ ```
64
+
65
+ Recommended first test:
66
+
67
+ - Duration: `8 s`
68
+ - CFG: `4.5`
69
+ - Steps: `25`
70
+ - Seed: `42`
71
+
72
+ For a quicker smoke test, use `10–15` steps.
73
+
74
+ ## Licensing
75
+
76
+ The upstream **source code** repository is Apache-2.0.
77
+
78
+ The **ControlFoley model weights** on Hugging Face are published under
79
+ **CC BY-NC 4.0**. That means the model weights are intended for non-commercial
80
+ use unless you obtain different permission from the model owner.
81
+
82
+ Review the upstream licenses before publishing or using this Space in a
83
+ commercial context.
84
+
85
+ ## Upstream
86
+
87
+ - GitHub: `xiaomi-research/controlfoley`
88
+ - Model: `YJX-Xiaomi/ControlFoley`
89
+ - Paper: *ControlFoley: Unified and Controllable Video-to-Audio Generation with Cross-Modal Conflict Handling*
app.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ControlFoley Gradio app for Hugging Face ZeroGPU
2
+ # Based on: https://github.com/xiaomi-research/controlfoley
3
+ #
4
+ # IMPORTANT: `spaces` must be imported before torch on ZeroGPU.
5
+ import spaces
6
+
7
+ import os
8
+ import sys
9
+ import time
10
+ import shutil
11
+ import logging
12
+ import subprocess
13
+ import tempfile
14
+ from pathlib import Path
15
+
16
+ import gradio as gr
17
+ from huggingface_hub import snapshot_download
18
+
19
+ APP_DIR = Path(__file__).resolve().parent
20
+ SOURCE_DIR = APP_DIR / "upstream_controlfoley"
21
+ MODEL_DIR = APP_DIR / "model_weights"
22
+ OUTPUT_DIR = APP_DIR / "outputs"
23
+ ASSET_DIR = APP_DIR / "sample_assets"
24
+
25
+ UPSTREAM_REPO = "https://github.com/xiaomi-research/controlfoley.git"
26
+ MODEL_REPO = "YJX-Xiaomi/ControlFoley"
27
+
28
+ OUTPUT_DIR.mkdir(exist_ok=True)
29
+ ASSET_DIR.mkdir(exist_ok=True)
30
+
31
+
32
+ def clone_upstream():
33
+ """Fetch the official ControlFoley inference source on first startup."""
34
+ if (SOURCE_DIR / "controlfoley").exists():
35
+ return
36
+
37
+ print("Downloading official ControlFoley source...")
38
+ try:
39
+ subprocess.run(
40
+ ["git", "clone", "--depth", "1", UPSTREAM_REPO, str(SOURCE_DIR)],
41
+ check=True,
42
+ )
43
+ except Exception as exc:
44
+ raise RuntimeError(
45
+ "Could not clone the official ControlFoley repository. "
46
+ "Check that git/network access is available in the Space."
47
+ ) from exc
48
+
49
+
50
+ def download_model_files():
51
+ """Download only the checkpoint folders needed by official inference."""
52
+ print("Downloading ControlFoley model files...")
53
+ snapshot_download(
54
+ repo_id=MODEL_REPO,
55
+ local_dir=str(MODEL_DIR),
56
+ allow_patterns=["weights/*", "ext_weights/*"],
57
+ )
58
+
59
+
60
+ def download_sample_video():
61
+ """Use the official skateboard example from the ControlFoley repo."""
62
+ sample_path = ASSET_DIR / "001.mp4"
63
+ if sample_path.exists():
64
+ return sample_path
65
+
66
+ src = SOURCE_DIR / "assets" / "001.mp4"
67
+ if not src.exists():
68
+ raise RuntimeError("Official sample video assets/001.mp4 was not found.")
69
+ shutil.copy2(src, sample_path)
70
+ return sample_path
71
+
72
+
73
+ # Bootstrap source and model files before importing ControlFoley.
74
+ clone_upstream()
75
+ download_model_files()
76
+
77
+ # The upstream code imports `lib` as a top-level module.
78
+ sys.path.insert(0, str(SOURCE_DIR))
79
+ sys.path.insert(0, str(SOURCE_DIR / "lib"))
80
+
81
+ import torch
82
+ import torchaudio
83
+
84
+ from controlfoley.inference_utils import (
85
+ all_model_cfg,
86
+ generate,
87
+ load_video,
88
+ make_video,
89
+ setup_eval_logging,
90
+ )
91
+ from controlfoley.audio_model import create_audio_generation_model
92
+ from controlfoley.feature_extractor import FeaturesUtils
93
+ from lib.flow_matching import FlowMatching
94
+
95
+ setup_eval_logging()
96
+ log = logging.getLogger("controlfoley-space")
97
+
98
+ torch.backends.cuda.matmul.allow_tf32 = True
99
+ torch.backends.cudnn.allow_tf32 = True
100
+
101
+ MODEL_CFG = all_model_cfg["large_44k"]
102
+ SEQ_CFG = MODEL_CFG.seq_cfg
103
+
104
+ # ZeroGPU recommends placing models on CUDA at module scope.
105
+ # Outside @spaces.GPU this uses Hugging Face CUDA emulation.
106
+ print("Loading ControlFoley main network...")
107
+ NET = create_audio_generation_model(MODEL_CFG.model_name).to(
108
+ "cuda", torch.float32
109
+ ).eval()
110
+ NET.load_weights(
111
+ torch.load(
112
+ MODEL_DIR / "weights" / "controlfoley.pth",
113
+ map_location="cuda",
114
+ weights_only=True,
115
+ )
116
+ )
117
+
118
+ print("Loading ControlFoley feature extractors...")
119
+ FEATURES = FeaturesUtils(
120
+ tod_vae_ckpt=str(MODEL_DIR / "ext_weights" / "v1-44.pth"),
121
+ synchformer_ckpt=str(MODEL_DIR / "ext_weights" / "synchformer_state_dict.pth"),
122
+ cav_mae_ckpt=str(MODEL_DIR / "ext_weights" / "cav_mae_st.pth"),
123
+ clap_ckpt=str(
124
+ MODEL_DIR / "ext_weights" / "music_speech_audioset_epoch_15_esc_89.98.pt"
125
+ ),
126
+ mode=MODEL_CFG.mode,
127
+ enable_conditions=True,
128
+ need_vae_encoder=False,
129
+ ).to("cuda", torch.float32).eval()
130
+
131
+ SAMPLE_VIDEO = download_sample_video()
132
+
133
+
134
+ def gpu_budget(video_path, prompt, negative_prompt, duration, cfg_strength, steps, seed):
135
+ # A conservative ZeroGPU reservation. Shorter requests get a smaller reservation.
136
+ return min(240, max(90, int(45 + float(duration) * 10 + int(steps) * 2)))
137
+
138
+
139
+ @spaces.GPU(size="xlarge", duration=gpu_budget)
140
+ @torch.inference_mode()
141
+ def generate_foley(
142
+ video_path,
143
+ prompt,
144
+ negative_prompt,
145
+ duration,
146
+ cfg_strength,
147
+ steps,
148
+ seed,
149
+ ):
150
+ """Generate synchronized Foley audio for a video with ControlFoley."""
151
+ if not video_path:
152
+ raise gr.Error("Please upload a video or select the sample.")
153
+
154
+ video_path = Path(video_path)
155
+ duration = float(duration)
156
+ cfg_strength = float(cfg_strength)
157
+ steps = int(steps)
158
+ seed = int(seed)
159
+
160
+ if not 1.0 <= duration <= 8.0:
161
+ raise gr.Error("Duration must be between 1 and 8 seconds.")
162
+ if not 5 <= steps <= 30:
163
+ raise gr.Error("Inference steps must be between 5 and 30.")
164
+
165
+ job_dir = Path(tempfile.mkdtemp(prefix="controlfoley_", dir=OUTPUT_DIR))
166
+ audio_path = job_dir / "generated_foley.flac"
167
+ video_out_path = job_dir / "video_with_generated_audio.mp4"
168
+
169
+ # Load video according to the official preprocessing pipeline.
170
+ video_info = load_video(video_path, duration)
171
+ actual_duration = min(duration, float(video_info.total_duration))
172
+
173
+ clip_frames = video_info.clip_embeddings.unsqueeze(0)
174
+ visual_frames = video_info.visual_features.unsqueeze(0)
175
+ sync_frames = video_info.sync_embeddings.unsqueeze(0)
176
+
177
+ # Update temporal sequence lengths for the selected generation duration.
178
+ SEQ_CFG.total_time_seconds = actual_duration
179
+ NET.update_seq_lengths(
180
+ SEQ_CFG.latent_sequence_length,
181
+ SEQ_CFG.clip_sequence_length,
182
+ SEQ_CFG.visual_sequence_length,
183
+ SEQ_CFG.sync_sequence_length,
184
+ )
185
+
186
+ rng = torch.Generator(device="cuda")
187
+ rng.manual_seed(seed)
188
+ fm = FlowMatching(
189
+ min_sigma=0,
190
+ inference_mode="euler",
191
+ num_steps=steps,
192
+ )
193
+
194
+ start = time.time()
195
+
196
+ audios = generate(
197
+ clip_frames,
198
+ visual_frames,
199
+ sync_frames,
200
+ None, # no reference audio
201
+ None, # no timbre reference
202
+ 0.0,
203
+ [prompt or ""],
204
+ negative_text=[negative_prompt or ""],
205
+ feature_utils=FEATURES,
206
+ net=NET,
207
+ fm=fm,
208
+ rng=rng,
209
+ cfg_strength=cfg_strength,
210
+ )
211
+
212
+ audio = audios.float().cpu()[0]
213
+ torchaudio.save(
214
+ str(audio_path),
215
+ audio,
216
+ SEQ_CFG.audio_sample_rate,
217
+ )
218
+
219
+ # Mux generated audio back onto the input video using the upstream helper.
220
+ make_video(
221
+ video_info,
222
+ video_out_path,
223
+ audio,
224
+ sampling_rate=SEQ_CFG.audio_sample_rate,
225
+ )
226
+
227
+ elapsed = time.time() - start
228
+ status = (
229
+ f"Generated {actual_duration:.2f}s of synchronized audio in "
230
+ f"{elapsed:.1f}s. Seed: {seed}. Steps: {steps}."
231
+ )
232
+
233
+ # Help release transient allocations before ZeroGPU returns the GPU.
234
+ del audios, audio
235
+ torch.cuda.empty_cache()
236
+
237
+ return str(audio_path), str(video_out_path), status
238
+
239
+
240
+ TITLE = """
241
+ # 🎬 ControlFoley — Video → Foley Audio
242
+ Generate synchronized sound effects from a silent video using Xiaomi Research's
243
+ **ControlFoley**.
244
+
245
+ Upload a video, optionally guide the sound with text, and download both the
246
+ generated FLAC and a video with the generated soundtrack.
247
+ """
248
+
249
+ INFO = """
250
+ **Modes**
251
+ - Leave the prompt empty for pure **Video-to-Audio (V2A)**.
252
+ - Add a prompt for **Text + Video-to-Audio (TV2A)**.
253
+ - Maximum demo duration is capped at **8 seconds** to keep ZeroGPU usage practical.
254
+
255
+ **Sample**
256
+ The bundled example is the official ControlFoley skateboard sample. Try:
257
+ `the skateboard wheels scraping and grinding on the ground.`
258
+ """
259
+
260
+ with gr.Blocks(title="ControlFoley Video to Audio") as demo:
261
+ gr.Markdown(TITLE)
262
+
263
+ with gr.Row():
264
+ with gr.Column():
265
+ video = gr.Video(
266
+ label="Input video",
267
+ sources=["upload"],
268
+ format="mp4",
269
+ )
270
+ prompt = gr.Textbox(
271
+ label="Sound prompt (optional)",
272
+ placeholder="e.g. skateboard wheels scraping on concrete",
273
+ value="the skateboard wheels scraping and grinding on the ground.",
274
+ )
275
+ negative_prompt = gr.Textbox(
276
+ label="Negative prompt (optional)",
277
+ placeholder="e.g. music, speech, crowd noise",
278
+ value="",
279
+ )
280
+
281
+ with gr.Accordion("Generation settings", open=False):
282
+ duration = gr.Slider(
283
+ minimum=1,
284
+ maximum=8,
285
+ value=8,
286
+ step=0.5,
287
+ label="Duration (seconds)",
288
+ )
289
+ cfg_strength = gr.Slider(
290
+ minimum=1.0,
291
+ maximum=8.0,
292
+ value=4.5,
293
+ step=0.5,
294
+ label="CFG strength",
295
+ )
296
+ steps = gr.Slider(
297
+ minimum=5,
298
+ maximum=30,
299
+ value=25,
300
+ step=1,
301
+ label="Inference steps",
302
+ )
303
+ seed = gr.Number(
304
+ value=42,
305
+ precision=0,
306
+ label="Seed",
307
+ )
308
+
309
+ generate_btn = gr.Button(
310
+ "Generate Foley Sound",
311
+ variant="primary",
312
+ )
313
+
314
+ with gr.Column():
315
+ audio_out = gr.Audio(
316
+ label="Generated Foley audio",
317
+ type="filepath",
318
+ )
319
+ video_out = gr.Video(
320
+ label="Video with generated audio",
321
+ )
322
+ status = gr.Markdown()
323
+
324
+ gr.Markdown(INFO)
325
+
326
+ gr.Examples(
327
+ examples=[
328
+ [
329
+ str(SAMPLE_VIDEO),
330
+ "the skateboard wheels scraping and grinding on the ground.",
331
+ "",
332
+ 8,
333
+ 4.5,
334
+ 25,
335
+ 42,
336
+ ],
337
+ ],
338
+ inputs=[
339
+ video,
340
+ prompt,
341
+ negative_prompt,
342
+ duration,
343
+ cfg_strength,
344
+ steps,
345
+ seed,
346
+ ],
347
+ label="Official ControlFoley sample",
348
+ )
349
+
350
+ generate_btn.click(
351
+ fn=generate_foley,
352
+ inputs=[
353
+ video,
354
+ prompt,
355
+ negative_prompt,
356
+ duration,
357
+ cfg_strength,
358
+ steps,
359
+ seed,
360
+ ],
361
+ outputs=[
362
+ audio_out,
363
+ video_out,
364
+ status,
365
+ ],
366
+ api_name="generate_foley",
367
+ )
368
+
369
+ demo.queue(max_size=20).launch()
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ffmpeg
2
+ git
3
+ libsndfile1
requirements.txt ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face ZeroGPU currently supports PyTorch 2.8+
2
+ torch==2.8.0
3
+ torchvision==0.23.0
4
+ torchaudio==2.8.0
5
+
6
+ gradio>=5.50,<7
7
+ spaces
8
+ huggingface_hub>=0.36.0
9
+
10
+ # ControlFoley runtime dependencies
11
+ numpy==1.26.4
12
+ av==14.0.1
13
+ colorlog==6.9.0
14
+ einops==0.8.0
15
+ omegaconf==2.3.0
16
+ open_clip_torch==2.29.0
17
+ laion_clap==1.1.6
18
+ julius==0.2.7
19
+ librosa==0.11.0
20
+ soundfile==0.13.1
21
+ scipy>=1.13,<1.16
22
+ scikit-learn>=1.5,<1.8
23
+ pandas>=2.2,<2.4
24
+ h5py>=3.11
25
+ timm>=1.0
26
+ transformers==4.38.2
27
+ tokenizers==0.15.2
28
+ sentencepiece>=0.2
29
+ safetensors>=0.4
30
+ torchdiffeq==0.2.5
31
+ torchlibrosa==0.1.0
32
+ nnAudio==0.3.3
33
+ webdataset==1.0.2
34
+ flashy==0.0.2
35
+ dora_search==0.1.12
36
+ submitit==1.5.4
37
+ treetable==0.2.6
38
+ wget==3.2
39
+ ftfy>=6.2
40
+ regex
41
+ requests
42
+ PyYAML