yasserrmd commited on
Commit
b7071af
·
verified ·
1 Parent(s): 652a204

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +641 -146
app.py CHANGED
@@ -1,7 +1,11 @@
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
@@ -12,11 +16,21 @@ 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"
@@ -25,61 +39,93 @@ ASSET_DIR = APP_DIR / "sample_assets"
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,
@@ -88,55 +134,246 @@ from controlfoley.inference_utils import (
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,
@@ -147,35 +384,107 @@ def generate_foley(
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,
@@ -183,25 +492,68 @@ def generate_foley(
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,
@@ -209,14 +561,33 @@ def generate_foley(
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,
@@ -224,61 +595,142 @@ def generate_foley(
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,
@@ -286,69 +738,98 @@ with gr.Blocks(title="ControlFoley Video to Audio") as demo:
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,
@@ -358,12 +839,26 @@ with gr.Blocks(title="ControlFoley Video to Audio") as demo:
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()
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # ControlFoley - Hugging Face ZeroGPU Gradio App
3
+ # Python 3.10
4
+ # PyTorch 2.8 / ZeroGPU compatible
5
+ # ============================================================
6
+
7
+ # IMPORTANT:
8
+ # Hugging Face ZeroGPU requires importing spaces BEFORE torch.
9
  import spaces
10
 
11
  import os
 
16
  import subprocess
17
  import tempfile
18
  from pathlib import Path
19
+ from contextlib import contextmanager
20
 
21
  import gradio as gr
22
  from huggingface_hub import snapshot_download
23
 
24
+ import torch
25
+ import torchaudio
26
+
27
+
28
+ # ============================================================
29
+ # Paths / configuration
30
+ # ============================================================
31
+
32
  APP_DIR = Path(__file__).resolve().parent
33
+
34
  SOURCE_DIR = APP_DIR / "upstream_controlfoley"
35
  MODEL_DIR = APP_DIR / "model_weights"
36
  OUTPUT_DIR = APP_DIR / "outputs"
 
39
  UPSTREAM_REPO = "https://github.com/xiaomi-research/controlfoley.git"
40
  MODEL_REPO = "YJX-Xiaomi/ControlFoley"
41
 
42
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
43
+ ASSET_DIR.mkdir(parents=True, exist_ok=True)
44
+
45
+ # ControlFoley contains several relative paths internally.
46
+ # Make sure its expected working directory is the Space root.
47
+ os.chdir(APP_DIR)
48
 
49
 
50
+ # ============================================================
51
+ # Download official ControlFoley source
52
+ # ============================================================
53
+
54
  def clone_upstream():
 
55
  if (SOURCE_DIR / "controlfoley").exists():
56
+ print("ControlFoley source already available.")
57
  return
58
 
59
+ print("Cloning official ControlFoley repository...")
60
+
61
+ subprocess.run(
62
+ [
63
+ "git",
64
+ "clone",
65
+ "--depth",
66
+ "1",
67
+ UPSTREAM_REPO,
68
+ str(SOURCE_DIR),
69
+ ],
70
+ check=True,
71
+ )
72
+
73
 
74
+ # ============================================================
75
+ # Download official ControlFoley model weights
76
+ # ============================================================
77
 
78
  def download_model_files():
79
+ required_main = MODEL_DIR / "weights" / "controlfoley.pth"
80
+
81
+ required_ext = [
82
+ MODEL_DIR / "ext_weights" / "v1-44.pth",
83
+ MODEL_DIR / "ext_weights" / "synchformer_state_dict.pth",
84
+ MODEL_DIR / "ext_weights" / "cav_mae_st.pth",
85
+ MODEL_DIR
86
+ / "ext_weights"
87
+ / "music_speech_audioset_epoch_15_esc_89.98.pt",
88
+ ]
89
+
90
+ if required_main.exists() and all(x.exists() for x in required_ext):
91
+ print("ControlFoley model files already available.")
92
+ return
93
+
94
+ print("Downloading ControlFoley model weights...")
95
+
96
  snapshot_download(
97
  repo_id=MODEL_REPO,
98
  local_dir=str(MODEL_DIR),
99
+ allow_patterns=[
100
+ "weights/*",
101
+ "ext_weights/*",
102
+ ],
103
  )
104
 
105
 
106
+ # ============================================================
107
+ # Startup downloads
108
+ # ============================================================
 
 
109
 
 
 
 
 
 
 
 
 
110
  clone_upstream()
111
  download_model_files()
112
 
 
 
 
113
 
114
+ # ============================================================
115
+ # Configure Python paths
116
+ # ============================================================
117
+
118
+ # ControlFoley imports modules from both repository root and lib/.
119
+ if str(SOURCE_DIR) not in sys.path:
120
+ sys.path.insert(0, str(SOURCE_DIR))
121
+
122
+ if str(SOURCE_DIR / "lib") not in sys.path:
123
+ sys.path.insert(0, str(SOURCE_DIR / "lib"))
124
+
125
+
126
+ # ============================================================
127
+ # Import ControlFoley
128
+ # ============================================================
129
 
130
  from controlfoley.inference_utils import (
131
  all_model_cfg,
 
134
  make_video,
135
  setup_eval_logging,
136
  )
137
+
138
+ from controlfoley.audio_model import (
139
+ create_audio_generation_model,
140
+ )
141
+
142
  from controlfoley.feature_extractor import FeaturesUtils
143
+
144
  from lib.flow_matching import FlowMatching
145
 
146
+
147
+ # ============================================================
148
+ # Logging
149
+ # ============================================================
150
+
151
  setup_eval_logging()
152
+
153
  log = logging.getLogger("controlfoley-space")
154
 
155
  torch.backends.cuda.matmul.allow_tf32 = True
156
  torch.backends.cudnn.allow_tf32 = True
157
 
158
+
159
+ # ============================================================
160
+ # PyTorch 2.6+ compatibility
161
+ # ============================================================
162
+ #
163
+ # PyTorch >=2.6 changed:
164
+ #
165
+ # torch.load(..., weights_only=False)
166
+ #
167
+ # from effectively the old default behavior to:
168
+ #
169
+ # weights_only=True
170
+ #
171
+ # Some older libraries bundled/used by ControlFoley, especially
172
+ # LAION-CLAP and AudioCraft/MusicGen, call torch.load() without
173
+ # specifying weights_only.
174
+ #
175
+ # Their official checkpoints contain more than plain tensor
176
+ # state dictionaries, so weights_only=True fails.
177
+ #
178
+ # We DO NOT monkey-patch torch.load globally.
179
+ #
180
+ # Instead, this compatibility context is active ONLY while
181
+ # initializing trusted upstream ControlFoley feature models.
182
+ # ============================================================
183
+
184
+ @contextmanager
185
+ def legacy_checkpoint_loading():
186
+ original_torch_load = torch.load
187
+
188
+ def compatible_torch_load(*args, **kwargs):
189
+ # Preserve an explicit value supplied by a library.
190
+ #
191
+ # Only restore the old behavior when the caller does
192
+ # not specify weights_only at all.
193
+ if "weights_only" not in kwargs:
194
+ kwargs["weights_only"] = False
195
+
196
+ return original_torch_load(*args, **kwargs)
197
+
198
+ torch.load = compatible_torch_load
199
+
200
+ try:
201
+ yield
202
+ finally:
203
+ torch.load = original_torch_load
204
+
205
+
206
+ # ============================================================
207
+ # ControlFoley configuration
208
+ # ============================================================
209
+
210
  MODEL_CFG = all_model_cfg["large_44k"]
211
  SEQ_CFG = MODEL_CFG.seq_cfg
212
 
213
+
214
+ # ============================================================
215
+ # Main ControlFoley network
216
+ # ============================================================
217
+
218
+ print("=" * 60)
219
  print("Loading ControlFoley main network...")
220
+ print("=" * 60)
221
+
222
+ NET = create_audio_generation_model(
223
+ MODEL_CFG.model_name
224
+ ).to(
225
+ "cuda",
226
+ torch.float32,
227
  ).eval()
228
+
229
+
230
+ MAIN_CHECKPOINT = MODEL_DIR / "weights" / "controlfoley.pth"
231
+
232
+ # The main ControlFoley checkpoint is a normal weight state dict,
233
+ # therefore keeping weights_only=True is appropriate here.
234
+ main_state = torch.load(
235
+ MAIN_CHECKPOINT,
236
+ map_location="cuda",
237
+ weights_only=True,
238
  )
239
 
240
+ NET.load_weights(main_state)
241
+
242
+ del main_state
243
+
244
+ print("ControlFoley main network loaded.")
245
+
246
+
247
+ # ============================================================
248
+ # Feature extractor stack
249
+ # ============================================================
250
+ #
251
+ # Includes:
252
+ #
253
+ # - DFN5B CLIP
254
+ # - Synchformer
255
+ # - CAV-MAE-ST
256
+ # - LAION CLAP
257
+ # - AudioCraft MusicGen Style
258
+ # - VAE / vocoder
259
+ #
260
+ # LAION CLAP and some AudioCraft checkpoints require the legacy
261
+ # PyTorch checkpoint-loading behavior.
262
+ # ============================================================
263
+
264
+ print("=" * 60)
265
  print("Loading ControlFoley feature extractors...")
266
+ print("=" * 60)
267
+
268
+ with legacy_checkpoint_loading():
269
+
270
+ FEATURES = FeaturesUtils(
271
+ tod_vae_ckpt=str(
272
+ MODEL_DIR
273
+ / "ext_weights"
274
+ / "v1-44.pth"
275
+ ),
276
+
277
+ synchformer_ckpt=str(
278
+ MODEL_DIR
279
+ / "ext_weights"
280
+ / "synchformer_state_dict.pth"
281
+ ),
282
+
283
+ cav_mae_ckpt=str(
284
+ MODEL_DIR
285
+ / "ext_weights"
286
+ / "cav_mae_st.pth"
287
+ ),
288
+
289
+ clap_ckpt=str(
290
+ MODEL_DIR
291
+ / "ext_weights"
292
+ / "music_speech_audioset_epoch_15_esc_89.98.pt"
293
+ ),
294
+
295
+ mode=MODEL_CFG.mode,
296
+ enable_conditions=True,
297
+ need_vae_encoder=False,
298
+ )
299
+
300
+
301
+ FEATURES = FEATURES.to(
302
+ "cuda",
303
+ torch.float32,
304
+ ).eval()
305
+
306
+ print("ControlFoley feature extractors loaded.")
307
+
308
 
309
+ # ============================================================
310
+ # Sample video
311
+ # ============================================================
312
 
313
+ def prepare_sample_video():
314
+ target = ASSET_DIR / "001.mp4"
315
 
316
+ if target.exists():
317
+ return target
 
318
 
319
+ upstream_sample = SOURCE_DIR / "assets" / "001.mp4"
320
 
321
+ if not upstream_sample.exists():
322
+ print("Sample video not found.")
323
+ return None
324
+
325
+ shutil.copy2(
326
+ upstream_sample,
327
+ target,
328
+ )
329
+
330
+ return target
331
+
332
+
333
+ SAMPLE_VIDEO = prepare_sample_video()
334
+
335
+
336
+ # ============================================================
337
+ # Dynamic ZeroGPU duration
338
+ # ============================================================
339
+
340
+ def gpu_budget(
341
+ video_path,
342
+ prompt,
343
+ negative_prompt,
344
+ duration,
345
+ cfg_strength,
346
+ steps,
347
+ seed,
348
+ ):
349
+ """
350
+ Allocate enough ZeroGPU time depending on inference settings.
351
+ """
352
+
353
+ duration = float(duration)
354
+ steps = int(steps)
355
+
356
+ estimated = (
357
+ 60
358
+ + int(duration * 12)
359
+ + int(steps * 3)
360
+ )
361
+
362
+ # ZeroGPU maximum requested allocation.
363
+ return min(
364
+ 300,
365
+ max(120, estimated),
366
+ )
367
+
368
+
369
+ # ============================================================
370
+ # Main generation function
371
+ # ============================================================
372
+
373
+ @spaces.GPU(
374
+ size="xlarge",
375
+ duration=gpu_budget,
376
+ )
377
  @torch.inference_mode()
378
  def generate_foley(
379
  video_path,
 
384
  steps,
385
  seed,
386
  ):
387
+
388
  if not video_path:
389
+ raise gr.Error(
390
+ "Please upload a video or select the sample video."
391
+ )
392
+
393
+ # --------------------------------------------------------
394
+ # Parameters
395
+ # --------------------------------------------------------
396
 
397
  video_path = Path(video_path)
398
+
399
  duration = float(duration)
400
  cfg_strength = float(cfg_strength)
401
  steps = int(steps)
402
  seed = int(seed)
403
 
404
+ if duration < 1 or duration > 8:
405
+ raise gr.Error(
406
+ "Duration must be between 1 and 8 seconds."
407
+ )
408
+
409
+ if steps < 5 or steps > 30:
410
+ raise gr.Error(
411
+ "Inference steps must be between 5 and 30."
412
+ )
413
+
414
+ if not video_path.exists():
415
+ raise gr.Error(
416
+ "The uploaded video could not be found."
417
+ )
418
+
419
+
420
+ # --------------------------------------------------------
421
+ # Output directory
422
+ # --------------------------------------------------------
423
+
424
+ job_dir = Path(
425
+ tempfile.mkdtemp(
426
+ prefix="controlfoley_",
427
+ dir=str(OUTPUT_DIR),
428
+ )
429
+ )
430
+
431
+ audio_path = (
432
+ job_dir
433
+ / "generated_foley.flac"
434
+ )
435
+
436
+ video_out_path = (
437
+ job_dir
438
+ / "video_with_generated_audio.mp4"
439
+ )
440
+
441
 
442
+ # --------------------------------------------------------
443
+ # Load / preprocess video
444
+ # --------------------------------------------------------
445
 
446
+ print(f"Loading video: {video_path}")
447
+
448
+ video_info = load_video(
449
+ video_path,
450
+ duration,
451
+ )
452
+
453
+ actual_duration = min(
454
+ duration,
455
+ float(video_info.total_duration),
456
+ )
457
 
 
 
 
458
 
459
+ # --------------------------------------------------------
460
+ # Video conditioning
461
+ # --------------------------------------------------------
462
+
463
+ clip_frames = (
464
+ video_info
465
+ .clip_embeddings
466
+ .unsqueeze(0)
467
+ )
468
+
469
+ visual_frames = (
470
+ video_info
471
+ .visual_features
472
+ .unsqueeze(0)
473
+ )
474
+
475
+ sync_frames = (
476
+ video_info
477
+ .sync_embeddings
478
+ .unsqueeze(0)
479
+ )
480
+
481
+
482
+ # --------------------------------------------------------
483
+ # Configure temporal dimensions
484
+ # --------------------------------------------------------
485
+
486
  SEQ_CFG.total_time_seconds = actual_duration
487
+
488
  NET.update_seq_lengths(
489
  SEQ_CFG.latent_sequence_length,
490
  SEQ_CFG.clip_sequence_length,
 
492
  SEQ_CFG.sync_sequence_length,
493
  )
494
 
495
+
496
+ # --------------------------------------------------------
497
+ # Random generator
498
+ # --------------------------------------------------------
499
+
500
+ rng = torch.Generator(
501
+ device="cuda"
502
+ )
503
+
504
  rng.manual_seed(seed)
505
+
506
+
507
+ # --------------------------------------------------------
508
+ # Flow matching sampler
509
+ # --------------------------------------------------------
510
+
511
  fm = FlowMatching(
512
  min_sigma=0,
513
  inference_mode="euler",
514
  num_steps=steps,
515
  )
516
 
517
+
518
+ # --------------------------------------------------------
519
+ # Generate
520
+ # --------------------------------------------------------
521
+
522
+ print("=" * 60)
523
+ print("Generating Foley audio...")
524
+ print(f"Prompt: {prompt}")
525
+ print(f"Duration: {actual_duration}")
526
+ print(f"Steps: {steps}")
527
+ print(f"CFG: {cfg_strength}")
528
+ print(f"Seed: {seed}")
529
+ print("=" * 60)
530
+
531
+ start_time = time.time()
532
+
533
 
534
  audios = generate(
535
+
536
+ # Video conditioning
537
  clip_frames,
538
  visual_frames,
539
  sync_frames,
540
+
541
+ # No reference audio
542
+ None,
543
+
544
+ # No timbre reference
545
+ None,
546
+
547
+ # Reference audio duration
548
  0.0,
549
+
550
+ # Text prompt
551
  [prompt or ""],
552
+
553
+ negative_text=[
554
+ negative_prompt or ""
555
+ ],
556
+
557
  feature_utils=FEATURES,
558
  net=NET,
559
  fm=fm,
 
561
  cfg_strength=cfg_strength,
562
  )
563
 
564
+
565
+ # --------------------------------------------------------
566
+ # Convert generated tensor
567
+ # --------------------------------------------------------
568
+
569
+ audio = (
570
+ audios
571
+ .float()
572
+ .cpu()[0]
573
+ )
574
+
575
+
576
+ # --------------------------------------------------------
577
+ # Save generated FLAC
578
+ # --------------------------------------------------------
579
+
580
  torchaudio.save(
581
  str(audio_path),
582
  audio,
583
  SEQ_CFG.audio_sample_rate,
584
  )
585
 
586
+
587
+ # --------------------------------------------------------
588
+ # Mux generated audio with original video
589
+ # --------------------------------------------------------
590
+
591
  make_video(
592
  video_info,
593
  video_out_path,
 
595
  sampling_rate=SEQ_CFG.audio_sample_rate,
596
  )
597
 
 
 
 
 
 
598
 
599
+ # --------------------------------------------------------
600
+ # Finished
601
+ # --------------------------------------------------------
602
+
603
+ elapsed = time.time() - start_time
604
+
605
+ status = f"""
606
+ ### ✅ Generation complete
607
+
608
+ **Duration:** {actual_duration:.2f}s
609
+ **Generation time:** {elapsed:.1f}s
610
+ **Steps:** {steps}
611
+ **CFG:** {cfg_strength}
612
+ **Seed:** {seed}
613
+ """
614
+
615
 
616
+ # --------------------------------------------------------
617
+ # Release temporary tensors
618
+ # --------------------------------------------------------
619
 
620
+ del audios
621
+ del audio
622
+
623
+ try:
624
+ del clip_frames
625
+ del visual_frames
626
+ del sync_frames
627
+ except Exception:
628
+ pass
629
+
630
+ if torch.cuda.is_available():
631
+ torch.cuda.empty_cache()
632
+
633
+
634
+ return (
635
+ str(audio_path),
636
+ str(video_out_path),
637
+ status,
638
+ )
639
+
640
+
641
+ # ============================================================
642
+ # Gradio UI
643
+ # ============================================================
644
 
645
  TITLE = """
646
  # 🎬 ControlFoley — Video → Foley Audio
 
 
647
 
648
+ Generate synchronized Foley sound effects from video using
649
+ **Xiaomi Research ControlFoley**.
650
+
651
+ Upload a video or select the official sample below.
652
+
653
+ You can use:
654
+
655
+ - **V2A** — leave the prompt empty
656
+ - **TV2A** — describe the sound you want
657
  """
658
 
 
 
 
 
 
659
 
660
+ HELP_TEXT = """
661
+ ### Usage
662
+
663
+ **Pure Video → Audio**
664
+
665
+ Leave the prompt blank.
666
+
667
+ **Text-guided Video → Audio**
668
+
669
+ Describe the expected sound.
670
+
671
+ Example:
672
+
673
  `the skateboard wheels scraping and grinding on the ground.`
674
+
675
+ For the first test, use:
676
+
677
+ - Duration: **8 seconds**
678
+ - Steps: **25**
679
+ - CFG: **4.5**
680
+ - Seed: **42**
681
+
682
+ If you want a quicker test, reduce inference steps to **10–15**.
683
  """
684
 
685
+
686
+ with gr.Blocks(
687
+ title="ControlFoley Video to Audio"
688
+ ) as demo:
689
+
690
  gr.Markdown(TITLE)
691
 
692
  with gr.Row():
693
+
694
+ # ====================================================
695
+ # INPUT
696
+ # ====================================================
697
+
698
  with gr.Column():
699
+
700
  video = gr.Video(
701
+ label="Input Video",
702
  sources=["upload"],
703
  format="mp4",
704
  )
705
+
706
  prompt = gr.Textbox(
707
+ label="Sound Prompt",
708
+ placeholder=(
709
+ "Describe the sound, or leave blank "
710
+ "for pure Video-to-Audio"
711
+ ),
712
+ value=(
713
+ "the skateboard wheels scraping "
714
+ "and grinding on the ground."
715
+ ),
716
+ lines=2,
717
  )
718
+
719
  negative_prompt = gr.Textbox(
720
+ label="Negative Prompt",
721
+ placeholder=(
722
+ "Example: music, speech, crowd noise"
723
+ ),
724
  value="",
725
+ lines=1,
726
  )
727
 
728
+
729
+ with gr.Accordion(
730
+ "Generation Settings",
731
+ open=False,
732
+ ):
733
+
734
  duration = gr.Slider(
735
  minimum=1,
736
  maximum=8,
 
738
  step=0.5,
739
  label="Duration (seconds)",
740
  )
741
+
742
  cfg_strength = gr.Slider(
743
  minimum=1.0,
744
  maximum=8.0,
745
  value=4.5,
746
  step=0.5,
747
+ label="CFG Strength",
748
  )
749
+
750
  steps = gr.Slider(
751
  minimum=5,
752
  maximum=30,
753
  value=25,
754
  step=1,
755
+ label="Inference Steps",
756
  )
757
+
758
  seed = gr.Number(
759
  value=42,
760
  precision=0,
761
  label="Seed",
762
  )
763
 
764
+
765
  generate_btn = gr.Button(
766
  "Generate Foley Sound",
767
  variant="primary",
768
  )
769
 
770
+
771
+ # ====================================================
772
+ # OUTPUT
773
+ # ====================================================
774
+
775
  with gr.Column():
776
+
777
  audio_out = gr.Audio(
778
+ label="Generated Foley Audio",
779
  type="filepath",
780
  )
781
+
782
  video_out = gr.Video(
783
+ label="Video + Generated Foley",
784
  )
785
+
786
  status = gr.Markdown()
787
 
788
+
789
+ # ========================================================
790
+ # Sample
791
+ # ========================================================
792
+
793
+ if SAMPLE_VIDEO is not None:
794
+
795
+ gr.Examples(
796
+ examples=[
797
+ [
798
+ str(SAMPLE_VIDEO),
799
+ (
800
+ "the skateboard wheels scraping "
801
+ "and grinding on the ground."
802
+ ),
803
+ "",
804
+ 8,
805
+ 4.5,
806
+ 25,
807
+ 42,
808
+ ],
809
  ],
810
+ inputs=[
811
+ video,
812
+ prompt,
813
+ negative_prompt,
814
+ duration,
815
+ cfg_strength,
816
+ steps,
817
+ seed,
818
+ ],
819
+ label="Official ControlFoley Sample",
820
+ )
821
+
822
+
823
+ gr.Markdown(HELP_TEXT)
824
+
825
+
826
+ # ========================================================
827
+ # Generation event
828
+ # ========================================================
829
 
830
  generate_btn.click(
831
  fn=generate_foley,
832
+
833
  inputs=[
834
  video,
835
  prompt,
 
839
  steps,
840
  seed,
841
  ],
842
+
843
  outputs=[
844
  audio_out,
845
  video_out,
846
  status,
847
  ],
848
+
849
  api_name="generate_foley",
850
+
851
+ concurrency_limit=1,
852
  )
853
 
854
+
855
+ # ============================================================
856
+ # Launch
857
+ # ============================================================
858
+
859
+ if __name__ == "__main__":
860
+
861
+ demo.queue(
862
+ max_size=20,
863
+ default_concurrency_limit=1,
864
+ ).launch()