R-Kentaren commited on
Commit
1d26fcf
·
verified ·
1 Parent(s): 5328a46

fix: main.py - bug fixes for audio loading & validation

Browse files
Files changed (1) hide show
  1. src/main.py +766 -246
src/main.py CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  import spaces
2
  import torch
3
  import argparse
@@ -7,9 +11,12 @@ import json
7
  import os
8
  import shlex
9
  import subprocess
 
10
  from contextlib import suppress
 
 
 
11
  from urllib.parse import urlparse, parse_qs
12
- import time
13
  import shutil
14
 
15
  import gradio as gr
@@ -37,33 +44,289 @@ rvc_models_dir = os.path.join(BASE_DIR, 'rvc_models')
37
  output_dir = os.path.join(BASE_DIR, 'song_output')
38
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  def clean_old_folders(base_path: str, max_age_seconds: int = 10800):
 
41
  if not os.path.isdir(base_path):
42
- print(f"Error: {base_path} is not a valid directory.")
43
  return
44
 
45
  now = time.time()
 
46
 
47
  for folder_name in os.listdir(base_path):
48
  folder_path = os.path.join(base_path, folder_name)
49
  if os.path.isdir(folder_path):
50
  last_modified = os.path.getmtime(folder_path)
51
  if now - last_modified > max_age_seconds:
52
- # print(f"Deleting folder: {folder_path}")
53
- shutil.rmtree(folder_path)
 
 
 
 
 
 
54
 
55
 
56
  def get_youtube_video_id(url, ignore_playlist=True):
57
  """
 
 
58
  Examples:
59
- http://youtu.be/SA2iWivDJiE
60
- http://www.youtube.com/watch?v=_oPAwA_Udwc&feature=feedu
61
- http://www.youtube.com/embed/SA2iWivDJiE
62
- http://www.youtube.com/v/SA2iWivDJiE?version=3&hl=en_US
63
  """
64
  if "m.youtube.com" in url:
65
  url = url.replace("m.youtube.com", "www.youtube.com")
 
66
  query = urlparse(url)
 
67
  if query.hostname == 'youtu.be':
68
  if query.path[1:] == 'watch':
69
  return query.query[2:]
@@ -71,7 +334,6 @@ def get_youtube_video_id(url, ignore_playlist=True):
71
 
72
  if query.hostname in {'www.youtube.com', 'youtube.com', 'music.youtube.com'}:
73
  if not ignore_playlist:
74
- # use case: get playlist id not current video in playlist
75
  with suppress(KeyError):
76
  return parse_qs(query.query)['list'][0]
77
  if query.path == '/watch':
@@ -81,34 +343,58 @@ def get_youtube_video_id(url, ignore_playlist=True):
81
  if query.path[:7] == '/embed/':
82
  return query.path.split('/')[2]
83
  if query.path[:3] == '/v/':
84
- return query.path.split('/')[2]
85
 
86
- # returns None for invalid YouTube url
87
  return None
88
 
89
 
90
- def yt_download(link):
 
 
 
 
 
 
 
 
 
 
91
  if not link.strip():
92
- gr.Info("You need to provide a download link.")
93
- return None
 
 
 
 
 
 
94
  ydl_opts = {
95
- 'format': 'bestaudio',
96
  'outtmpl': '%(title)s',
97
  'nocheckcertificate': True,
98
  'ignoreerrors': True,
99
  'no_warnings': True,
100
  'quiet': True,
101
  'extractaudio': True,
102
- 'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3'}],
 
 
 
 
 
103
  }
 
104
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
105
  result = ydl.extract_info(link, download=True)
 
 
106
  download_path = ydl.prepare_filename(result, outtmpl='%(title)s.mp3')
107
 
108
  return download_path
109
 
110
 
111
  def raise_exception(error_msg, is_webui):
 
112
  if is_webui:
113
  raise gr.Error(error_msg)
114
  else:
@@ -116,33 +402,49 @@ def raise_exception(error_msg, is_webui):
116
 
117
 
118
  def get_rvc_model(voice_model, is_webui):
 
 
 
 
 
 
119
  rvc_model_filename, rvc_index_filename = None, None
120
  model_dir = os.path.join(rvc_models_dir, voice_model)
121
- # print(model_dir)
 
 
 
 
122
  for file in os.listdir(model_dir):
123
- # print(file)
124
- if os.path.isdir(file):
125
- for ff in os.listdir(file):
126
- # print("subfile", ff)
 
127
  ext = os.path.splitext(ff)[1]
128
  if ext == '.pth':
129
  rvc_model_filename = ff
130
- if ext == '.index':
131
  rvc_index_filename = ff
132
- ext = os.path.splitext(file)[1]
133
- if ext == '.pth':
134
- rvc_model_filename = file
135
- if ext == '.index':
136
- rvc_index_filename = file
 
137
 
138
  if rvc_model_filename is None:
139
- error_msg = f'No model file exists in {model_dir}.'
140
  raise_exception(error_msg, is_webui)
141
 
142
- return os.path.join(model_dir, rvc_model_filename), os.path.join(model_dir, rvc_index_filename) if rvc_index_filename else ''
 
 
 
143
 
144
 
145
  def get_audio_paths(song_dir):
 
146
  orig_song_path = None
147
  instrumentals_path = None
148
  main_vocals_dereverb_path = None
@@ -153,41 +455,75 @@ def get_audio_paths(song_dir):
153
  instrumentals_path = os.path.join(song_dir, file)
154
  orig_song_path = instrumentals_path.replace('_Instrumental', '')
155
 
156
- elif file.endswith('_Vocals_Main_DeReverb.wav'):
157
  main_vocals_dereverb_path = os.path.join(song_dir, file)
158
 
159
  elif file.endswith('_Vocals_Backup.wav'):
160
  backup_vocals_path = os.path.join(song_dir, file)
161
 
162
- # print(orig_song_path, instrumentals_path, main_vocals_dereverb_path, backup_vocals_path)
163
  return orig_song_path, instrumentals_path, main_vocals_dereverb_path, backup_vocals_path
164
 
165
 
166
  def get_audio_with_suffix(song_dir, suffix="_mysuffix.wav"):
167
- target_path = None
168
-
169
  for file in os.listdir(song_dir):
170
  if file.endswith(suffix):
171
- target_path = os.path.join(song_dir, file)
172
- break
173
-
174
- return target_path
175
 
176
 
177
  def convert_to_stereo(audio_path):
178
- wave, sr = librosa.load(audio_path, mono=False, sr=44100)
179
-
180
- # check if mono
181
- if type(wave[0]) != np.ndarray:
182
- stereo_path = f'{os.path.splitext(audio_path)[0]}_stereo.wav'
183
- command = shlex.split(f'ffmpeg -y -loglevel error -i "{audio_path}" -ac 2 -f wav "{stereo_path}"')
184
- subprocess.run(command)
185
- return stereo_path
186
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return audio_path
188
 
189
 
190
  def pitch_shift(audio_path, pitch_change):
 
191
  output_path = f'{os.path.splitext(audio_path)[0]}_p{pitch_change}.wav'
192
  if not os.path.exists(output_path):
193
  y, sr = sf.read(audio_path)
@@ -200,76 +536,25 @@ def pitch_shift(audio_path, pitch_change):
200
 
201
 
202
  def get_hash(filepath):
 
203
  with open(filepath, 'rb') as f:
204
  file_hash = hashlib.blake2b()
205
  while chunk := f.read(8192):
206
  file_hash.update(chunk)
207
-
208
  return file_hash.hexdigest()[:11]
209
 
210
 
211
- def display_progress(message, percent, is_webui, progress=None):
212
- if is_webui:
213
- progress(percent, desc=message)
214
- else:
215
- print(message)
216
-
217
-
218
- def preprocess_song(song_input, mdx_model_params, song_id, is_webui, input_type, progress, keep_orig, orig_song_path):
219
-
220
- song_output_dir = os.path.join(output_dir, song_id)
221
-
222
- display_progress('[~] Separating Vocals from Instrumental...', 0.1, is_webui, progress)
223
- vocals_path, instrumentals_path = run_mdx(mdx_model_params, song_output_dir, os.path.join(mdxnet_models_dir, 'UVR-MDX-NET-Voc_FT.onnx'), orig_song_path, denoise=True, keep_orig=keep_orig)
224
-
225
- display_progress('[~] Separating Main Vocals from Backup Vocals...', 0.2, is_webui, progress)
226
- backup_vocals_path, main_vocals_path = run_mdx(mdx_model_params, song_output_dir, os.path.join(mdxnet_models_dir, 'UVR_MDXNET_KARA_2.onnx'), vocals_path, suffix='Backup', invert_suffix='Main', denoise=True)
227
-
228
- display_progress('[~] Applying DeReverb to Vocals...', 0.3, is_webui, progress)
229
- _, main_vocals_dereverb_path = run_mdx(mdx_model_params, song_output_dir, os.path.join(mdxnet_models_dir, 'Reverb_HQ_By_FoxJoy.onnx'), main_vocals_path, invert_suffix='DeReverb', exclude_main=True, denoise=True)
230
-
231
- return orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path
232
-
233
-
234
- def get_audio_file(song_input, is_webui, input_type, progress):
235
- keep_orig = False
236
- if input_type == 'yt':
237
- display_progress('[~] Downloading song...', 0, is_webui, progress)
238
- song_link = song_input.split('&')[0]
239
- orig_song_path = yt_download(song_link)
240
- elif input_type == 'local':
241
- orig_song_path = song_input
242
- keep_orig = True
243
- else:
244
- orig_song_path = None
245
- return keep_orig, orig_song_path
246
-
247
- device = "cuda:0" if torch.cuda.is_available() else "cpu"
248
- compute_half = True if torch.cuda.is_available() else False
249
- config = Config(device, compute_half)
250
- hubert_model = load_hubert("cuda", config.is_half, None)
251
- print(device, "half>>", config.is_half)
252
-
253
- @spaces.GPU(duration=65)
254
- def voice_change(voice_model, vocals_path, output_path, pitch_change, f0_method, index_rate, filter_radius, rms_mix_rate, protect, crepe_hop_length, is_webui, steps):
255
- rvc_model_path, rvc_index_path = get_rvc_model(voice_model, is_webui)
256
-
257
- cpt, version, net_g, tgt_sr, vc = get_vc(device, config.is_half, config, rvc_model_path)
258
-
259
- # convert main vocals
260
- global hubert_model
261
- rvc_infer(rvc_index_path, index_rate, vocals_path, output_path, pitch_change, f0_method, cpt, version, net_g, filter_radius, tgt_sr, rms_mix_rate, protect, crepe_hop_length, vc, hubert_model, steps)
262
- del cpt
263
- gc.collect()
264
-
265
 
266
  def add_audio_effects(audio_path, reverb_rm_size, reverb_wet, reverb_dry, reverb_damping):
 
267
  output_path = f'{os.path.splitext(audio_path)[0]}_mixed.wav'
268
 
269
- # Initialize audio effects plugins
270
  board = Pedalboard(
271
  [
272
- HighpassFilter(),
273
  Compressor(ratio=4, threshold_db=-15),
274
  Reverb(room_size=reverb_rm_size, dry_level=reverb_dry, wet_level=reverb_wet, damping=reverb_damping)
275
  ]
@@ -277,7 +562,6 @@ def add_audio_effects(audio_path, reverb_rm_size, reverb_wet, reverb_dry, reverb
277
 
278
  with AudioFile(audio_path) as f:
279
  with AudioFile(output_path, 'w', f.samplerate, f.num_channels) as o:
280
- # Read one second of audio at a time, until the file is empty:
281
  while f.tell() < f.frames:
282
  chunk = f.read(int(f.samplerate))
283
  effected = board(chunk, f.samplerate, reset=False)
@@ -287,35 +571,171 @@ def add_audio_effects(audio_path, reverb_rm_size, reverb_wet, reverb_dry, reverb
287
 
288
 
289
  def combine_audio(audio_paths, output_path, main_gain, backup_gain, inst_gain, output_format):
290
- main_vocal_audio = AudioSegment.from_wav(audio_paths[0]) - 4 + main_gain
291
- backup_vocal_audio = AudioSegment.from_wav(audio_paths[1]) - 6 + backup_gain
292
- instrumental_audio = AudioSegment.from_wav(audio_paths[2]) - 7 + inst_gain
293
- main_vocal_audio.overlay(backup_vocal_audio).overlay(instrumental_audio).export(output_path, format=output_format)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
 
296
  @spaces.GPU(duration=65)
297
  def process_song(
298
- song_dir, song_input, mdx_model_params, song_id, is_webui, input_type, progress,
299
- keep_files, pitch_change, pitch_change_all, voice_model, index_rate, filter_radius,
300
- rms_mix_rate, protect, f0_method, crepe_hop_length, output_format, keep_orig, orig_song_path, steps
 
 
301
  ):
302
-
 
 
303
  if not os.path.exists(song_dir):
304
  os.makedirs(song_dir)
305
- orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path = preprocess_song(song_input, mdx_model_params, song_id, is_webui, input_type, progress, keep_orig, orig_song_path)
 
 
306
  else:
307
  vocals_path, main_vocals_path = None, None
308
  paths = get_audio_paths(song_dir)
309
 
310
- # if any of the audio files aren't available or keep intermediate files, rerun preprocess
311
  if any(path is None for path in paths):
312
- orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path = preprocess_song(song_input, mdx_model_params, song_id, is_webui, input_type, progress, keep_orig, orig_song_path)
 
 
313
  else:
314
  orig_song_path, instrumentals_path, main_vocals_dereverb_path, backup_vocals_path = paths
315
 
 
316
  ins_path = get_audio_with_suffix(song_dir, "_Voiceless.wav")
317
  if not ins_path:
318
- display_progress('[~] Extracting voiceless track...', 0.4, is_webui, progress)
 
 
319
  instrumentals_path, _ = run_mdx(
320
  mdx_model_params,
321
  song_dir,
@@ -331,61 +751,46 @@ def process_song(
331
  else:
332
  instrumentals_path = ins_path
333
 
334
- pitch_change = pitch_change * 12 + pitch_change_all
335
- ai_vocals_path = os.path.join(song_dir, f'{os.path.splitext(os.path.basename(orig_song_path))[0]}_{voice_model}_p{pitch_change}_i{index_rate}_fr{filter_radius}_rms{rms_mix_rate}_pro{protect}_{f0_method}{"" if f0_method != "mangio-crepe" else f"_{crepe_hop_length}"}_s{steps}.wav')
336
- ai_cover_path = os.path.join(song_dir, f'{os.path.splitext(os.path.basename(orig_song_path))[0]} ({voice_model} Ver).{output_format}')
 
 
 
 
 
 
 
337
 
 
338
  if not os.path.exists(ai_vocals_path):
339
- display_progress('[~] Converting voice using RVC...', 0.5, is_webui, progress)
340
- voice_change(voice_model, main_vocals_dereverb_path, ai_vocals_path, pitch_change, f0_method, index_rate, filter_radius, rms_mix_rate, protect, crepe_hop_length, is_webui, steps)
 
 
 
 
 
341
 
342
  return ai_vocals_path, ai_cover_path, instrumentals_path, backup_vocals_path, vocals_path, main_vocals_path, ins_path
343
 
344
 
345
- def apply_noisereduce(audio_list, type_output="wav"):
346
- # https://github.com/sa-if/Audio-Denoiser
347
- print("Noice reduce")
348
-
349
- result = []
350
- for audio_path in audio_list:
351
- out_path = f"{os.path.splitext(audio_path)[0]}_nr.{type_output}"
352
-
353
- try:
354
- # Load audio file
355
- audio = AudioSegment.from_file(audio_path)
356
-
357
- # Convert audio to numpy array
358
- samples = np.array(audio.get_array_of_samples())
359
-
360
- # Reduce noise
361
- reduced_noise = nr.reduce_noise(samples, sr=audio.frame_rate, prop_decrease=0.6)
362
-
363
- # Convert reduced noise signal back to audio
364
- reduced_audio = AudioSegment(
365
- reduced_noise.tobytes(),
366
- frame_rate=audio.frame_rate,
367
- sample_width=audio.sample_width,
368
- channels=audio.channels
369
- )
370
-
371
- # Save reduced audio to file
372
- reduced_audio.export(out_path, format=type_output)
373
- result.append(out_path)
374
-
375
- except Exception as e:
376
- print(f"Error noisereduce: {str(e)}")
377
- result.append(audio_path)
378
-
379
- return result
380
-
381
-
382
- # @spaces.GPU(duration=140)
383
- def song_cover_pipeline(song_input, voice_model, pitch_change, keep_files,
384
- is_webui=0, main_gain=0, backup_gain=0, inst_gain=0, index_rate=0.5, filter_radius=3,
385
- rms_mix_rate=0.25, f0_method='rmvpe', crepe_hop_length=128, protect=0.33, pitch_change_all=0,
386
- reverb_rm_size=0.15, reverb_wet=0.2, reverb_dry=0.8, reverb_damping=0.7, output_format='mp3',
387
- extra_denoise=False, steps=1,
388
- progress=gr.Progress()):
389
  if not keep_files or IS_ZERO_GPU:
390
  clean_old_folders("./song_output", 14400)
391
 
@@ -393,52 +798,91 @@ def song_cover_pipeline(song_input, voice_model, pitch_change, keep_files,
393
  clean_old_folders("./rvc_models", 10800)
394
 
395
  try:
396
- # Improved input validation
397
  if not voice_model:
398
- raise_exception('Ensure that the voice model field is filled.', is_webui)
 
 
 
 
 
399
 
400
- # Handle song_input - allow None for audio component, check if it's a valid path
401
  if song_input is None or (isinstance(song_input, str) and len(song_input.strip()) == 0):
402
- raise_exception('Ensure that the song input field and voice model field is filled.', is_webui)
403
 
404
- # Clean up the input path
405
  if isinstance(song_input, str):
406
  song_input = song_input.strip().strip('\"').strip("'")
407
 
408
- print(f"[DEBUG] song_input type: {type(song_input)}, value: {song_input}")
409
- print(f"[DEBUG] voice_model: {voice_model}")
410
-
411
- display_progress('[~] Starting Hyper RVC Voice Conversion Pipeline...', 0, is_webui, progress)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
 
413
  with open(os.path.join(mdxnet_models_dir, 'model_data.json')) as infile:
414
  mdx_model_params = json.load(infile)
415
 
416
- # if youtube url
417
  if urlparse(song_input).scheme == 'https':
418
  input_type = 'yt'
419
  song_id = get_youtube_video_id(song_input)
420
  if song_id is None:
421
- error_msg = 'Invalid YouTube url.'
422
- raise_exception(error_msg, is_webui)
423
-
424
- # local audio file
425
  else:
426
  input_type = 'local'
427
- song_input = song_input.strip('\"')
428
  if os.path.exists(song_input):
429
  song_id = get_hash(song_input)
430
  else:
431
- error_msg = f'{song_input} does not exist.'
432
- song_id = None
433
  raise_exception(error_msg, is_webui)
 
434
 
435
  song_dir = os.path.join(output_dir, song_id)
436
 
437
- keep_orig, orig_song_path = get_audio_file(song_input, is_webui, input_type, progress)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  orig_song_path = convert_to_stereo(orig_song_path)
439
 
440
- start = time.time()
441
 
 
442
  (
443
  ai_vocals_path,
444
  ai_cover_path,
@@ -449,12 +893,10 @@ def song_cover_pipeline(song_input, voice_model, pitch_change, keep_files,
449
  ins_path
450
  ) = process_song(
451
  song_dir,
452
- song_input,
453
  mdx_model_params,
454
  song_id,
455
  is_webui,
456
  input_type,
457
- progress,
458
  keep_files,
459
  pitch_change,
460
  pitch_change_all,
@@ -469,52 +911,75 @@ def song_cover_pipeline(song_input, voice_model, pitch_change, keep_files,
469
  keep_orig,
470
  orig_song_path,
471
  steps,
 
472
  )
473
 
474
- end = time.time()
475
- print(f"Execution time: {end - start:.4f} seconds")
476
 
477
- display_progress('[~] Applying audio effects to Vocals...', 0.8, is_webui, progress)
 
478
 
479
  if extra_denoise:
 
480
  ai_vocals_path = apply_noisereduce([ai_vocals_path])[0]
481
 
482
- ai_vocals_mixed_path = add_audio_effects(ai_vocals_path, reverb_rm_size, reverb_wet, reverb_dry, reverb_damping)
 
 
483
 
 
484
  if pitch_change_all != 0:
485
- display_progress('[~] Applying overall pitch change', 0.85, is_webui, progress)
486
  instrumentals_path = pitch_shift(instrumentals_path, pitch_change_all)
487
  backup_vocals_path = pitch_shift(backup_vocals_path, pitch_change_all)
488
 
489
- display_progress('[~] Combining AI Vocals and Instrumentals...', 0.9, is_webui, progress)
490
- combine_audio([ai_vocals_mixed_path, backup_vocals_path, instrumentals_path], ai_cover_path, main_gain, backup_gain, inst_gain, output_format)
 
 
 
 
491
 
 
492
  if not keep_files:
493
- display_progress('[~] Removing intermediate audio files...', 0.95, is_webui, progress)
494
  intermediate_files = [vocals_path, main_vocals_path, ai_vocals_mixed_path]
495
- # Add noise-reduced file path if denoise was applied
496
  if extra_denoise and ai_vocals_path.endswith('_nr.wav'):
497
- nr_path = ai_vocals_path
498
- intermediate_files.append(nr_path)
499
  if pitch_change_all != 0:
500
  intermediate_files += [instrumentals_path, backup_vocals_path]
 
501
  for file in intermediate_files:
502
  if file and os.path.exists(file) and file != ins_path:
503
- os.remove(file)
 
 
 
504
 
 
 
 
505
  return ai_cover_path
506
 
507
  except Exception as e:
 
508
  raise_exception(str(e), is_webui)
509
 
510
 
 
 
 
 
511
  @spaces.GPU(duration=120)
512
  def batch_process_files(
513
- input_files, voice_model, pitch_change, f0_method, index_rate, protect, output_format, progress=gr.Progress()
 
514
  ):
515
  """
516
  Process multiple audio files with the same RVC model settings.
517
- Returns a list of processed file paths.
518
  """
519
  if not input_files:
520
  raise gr.Error("No files provided for batch processing!")
@@ -522,14 +987,17 @@ def batch_process_files(
522
  if not voice_model:
523
  raise gr.Error("Please select a voice model!")
524
 
 
 
 
 
525
  results = []
526
  total_files = len(input_files)
527
  batch_output_dir = os.path.join(output_dir, 'batch_output')
528
  os.makedirs(batch_output_dir, exist_ok=True)
529
 
530
- display_progress(f'[~] Starting batch processing of {total_files} files...', 0, True, progress)
531
-
532
  # Load RVC model once for efficiency
 
533
  rvc_model_path, rvc_index_path = get_rvc_model(voice_model, True)
534
  cpt, version, net_g, tgt_sr, vc = get_vc(device, config.is_half, config, rvc_model_path)
535
 
@@ -548,20 +1016,27 @@ def batch_process_files(
548
  filename = os.path.basename(input_path)
549
  base_name = os.path.splitext(filename)[0]
550
 
551
- # Update progress
552
- progress_val = (i / total_files) * 100
553
- display_progress(f'[~] Processing ({i+1}/{total_files}): {filename}', progress_val, True, progress)
 
 
 
554
 
555
  # Load and convert audio
556
  audio, sr = librosa.load(input_path, sr=16000, mono=True)
557
 
558
- # Create temp wav file for processing
559
  temp_input = os.path.join(batch_output_dir, f'temp_{base_name}.wav')
560
  sf.write(temp_input, audio, 16000)
561
 
562
- # Output path
563
  output_path = os.path.join(batch_output_dir, f'{base_name}_{voice_model}_converted.{output_format}')
564
 
 
 
 
 
 
 
565
  # Run voice conversion
566
  rvc_infer(
567
  rvc_index_path, index_rate, temp_input,
@@ -580,32 +1055,56 @@ def batch_process_files(
580
  output_path = output_path.replace(f'.{output_format}', '.wav')
581
 
582
  results.append(output_path)
583
- print(f'[+] Processed: {filename}')
 
 
 
 
 
 
584
 
585
  except Exception as e:
586
- print(f'[-] Error processing {getattr(file_info, "name", "unknown")}: {str(e)}')
587
  continue
588
 
589
- # Cleanup
590
  del cpt
591
  gc.collect()
 
 
592
 
593
- display_progress(f'[+] Batch processing complete! {len(results)}/{total_files} files processed.', 100, True, progress)
594
 
595
  return results
596
 
597
 
 
 
 
 
598
  @spaces.GPU(duration=30)
599
- def generate_preview(audio_path, duration=10, voice_model=None, pitch_change=0, f0_method='rmvpe+',
600
- index_rate=0.5, protect=0.33, progress=gr.Progress()):
 
 
601
  """
602
  Generate a short preview clip of the voice conversion.
603
  Useful for testing settings before full conversion.
604
  """
 
 
 
 
 
 
 
 
 
 
605
  if not audio_path:
606
  raise gr.Error("Please provide an audio file for preview!")
607
 
608
- display_progress('[~] Loading audio and extracting preview segment...', 10, True, progress)
609
 
610
  # Load audio
611
  audio, sr = librosa.load(audio_path, sr=16000, mono=True)
@@ -622,10 +1121,11 @@ def generate_preview(audio_path, duration=10, voice_model=None, pitch_change=0,
622
  preview_input_path = os.path.join(preview_dir, 'preview_input.wav')
623
  sf.write(preview_input_path, preview_audio, 16000)
624
 
625
- display_progress('[~] Generating voice conversion preview...', 30, True, progress)
626
 
627
  # If no model specified, return original audio
628
  if not voice_model:
 
629
  return preview_input_path
630
 
631
  # Load model
@@ -634,6 +1134,8 @@ def generate_preview(audio_path, duration=10, voice_model=None, pitch_change=0,
634
 
635
  global hubert_model
636
 
 
 
637
  # Convert preview
638
  preview_output_path = os.path.join(preview_dir, 'preview_output.wav')
639
  rvc_infer(
@@ -645,45 +1147,63 @@ def generate_preview(audio_path, duration=10, voice_model=None, pitch_change=0,
645
  del cpt
646
  gc.collect()
647
 
648
- display_progress('[+] Preview generated successfully!', 100, True, progress)
649
 
650
  return preview_output_path
651
 
652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  if __name__ == '__main__':
654
- parser = argparse.ArgumentParser(description='Generate a AI cover song in the song_output/id directory.', add_help=True)
655
- parser.add_argument('-i', '--song-input', type=str, required=True, help='Link to a YouTube video or the filepath to a local mp3/wav file to create an AI cover of')
656
- parser.add_argument('-dir', '--rvc-dirname', type=str, required=True, help='Name of the folder in the rvc_models directory containing the RVC model file and optional index file to use')
657
- parser.add_argument('-p', '--pitch-change', type=int, required=True, help='Change the pitch of AI Vocals only. Generally, use 1 for male to female and -1 for vice-versa. (Octaves)')
658
- parser.add_argument('-k', '--keep-files', action=argparse.BooleanOptionalAction, help='Whether to keep all intermediate audio files generated in the song_output/id directory, e.g. Isolated Vocals/Instrumentals')
659
- parser.add_argument('-ir', '--index-rate', type=float, default=0.5, help='A decimal number e.g. 0.5, used to reduce/resolve the timbre leakage problem. If set to 1, more biased towards the timbre quality of the training dataset')
660
- parser.add_argument('-fr', '--filter-radius', type=int, default=3, help='A number between 0 and 7. If >=3: apply median filtering to the harvested pitch results. The value represents the filter radius and can reduce breathiness.')
661
- parser.add_argument('-rms', '--rms-mix-rate', type=float, default=0.25, help="A decimal number e.g. 0.25. Control how much to use the original vocal's loudness (0) or a fixed loudness (1).")
662
- parser.add_argument('-palgo', '--pitch-detection-algo', type=str, default='rmvpe', help='Best option is rmvpe (clarity in vocals), then mangio-crepe (smoother vocals).')
663
- parser.add_argument('-hop', '--crepe-hop-length', type=int, default=128, help='If pitch detection algo is mangio-crepe, controls how often it checks for pitch changes in milliseconds. The higher the value, the faster the conversion and less risk of voice cracks, but there is less pitch accuracy. Recommended: 128.')
664
- parser.add_argument('-pro', '--protect', type=float, default=0.33, help='A decimal number e.g. 0.33. Protect voiceless consonants and breath sounds to prevent artifacts such as tearing in electronic music. Set to 0.5 to disable. Decrease the value to increase protection, but it may reduce indexing accuracy.')
665
- parser.add_argument('-mv', '--main-vol', type=int, default=0, help='Volume change for AI main vocals in decibels. Use -3 to decrease by 3 decibels and 3 to increase by 3 decibels')
666
- parser.add_argument('-bv', '--backup-vol', type=int, default=0, help='Volume change for backup vocals in decibels')
667
- parser.add_argument('-iv', '--inst-vol', type=int, default=0, help='Volume change for instrumentals in decibels')
668
- parser.add_argument('-pall', '--pitch-change-all', type=int, default=0, help='Change the pitch/key of vocals and instrumentals. Changing this slightly reduces sound quality')
669
- parser.add_argument('-rsize', '--reverb-size', type=float, default=0.15, help='Reverb room size between 0 and 1')
670
- parser.add_argument('-rwet', '--reverb-wetness', type=float, default=0.2, help='Reverb wet level between 0 and 1')
671
- parser.add_argument('-rdry', '--reverb-dryness', type=float, default=0.8, help='Reverb dry level between 0 and 1')
672
- parser.add_argument('-rdamp', '--reverb-damping', type=float, default=0.7, help='Reverb damping between 0 and 1')
673
- parser.add_argument('-oformat', '--output-format', type=str, default='mp3', help='Output format of audio file. mp3 for smaller file size, wav for best quality')
 
 
 
674
  args = parser.parse_args()
675
 
676
  rvc_dirname = args.rvc_dirname
677
  if not os.path.exists(os.path.join(rvc_models_dir, rvc_dirname)):
678
  raise Exception(f'The folder {os.path.join(rvc_models_dir, rvc_dirname)} does not exist.')
679
 
680
- cover_path = song_cover_pipeline(args.song_input, rvc_dirname, args.pitch_change, args.keep_files,
681
- main_gain=args.main_vol, backup_gain=args.backup_vol, inst_gain=args.inst_vol,
682
- index_rate=args.index_rate, filter_radius=args.filter_radius,
683
- rms_mix_rate=args.rms_mix_rate, f0_method=args.pitch_detection_algo,
684
- crepe_hop_length=args.crepe_hop_length, protect=args.protect,
685
- pitch_change_all=args.pitch_change_all,
686
- reverb_rm_size=args.reverb_size, reverb_wet=args.reverb_wetness,
687
- reverb_dry=args.reverb_dryness, reverb_damping=args.reverb_damping,
688
- output_format=args.output_format)
689
- print(f'[+] Cover generated at {cover_path}')
 
 
 
1
+ """
2
+ Hyper-RVC Main Processing Module
3
+ Enhanced with Advanced Progress Tracking System
4
+ """
5
  import spaces
6
  import torch
7
  import argparse
 
11
  import os
12
  import shlex
13
  import subprocess
14
+ import time
15
  from contextlib import suppress
16
+ from dataclasses import dataclass, field
17
+ from datetime import timedelta
18
+ from typing import Optional, Callable, List, Tuple, Any
19
  from urllib.parse import urlparse, parse_qs
 
20
  import shutil
21
 
22
  import gradio as gr
 
44
  output_dir = os.path.join(BASE_DIR, 'song_output')
45
 
46
 
47
+ # =============================================================================
48
+ # ENHANCED PROGRESS TRACKING SYSTEM
49
+ # =============================================================================
50
+
51
+ @dataclass
52
+ class ProgressStep:
53
+ """Represents a single step in the processing pipeline."""
54
+ name: str
55
+ weight: float = 1.0 # Relative weight for progress calculation
56
+ status: str = "pending" # pending, in_progress, completed, error
57
+ start_time: Optional[float] = None
58
+ end_time: Optional[float] = None
59
+ detail: str = ""
60
+
61
+
62
+ class EnhancedProgressTracker:
63
+ """
64
+ Advanced progress tracker with:
65
+ - Step-by-step progress visualization
66
+ - Time estimation (ETA)
67
+ - Weighted progress calculation
68
+ - Detailed status messages
69
+ - Smooth progress updates
70
+ """
71
+
72
+ def __init__(self, progress: gr.Progress = None, is_webui: bool = True):
73
+ self.progress = progress
74
+ self.is_webui = is_webui
75
+ self.steps: List[ProgressStep] = []
76
+ self.current_step_index: int = 0
77
+ self.total_weight: float = 0.0
78
+ self.completed_weight: float = 0.0
79
+ self.start_time: float = time.time()
80
+ self.last_update_time: float = 0.0
81
+ self.min_update_interval: float = 0.1 # Minimum seconds between UI updates
82
+
83
+ def add_step(self, name: str, weight: float = 1.0) -> 'EnhancedProgressTracker':
84
+ """Add a processing step."""
85
+ step = ProgressStep(name=name, weight=weight)
86
+ self.steps.append(step)
87
+ self.total_weight += weight
88
+ return self
89
+
90
+ def set_steps(self, steps: List[Tuple[str, float]]) -> 'EnhancedProgressTracker':
91
+ """Set multiple steps at once. Format: [(name, weight), ...]"""
92
+ self.steps = []
93
+ self.total_weight = 0.0
94
+ for name, weight in steps:
95
+ self.add_step(name, weight)
96
+ return self
97
+
98
+ def start(self, message: str = None):
99
+ """Initialize and start tracking."""
100
+ self.start_time = time.time()
101
+ self.current_step_index = 0
102
+ self.completed_weight = 0.0
103
+
104
+ if message:
105
+ self._update_progress(0, message)
106
+
107
+ if self.steps:
108
+ self._start_step(0)
109
+
110
+ def _start_step(self, index: int):
111
+ """Mark a step as started."""
112
+ if index < len(self.steps):
113
+ self.steps[index].status = "in_progress"
114
+ self.steps[index].start_time = time.time()
115
+ self.current_step_index = index
116
+
117
+ def _complete_step(self, index: int):
118
+ """Mark a step as completed."""
119
+ if index < len(self.steps):
120
+ self.steps[index].status = "completed"
121
+ self.steps[index].end_time = time.time()
122
+ self.completed_weight += self.steps[index].weight
123
+
124
+ def _calculate_progress(self) -> float:
125
+ """Calculate overall progress (0-100)."""
126
+ if self.total_weight == 0:
127
+ return 0.0
128
+
129
+ # Completed weight contributes fully
130
+ progress = (self.completed_weight / self.total_weight) * 100
131
+
132
+ # Current step contributes partially based on sub-progress
133
+ if self.current_step_index < len(self.steps):
134
+ current_step = self.steps[self.current_step_index]
135
+ if current_step.status == "in_progress":
136
+ # Assume current step is 50% done for smooth progression
137
+ step_contribution = (current_step.weight * 0.5 / self.total_weight) * 100
138
+ progress += step_contribution
139
+
140
+ return min(progress, 99.9) # Cap at 99.9 until fully complete
141
+
142
+ def _estimate_remaining_time(self) -> str:
143
+ """Estimate time remaining based on progress."""
144
+ elapsed = time.time() - self.start_time
145
+ current_progress = self._calculate_progress()
146
+
147
+ if current_progress > 0.1:
148
+ total_estimated = (elapsed / current_progress) * 100
149
+ remaining = max(0, total_estimated - elapsed)
150
+ return str(timedelta(seconds=int(remaining)))
151
+ return "Calculating..."
152
+
153
+ def _get_status_message(self, custom_message: str = None) -> str:
154
+ """Generate detailed status message."""
155
+ if custom_message:
156
+ return custom_message
157
+
158
+ if self.current_step_index < len(self.steps):
159
+ step = self.steps[self.current_step_index]
160
+ eta = self._estimate_remaining_time()
161
+ return f"[{self.current_step_index + 1}/{len(self.steps)}] {step.name} | ETA: {eta}"
162
+
163
+ return "Processing..."
164
+
165
+ def _update_progress(self, percent: float, message: str = None):
166
+ """Update the Gradio progress bar."""
167
+ current_time = time.time()
168
+
169
+ # Throttle updates to prevent UI flooding
170
+ if current_time - self.last_update_time < self.min_update_interval and percent < 100:
171
+ return
172
+
173
+ self.last_update_time = current_time
174
+
175
+ if self.is_webui and self.progress:
176
+ status_msg = self._get_status_message(message)
177
+ self.progress(percent / 100.0, desc=status_msg)
178
+ elif not self.is_webui:
179
+ print(f"[{percent:.1f}%] {self._get_status_message(message)}")
180
+
181
+ def update_step(self, step_name: str = None, detail: str = "", sub_progress: float = None):
182
+ """
183
+ Update progress within current or specified step.
184
+
185
+ Args:
186
+ step_name: Name of step to update (uses current if None)
187
+ detail: Additional detail about current operation
188
+ sub_progress: Sub-progress within current step (0-1)
189
+ """
190
+ # Find step index
191
+ if step_name:
192
+ for i, step in enumerate(self.steps):
193
+ if step.name == step_name:
194
+ if step.status == "pending":
195
+ self._complete_step(self.current_step_index)
196
+ self._start_step(i)
197
+ break
198
+
199
+ # Update detail
200
+ if self.current_step_index < len(self.steps):
201
+ self.steps[self.current_step_index].detail = detail
202
+
203
+ # Calculate and display progress
204
+ overall_progress = self._calculate_progress()
205
+
206
+ # Adjust for sub-progress if provided
207
+ if sub_progress is not None and self.current_step_index < len(self.steps):
208
+ step = self.steps[self.current_step_index]
209
+ step_progress = (step.weight * sub_progress / self.total_weight) * 100
210
+ overall_progress = ((self.completed_weight + step.weight * sub_progress) / self.total_weight) * 100
211
+
212
+ message = f"{detail}" if detail else None
213
+ self._update_progress(min(overall_progress, 99.9), message)
214
+
215
+ def next_step(self, detail: str = ""):
216
+ """Complete current step and move to next."""
217
+ if self.current_step_index < len(self.steps):
218
+ self._complete_step(self.current_step_index)
219
+
220
+ next_idx = self.current_step_index + 1
221
+ if next_idx < len(self.steps):
222
+ self._start_step(next_idx)
223
+
224
+ # Update progress for new step
225
+ overall_progress = self._calculate_progress()
226
+ message = f"{detail}" if detail else None
227
+ self._update_progress(overall_progress, message)
228
+
229
+ def complete(self, final_message: str = "Complete!"):
230
+ """Mark all processing as complete."""
231
+ # Complete any remaining in-progress step
232
+ if self.current_step_index < len(self.steps):
233
+ self._complete_step(self.current_step_index)
234
+
235
+ elapsed = time.time() - self.start_time
236
+ completion_msg = f"{final_message} | Total time: {timedelta(seconds=int(elapsed))}"
237
+ self._update_progress(100, completion_msg)
238
+
239
+ def error(self, error_message: str):
240
+ """Mark current step as errored."""
241
+ if self.current_step_index < len(self.steps):
242
+ self.steps[self.current_step_index].status = "error"
243
+ self.steps[self.current_step_index].detail = error_message
244
+
245
+ self._update_progress(self._calculate_progress(), f"ERROR: {error_message}")
246
+
247
+ def get_progress_info(self) -> dict:
248
+ """Get current progress information as dictionary."""
249
+ return {
250
+ "current_step": self.current_step_index,
251
+ "total_steps": len(self.steps),
252
+ "progress_percent": self._calculate_progress(),
253
+ "eta": self._estimate_remaining_time(),
254
+ "elapsed": str(timedelta(seconds=int(time.time() - self.start_time))),
255
+ "steps": [
256
+ {"name": s.name, "status": s.status, "detail": s.detail}
257
+ for s in self.steps
258
+ ]
259
+ }
260
+
261
+
262
+ def create_progress_tracker(progress: gr.Progress = None, is_webui: bool = True) -> EnhancedProgressTracker:
263
+ """
264
+ Factory function to create a pre-configured progress tracker
265
+ for the song cover pipeline.
266
+ """
267
+ tracker = EnhancedProgressTracker(progress=progress, is_webui=is_webui)
268
+
269
+ # Define pipeline steps with weights reflecting relative processing time
270
+ tracker.set_steps([
271
+ ("Initializing Pipeline", 1),
272
+ ("Downloading/Loading Audio", 3),
273
+ ("Converting to Stereo", 1),
274
+ ("Separating Vocals from Instrumental", 15),
275
+ ("Separating Main Vocals from Backup", 12),
276
+ ("Applying DeReverb to Vocals", 10),
277
+ ("Extracting Voiceless Track", 8),
278
+ ("Converting Voice with RVC", 25),
279
+ ("Applying Audio Effects", 8),
280
+ ("Applying Pitch Shift", 5),
281
+ ("Combining Audio Tracks", 7),
282
+ ("Finalizing Output", 5),
283
+ ])
284
+
285
+ return tracker
286
+
287
+
288
+ # =============================================================================
289
+ # UTILITY FUNCTIONS
290
+ # =============================================================================
291
+
292
  def clean_old_folders(base_path: str, max_age_seconds: int = 10800):
293
+ """Clean up old output folders to save disk space."""
294
  if not os.path.isdir(base_path):
295
+ logging.warning(f"Error: {base_path} is not a valid directory.")
296
  return
297
 
298
  now = time.time()
299
+ cleaned_count = 0
300
 
301
  for folder_name in os.listdir(base_path):
302
  folder_path = os.path.join(base_path, folder_name)
303
  if os.path.isdir(folder_path):
304
  last_modified = os.path.getmtime(folder_path)
305
  if now - last_modified > max_age_seconds:
306
+ try:
307
+ shutil.rmtree(folder_path)
308
+ cleaned_count += 1
309
+ except Exception as e:
310
+ logging.warning(f"Failed to delete {folder_path}: {e}")
311
+
312
+ if cleaned_count > 0:
313
+ logging.info(f"Cleaned up {cleaned_count} old folders from {base_path}")
314
 
315
 
316
  def get_youtube_video_id(url, ignore_playlist=True):
317
  """
318
+ Extract video ID from various YouTube URL formats.
319
+
320
  Examples:
321
+ - http://youtu.be/SA2iWivDJiE
322
+ - http://www.youtube.com/watch?v=_oPAwA_Udwc&feature=feedu
323
+ - http://www.youtube.com/embed/SA2iWivDJiE
 
324
  """
325
  if "m.youtube.com" in url:
326
  url = url.replace("m.youtube.com", "www.youtube.com")
327
+
328
  query = urlparse(url)
329
+
330
  if query.hostname == 'youtu.be':
331
  if query.path[1:] == 'watch':
332
  return query.query[2:]
 
334
 
335
  if query.hostname in {'www.youtube.com', 'youtube.com', 'music.youtube.com'}:
336
  if not ignore_playlist:
 
337
  with suppress(KeyError):
338
  return parse_qs(query.query)['list'][0]
339
  if query.path == '/watch':
 
343
  if query.path[:7] == '/embed/':
344
  return query.path.split('/')[2]
345
  if query.path[:3] == '/v/':
346
+ return query.path.split('/')[1]
347
 
 
348
  return None
349
 
350
 
351
+ def yt_download(link, progress_callback=None):
352
+ """
353
+ Download audio from YouTube URL with progress callback support.
354
+
355
+ Args:
356
+ link: YouTube video URL
357
+ progress_callback: Optional function(status, percent) for progress updates
358
+
359
+ Returns:
360
+ Path to downloaded audio file
361
+ """
362
  if not link.strip():
363
+ raise ValueError("You need to provide a download link.")
364
+
365
+ def progress_hook(d):
366
+ if progress_callback and d['status'] == 'downloading':
367
+ if 'downloaded_bytes' in d and 'total_bytes' in d and d['total_bytes'] > 0:
368
+ percent = (d['downloaded_bytes'] / d['total_bytes']) * 100
369
+ progress_callback(f"Downloading: {percent:.1f}%", min(percent, 90))
370
+
371
  ydl_opts = {
372
+ 'format': 'bestaudio/best',
373
  'outtmpl': '%(title)s',
374
  'nocheckcertificate': True,
375
  'ignoreerrors': True,
376
  'no_warnings': True,
377
  'quiet': True,
378
  'extractaudio': True,
379
+ 'postprocessors': [{
380
+ 'key': 'FFmpegExtractAudio',
381
+ 'preferredcodec': 'mp3',
382
+ 'preferredquality': '192',
383
+ }],
384
+ 'progress_hooks': [progress_hook] if progress_callback else [],
385
  }
386
+
387
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
388
  result = ydl.extract_info(link, download=True)
389
+ if result is None:
390
+ raise Exception("Failed to extract video information")
391
  download_path = ydl.prepare_filename(result, outtmpl='%(title)s.mp3')
392
 
393
  return download_path
394
 
395
 
396
  def raise_exception(error_msg, is_webui):
397
+ """Raise appropriate exception based on context."""
398
  if is_webui:
399
  raise gr.Error(error_msg)
400
  else:
 
402
 
403
 
404
  def get_rvc_model(voice_model, is_webui):
405
+ """
406
+ Locate RVC model files (.pth and .index) in model directory.
407
+
408
+ Returns:
409
+ Tuple of (model_path, index_path)
410
+ """
411
  rvc_model_filename, rvc_index_filename = None, None
412
  model_dir = os.path.join(rvc_models_dir, voice_model)
413
+
414
+ if not os.path.isdir(model_dir):
415
+ error_msg = f'Model directory does not exist: {model_dir}'
416
+ raise_exception(error_msg, is_webui)
417
+
418
  for file in os.listdir(model_dir):
419
+ file_path = os.path.join(model_dir, file)
420
+
421
+ # Handle nested directories
422
+ if os.path.isdir(file_path):
423
+ for ff in os.listdir(file_path):
424
  ext = os.path.splitext(ff)[1]
425
  if ext == '.pth':
426
  rvc_model_filename = ff
427
+ elif ext == '.index':
428
  rvc_index_filename = ff
429
+ else:
430
+ ext = os.path.splitext(file)[1]
431
+ if ext == '.pth':
432
+ rvc_model_filename = file
433
+ elif ext == '.index':
434
+ rvc_index_filename = file
435
 
436
  if rvc_model_filename is None:
437
+ error_msg = f'No model file (.pth) found in {model_dir}.'
438
  raise_exception(error_msg, is_webui)
439
 
440
+ model_path = os.path.join(model_dir, rvc_model_filename)
441
+ index_path = os.path.join(model_dir, rvc_index_filename) if rvc_index_filename else ''
442
+
443
+ return model_path, index_path
444
 
445
 
446
  def get_audio_paths(song_dir):
447
+ """Extract various audio paths from processed song directory."""
448
  orig_song_path = None
449
  instrumentals_path = None
450
  main_vocals_dereverb_path = None
 
455
  instrumentals_path = os.path.join(song_dir, file)
456
  orig_song_path = instrumentals_path.replace('_Instrumental', '')
457
 
458
+ elif file.endswith('_Vocals_Main_DeReVerb.wav'):
459
  main_vocals_dereverb_path = os.path.join(song_dir, file)
460
 
461
  elif file.endswith('_Vocals_Backup.wav'):
462
  backup_vocals_path = os.path.join(song_dir, file)
463
 
 
464
  return orig_song_path, instrumentals_path, main_vocals_dereverb_path, backup_vocals_path
465
 
466
 
467
  def get_audio_with_suffix(song_dir, suffix="_mysuffix.wav"):
468
+ """Find audio file with specific suffix in directory."""
 
469
  for file in os.listdir(song_dir):
470
  if file.endswith(suffix):
471
+ return os.path.join(song_dir, file)
472
+ return None
 
 
473
 
474
 
475
  def convert_to_stereo(audio_path):
476
+ """
477
+ Convert mono audio to stereo if needed.
478
+ FIXED: Added file existence validation and better error handling
479
+ """
480
+ # Validate file exists first
481
+ if not audio_path or not os.path.exists(audio_path):
482
+ raise FileNotFoundError(f"Audio file not found for stereo conversion: {audio_path}")
483
+
484
+ try:
485
+ # Try loading with soundfile first (more reliable)
486
+ try:
487
+ data, sr = sf.read(audio_path)
488
+ if len(data.shape) == 1 or data.shape[1] == 1:
489
+ # Mono file - need conversion
490
+ stereo_path = f'{os.path.splitext(audio_path)[0]}_stereo.wav'
491
+ command = shlex.split(f'ffmpeg -y -loglevel error -i "{audio_path}" -ac 2 -f wav "{stereo_path}"')
492
+ result = subprocess.run(command, capture_output=True, text=True, timeout=60)
493
+ if result.returncode != 0:
494
+ logging.error(f"FFmpeg stereo conversion failed: {result.stderr}")
495
+ return audio_path # Return original if conversion fails
496
+ return stereo_path
497
+ else:
498
+ return audio_path # Already stereo
499
+ except Exception as sf_error:
500
+ logging.debug(f"soundfile failed, trying librosa: {sf_error}")
501
+
502
+ # Fallback to librosa
503
+ wave, sr_librosa = librosa.load(audio_path, mono=False, sr=44100)
504
+
505
+ # Check if mono
506
+ if type(wave[0]) != np.ndarray:
507
+ stereo_path = f'{os.path.splitext(audio_path)[0]}_stereo.wav'
508
+ command = shlex.split(f'ffmpeg -y -loglevel error -i "{audio_path}" -ac 2 -f wav "{stereo_path}"')
509
+ result = subprocess.run(command, capture_output=True, text=True, timeout=60)
510
+ if result.returncode != 0:
511
+ logging.error(f"FFmpeg stereo conversion failed: {result.stderr}")
512
+ return audio_path
513
+ return stereo_path
514
+ else:
515
+ return audio_path
516
+
517
+ except FileNotFoundError:
518
+ raise # Re-raise FileNotFoundError with original message
519
+ except Exception as e:
520
+ logging.warning(f"Stereo conversion warning for {audio_path}: {e}")
521
+ # Return original path if conversion fails completely
522
  return audio_path
523
 
524
 
525
  def pitch_shift(audio_path, pitch_change):
526
+ """Apply pitch shift to audio file."""
527
  output_path = f'{os.path.splitext(audio_path)[0]}_p{pitch_change}.wav'
528
  if not os.path.exists(output_path):
529
  y, sr = sf.read(audio_path)
 
536
 
537
 
538
  def get_hash(filepath):
539
+ """Generate short hash of file for identification."""
540
  with open(filepath, 'rb') as f:
541
  file_hash = hashlib.blake2b()
542
  while chunk := f.read(8192):
543
  file_hash.update(chunk)
 
544
  return file_hash.hexdigest()[:11]
545
 
546
 
547
+ # =============================================================================
548
+ # AUDIO PROCESSING FUNCTIONS
549
+ # =============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
 
551
  def add_audio_effects(audio_path, reverb_rm_size, reverb_wet, reverb_dry, reverb_damping):
552
+ """Apply professional audio effects (HPF, Compressor, Reverb)."""
553
  output_path = f'{os.path.splitext(audio_path)[0]}_mixed.wav'
554
 
 
555
  board = Pedalboard(
556
  [
557
+ HighpassFilter(cutoff_frequency=80),
558
  Compressor(ratio=4, threshold_db=-15),
559
  Reverb(room_size=reverb_rm_size, dry_level=reverb_dry, wet_level=reverb_wet, damping=reverb_damping)
560
  ]
 
562
 
563
  with AudioFile(audio_path) as f:
564
  with AudioFile(output_path, 'w', f.samplerate, f.num_channels) as o:
 
565
  while f.tell() < f.frames:
566
  chunk = f.read(int(f.samplerate))
567
  effected = board(chunk, f.samplerate, reset=False)
 
571
 
572
 
573
  def combine_audio(audio_paths, output_path, main_gain, backup_gain, inst_gain, output_format):
574
+ """Combine multiple audio tracks with gain adjustments."""
575
+ try:
576
+ main_vocal_audio = AudioSegment.from_wav(audio_paths[0]) - 4 + main_gain
577
+ backup_vocal_audio = AudioSegment.from_wav(audio_paths[1]) - 6 + backup_gain
578
+ instrumental_audio = AudioSegment.from_wav(audio_paths[2]) - 7 + inst_gain
579
+
580
+ combined = main_vocal_audio.overlay(backup_vocal_audio).overlay(instrumental_audio)
581
+ combined.export(output_path, format=output_format)
582
+ except Exception as e:
583
+ logging.error(f"Audio combining error: {e}")
584
+ raise
585
+
586
+
587
+ def apply_noisereduce(audio_list, type_output="wav"):
588
+ """Apply noise reduction to audio files."""
589
+ result = []
590
+ for audio_path in audio_list:
591
+ out_path = f"{os.path.splitext(audio_path)[0]}_nr.{type_output}"
592
+
593
+ try:
594
+ audio = AudioSegment.from_file(audio_path)
595
+ samples = np.array(audio.get_array_of_samples())
596
+
597
+ reduced_noise = nr.reduce_noise(
598
+ samples,
599
+ sr=audio.frame_rate,
600
+ prop_decrease=0.6,
601
+ n_std_thresh_stationary=1.5
602
+ )
603
+
604
+ reduced_audio = AudioSegment(
605
+ reduced_noise.tobytes(),
606
+ frame_rate=audio.frame_rate,
607
+ sample_width=audio.sample_width,
608
+ channels=audio.channels
609
+ )
610
+
611
+ reduced_audio.export(out_path, format=type_output)
612
+ result.append(out_path)
613
+
614
+ except Exception as e:
615
+ logging.warning(f"Noise reduction error for {audio_path}: {e}")
616
+ result.append(audio_path)
617
+
618
+ return result
619
+
620
+
621
+ # =============================================================================
622
+ # MAIN PROCESSING PIPELINE
623
+ # =============================================================================
624
+
625
+ def preprocess_song(mdx_model_params, song_output_dir, orig_song_path, keep_orig=False,
626
+ tracker: EnhancedProgressTracker = None):
627
+ """
628
+ Preprocess song: separate vocals/instrumentals, apply dereverb.
629
+
630
+ Returns:
631
+ Tuple of all processed audio paths
632
+ """
633
+ # Step: Vocal Separation
634
+ if tracker:
635
+ tracker.update_step(detail="Running MDX vocal separation...")
636
+
637
+ vocals_path, instrumentals_path = run_mdx(
638
+ mdx_model_params,
639
+ song_output_dir,
640
+ os.path.join(mdxnet_models_dir, 'UVR-MDX-NET-Voc_FT.onnx'),
641
+ orig_song_path,
642
+ denoise=True,
643
+ keep_orig=keep_orig
644
+ )
645
+
646
+ if tracker:
647
+ tracker.next_step(detail="Separating main from backup vocals...")
648
+
649
+ # Step: Main/Backup Vocal Separation
650
+ backup_vocals_path, main_vocals_path = run_mdx(
651
+ mdx_model_params,
652
+ song_output_dir,
653
+ os.path.join(mdxnet_models_dir, 'UVR_MDXNET_KARA_2.onnx'),
654
+ vocals_path,
655
+ suffix='Backup',
656
+ invert_suffix='Main',
657
+ denoise=True
658
+ )
659
+
660
+ if tracker:
661
+ tracker.next_step(detail="Applying dereverb processing...")
662
+
663
+ # Step: Dereverb
664
+ _, main_vocals_dereverb_path = run_mdx(
665
+ mdx_model_params,
666
+ song_output_dir,
667
+ os.path.join(mdxnet_models_dir, 'Reverb_HQ_By_FoxJoy.onnx'),
668
+ main_vocals_path,
669
+ invert_suffix='DeReverb',
670
+ exclude_main=True,
671
+ denoise=True
672
+ )
673
+
674
+ return orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path
675
+
676
+
677
+ @spaces.GPU(duration=65)
678
+ def voice_change(voice_model, vocals_path, output_path, pitch_change, f0_method,
679
+ index_rate, filter_radius, rms_mix_rate, protect, crepe_hop_length,
680
+ is_webui, steps, tracker: EnhancedProgressTracker = None):
681
+ """
682
+ Perform RVC voice conversion on vocals.
683
+ """
684
+ if tracker:
685
+ tracker.update_step(detail="Loading RVC model...")
686
+
687
+ rvc_model_path, rvc_index_path = get_rvc_model(voice_model, is_webui)
688
+
689
+ cpt, version, net_g, tgt_sr, vc = get_vc(device, config.is_half, config, rvc_model_path)
690
+
691
+ if tracker:
692
+ tracker.update_step(detail="Running voice conversion (this may take a while)...")
693
+
694
+ global hubert_model
695
+ rvc_infer(
696
+ rvc_index_path, index_rate, vocals_path, output_path, pitch_change,
697
+ f0_method, cpt, version, net_g, filter_radius, tgt_sr, rms_mix_rate,
698
+ protect, crepe_hop_length, vc, hubert_model, steps
699
+ )
700
+
701
+ del cpt
702
+ gc.collect()
703
+ torch.cuda.empty_cache() if torch.cuda.is_available() else None
704
 
705
 
706
  @spaces.GPU(duration=65)
707
  def process_song(
708
+ song_dir, mdx_model_params, song_id, is_webui, input_type,
709
+ keep_files, pitch_change, pitch_change_all, voice_model, index_rate,
710
+ filter_radius, rms_mix_rate, protect, f0_method, crepe_hop_length,
711
+ output_format, keep_orig, orig_song_path, steps,
712
+ tracker: EnhancedProgressTracker = None
713
  ):
714
+ """
715
+ Process song through the full pipeline.
716
+ """
717
  if not os.path.exists(song_dir):
718
  os.makedirs(song_dir)
719
+ orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path = preprocess_song(
720
+ mdx_model_params, song_dir, orig_song_path, keep_orig, tracker
721
+ )
722
  else:
723
  vocals_path, main_vocals_path = None, None
724
  paths = get_audio_paths(song_dir)
725
 
 
726
  if any(path is None for path in paths):
727
+ orig_song_path, vocals_path, instrumentals_path, main_vocals_path, backup_vocals_path, main_vocals_dereverb_path = preprocess_song(
728
+ mdx_model_params, song_dir, orig_song_path, keep_orig, tracker
729
+ )
730
  else:
731
  orig_song_path, instrumentals_path, main_vocals_dereverb_path, backup_vocals_path = paths
732
 
733
+ # Voiceless track extraction
734
  ins_path = get_audio_with_suffix(song_dir, "_Voiceless.wav")
735
  if not ins_path:
736
+ if tracker:
737
+ tracker.next_step(detail="Extracting voiceless instrumental track...")
738
+
739
  instrumentals_path, _ = run_mdx(
740
  mdx_model_params,
741
  song_dir,
 
751
  else:
752
  instrumentals_path = ins_path
753
 
754
+ # Prepare output paths
755
+ pitch_change_total = pitch_change * 12 + pitch_change_all
756
+ ai_vocals_path = os.path.join(
757
+ song_dir,
758
+ f'{os.path.splitext(os.path.basename(orig_song_path))[0]}_{voice_model}_p{pitch_change_total}_i{index_rate}_fr{filter_radius}_rms{rms_mix_rate}_pro{protect}_{f0_method}{"" if f0_method != "mangio-crepe" else f"_{crepe_hop_length}"}_s{steps}.wav'
759
+ )
760
+ ai_cover_path = os.path.join(
761
+ song_dir,
762
+ f'{os.path.splitext(os.path.basename(orig_song_path))[0]} ({voice_model} Ver).{output_format}'
763
+ )
764
 
765
+ # RVC Voice Conversion
766
  if not os.path.exists(ai_vocals_path):
767
+ if tracker:
768
+ tracker.next_step()
769
+ voice_change(
770
+ voice_model, main_vocals_dereverb_path, ai_vocals_path, pitch_change_total,
771
+ f0_method, index_rate, filter_radius, rms_mix_rate, protect,
772
+ crepe_hop_length, is_webui, steps, tracker
773
+ )
774
 
775
  return ai_vocals_path, ai_cover_path, instrumentals_path, backup_vocals_path, vocals_path, main_vocals_path, ins_path
776
 
777
 
778
+ @spaces.GPU(duration=140)
779
+ def song_cover_pipeline(
780
+ song_input, voice_model, pitch_change, keep_files,
781
+ is_webui=0, main_gain=0, backup_gain=0, inst_gain=0, index_rate=0.5,
782
+ filter_radius=3, rms_mix_rate=0.25, f0_method='rmvpe', crepe_hop_length=128,
783
+ protect=0.33, pitch_change_all=0, reverb_rm_size=0.15, reverb_wet=0.2,
784
+ reverb_dry=0.8, reverb_damping=0.7, output_format='mp3', extra_denoise=False,
785
+ steps=1, progress=gr.Progress()
786
+ ):
787
+ """
788
+ Main pipeline for generating AI song covers with enhanced progress tracking.
789
+ """
790
+ # Initialize enhanced progress tracker
791
+ tracker = create_progress_tracker(progress=progress, is_webui=bool(is_webui))
792
+
793
+ # Cleanup old folders
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794
  if not keep_files or IS_ZERO_GPU:
795
  clean_old_folders("./song_output", 14400)
796
 
 
798
  clean_old_folders("./rvc_models", 10800)
799
 
800
  try:
801
+ # Input validation - with detailed error messages
802
  if not voice_model:
803
+ raise_exception('Please select a voice model first!', is_webui)
804
+
805
+ # Validate voice model exists
806
+ model_dir = os.path.join(rvc_models_dir, voice_model)
807
+ if not os.path.isdir(model_dir):
808
+ raise_exception(f'Voice model not found: "{voice_model}". Please download or select a valid model.', is_webui)
809
 
 
810
  if song_input is None or (isinstance(song_input, str) and len(song_input.strip()) == 0):
811
+ raise_exception('Please provide an audio file (select from folder, upload, or paste URL).', is_webui)
812
 
813
+ # Clean up input path
814
  if isinstance(song_input, str):
815
  song_input = song_input.strip().strip('\"').strip("'")
816
 
817
+ # CRITICAL: Validate file exists BEFORE processing
818
+ if not urlparse(song_input).scheme: # Not a URL
819
+ if not os.path.exists(song_input):
820
+ raise_exception(
821
+ f'Audio file not found: "{song_input}"\n\n'
822
+ 'The file may have been deleted or moved. Please:\n'
823
+ '1. Re-select the audio from the folder dropdown\n'
824
+ '2. Re-upload the file\n'
825
+ '3. Check that the file path is correct',
826
+ is_webui
827
+ )
828
+
829
+ # Verify file is readable and has content
830
+ try:
831
+ file_size = os.path.getsize(song_input)
832
+ if file_size == 0:
833
+ raise_exception(f'Audio file is empty (0 bytes): {song_input}', is_webui)
834
+ logging.info(f"Audio file validated: {song_input} ({file_size/1024/1024:.2f} MB)")
835
+ except OSError as e:
836
+ raise_exception(f'Cannot read audio file: {song_input}\nError: {e}', is_webui)
837
+
838
+ logging.info(f"Starting pipeline - Input: {song_input}, Model: {voice_model}")
839
+
840
+ # Start progress tracking
841
+ tracker.start("Initializing Hyper-RVC pipeline...")
842
+ tracker.next_step(detail="Validating inputs...")
843
 
844
+ # Load MDX model parameters
845
  with open(os.path.join(mdxnet_models_dir, 'model_data.json')) as infile:
846
  mdx_model_params = json.load(infile)
847
 
848
+ # Determine input type
849
  if urlparse(song_input).scheme == 'https':
850
  input_type = 'yt'
851
  song_id = get_youtube_video_id(song_input)
852
  if song_id is None:
853
+ raise_exception('Invalid YouTube URL.', is_webui)
 
 
 
854
  else:
855
  input_type = 'local'
856
+ song_input = song_input.strip('"')
857
  if os.path.exists(song_input):
858
  song_id = get_hash(song_input)
859
  else:
860
+ error_msg = f'File not found: {song_input}'
 
861
  raise_exception(error_msg, is_webui)
862
+ song_id = None
863
 
864
  song_dir = os.path.join(output_dir, song_id)
865
 
866
+ # Download/Load audio
867
+ if input_type == 'yt':
868
+ def yt_progress(status, percent):
869
+ tracker.update_step(detail=status)
870
+
871
+ tracker.next_step(detail="Downloading from YouTube...")
872
+ orig_song_path = yt_download(song_input, progress_callback=yt_progress)
873
+ keep_orig = False
874
+ else:
875
+ orig_song_path = song_input
876
+ keep_orig = True
877
+ tracker.next_step(detail="Loading local audio file...")
878
+
879
+ # Convert to stereo
880
+ tracker.next_step(detail="Converting to stereo if needed...")
881
  orig_song_path = convert_to_stereo(orig_song_path)
882
 
883
+ start_time = time.time()
884
 
885
+ # Run main processing pipeline
886
  (
887
  ai_vocals_path,
888
  ai_cover_path,
 
893
  ins_path
894
  ) = process_song(
895
  song_dir,
 
896
  mdx_model_params,
897
  song_id,
898
  is_webui,
899
  input_type,
 
900
  keep_files,
901
  pitch_change,
902
  pitch_change_all,
 
911
  keep_orig,
912
  orig_song_path,
913
  steps,
914
+ tracker
915
  )
916
 
917
+ end_time = time.time()
918
+ logging.info(f"Pipeline execution time: {end_time - start_time:.2f}s")
919
 
920
+ # Apply audio effects
921
+ tracker.next_step(detail="Applying reverb and effects...")
922
 
923
  if extra_denoise:
924
+ tracker.update_step(detail="Applying noise reduction...")
925
  ai_vocals_path = apply_noisereduce([ai_vocals_path])[0]
926
 
927
+ ai_vocals_mixed_path = add_audio_effects(
928
+ ai_vocals_path, reverb_rm_size, reverb_wet, reverb_dry, reverb_damping
929
+ )
930
 
931
+ # Pitch shift for instrumentals if needed
932
  if pitch_change_all != 0:
933
+ tracker.next_step(detail="Applying overall pitch change...")
934
  instrumentals_path = pitch_shift(instrumentals_path, pitch_change_all)
935
  backup_vocals_path = pitch_shift(backup_vocals_path, pitch_change_all)
936
 
937
+ # Combine tracks
938
+ tracker.next_step(detail="Mixing final audio tracks...")
939
+ combine_audio(
940
+ [ai_vocals_mixed_path, backup_vocals_path, instrumentals_path],
941
+ ai_cover_path, main_gain, backup_gain, inst_gain, output_format
942
+ )
943
 
944
+ # Cleanup intermediate files
945
  if not keep_files:
946
+ tracker.next_step(detail="Cleaning up temporary files...")
947
  intermediate_files = [vocals_path, main_vocals_path, ai_vocals_mixed_path]
948
+
949
  if extra_denoise and ai_vocals_path.endswith('_nr.wav'):
950
+ intermediate_files.append(ai_vocals_path)
 
951
  if pitch_change_all != 0:
952
  intermediate_files += [instrumentals_path, backup_vocals_path]
953
+
954
  for file in intermediate_files:
955
  if file and os.path.exists(file) and file != ins_path:
956
+ try:
957
+ os.remove(file)
958
+ except Exception as e:
959
+ logging.debug(f"Cleanup: Could not remove {file}")
960
 
961
+ # Complete!
962
+ tracker.complete("AI Cover generated successfully!")
963
+
964
  return ai_cover_path
965
 
966
  except Exception as e:
967
+ tracker.error(str(e))
968
  raise_exception(str(e), is_webui)
969
 
970
 
971
+ # =============================================================================
972
+ # BATCH PROCESSING
973
+ # =============================================================================
974
+
975
  @spaces.GPU(duration=120)
976
  def batch_process_files(
977
+ input_files, voice_model, pitch_change, f0_method, index_rate, protect,
978
+ output_format, progress=gr.Progress()
979
  ):
980
  """
981
  Process multiple audio files with the same RVC model settings.
982
+ Features enhanced progress tracking with per-file progress.
983
  """
984
  if not input_files:
985
  raise gr.Error("No files provided for batch processing!")
 
987
  if not voice_model:
988
  raise gr.Error("Please select a voice model!")
989
 
990
+ # Initialize batch progress tracker
991
+ tracker = EnhancedProgressTracker(progress=progress, is_webui=True)
992
+ tracker.start("Starting batch processing...")
993
+
994
  results = []
995
  total_files = len(input_files)
996
  batch_output_dir = os.path.join(output_dir, 'batch_output')
997
  os.makedirs(batch_output_dir, exist_ok=True)
998
 
 
 
999
  # Load RVC model once for efficiency
1000
+ tracker.update_step(detail="Loading RVC model (shared across all files)...")
1001
  rvc_model_path, rvc_index_path = get_rvc_model(voice_model, True)
1002
  cpt, version, net_g, tgt_sr, vc = get_vc(device, config.is_half, config, rvc_model_path)
1003
 
 
1016
  filename = os.path.basename(input_path)
1017
  base_name = os.path.splitext(filename)[0]
1018
 
1019
+ # Update progress with file info
1020
+ file_progress = ((i) / total_files) * 100
1021
+ tracker.update_step(
1022
+ detail=f"Processing file {i+1}/{total_files}: {filename}",
1023
+ sub_progress=0
1024
+ )
1025
 
1026
  # Load and convert audio
1027
  audio, sr = librosa.load(input_path, sr=16000, mono=True)
1028
 
 
1029
  temp_input = os.path.join(batch_output_dir, f'temp_{base_name}.wav')
1030
  sf.write(temp_input, audio, 16000)
1031
 
 
1032
  output_path = os.path.join(batch_output_dir, f'{base_name}_{voice_model}_converted.{output_format}')
1033
 
1034
+ # Update progress for voice conversion
1035
+ tracker.update_step(
1036
+ detail=f"[{i+1}/{total_files}] Converting voice: {filename}",
1037
+ sub_progress=0.5
1038
+ )
1039
+
1040
  # Run voice conversion
1041
  rvc_infer(
1042
  rvc_index_path, index_rate, temp_input,
 
1055
  output_path = output_path.replace(f'.{output_format}', '.wav')
1056
 
1057
  results.append(output_path)
1058
+ logging.info(f"[+] Batch processed: {filename}")
1059
+
1060
+ # Mark file complete
1061
+ tracker.update_step(
1062
+ detail=f"[✓] Completed: {filename}",
1063
+ sub_progress=1.0
1064
+ )
1065
 
1066
  except Exception as e:
1067
+ logging.error(f"[-] Error processing {getattr(file_info, 'name', 'unknown')}: {str(e)}")
1068
  continue
1069
 
1070
+ # Cleanup GPU resources
1071
  del cpt
1072
  gc.collect()
1073
+ if torch.cuda.is_available():
1074
+ torch.cuda.empty_cache()
1075
 
1076
+ tracker.complete(f"Batch complete! {len(results)}/{total_files} files processed.")
1077
 
1078
  return results
1079
 
1080
 
1081
+ # =============================================================================
1082
+ # PREVIEW GENERATION
1083
+ # =============================================================================
1084
+
1085
  @spaces.GPU(duration=30)
1086
+ def generate_preview(
1087
+ audio_path, duration=10, voice_model=None, pitch_change=0, f0_method='rmvpe+',
1088
+ index_rate=0.5, protect=0.33, progress=gr.Progress()
1089
+ ):
1090
  """
1091
  Generate a short preview clip of the voice conversion.
1092
  Useful for testing settings before full conversion.
1093
  """
1094
+ tracker = EnhancedProgressTracker(progress=progress, is_webui=True)
1095
+ tracker.set_steps([
1096
+ ("Loading Audio", 2),
1097
+ ("Extracting Preview Segment", 1),
1098
+ ("Loading RVC Model", 3),
1099
+ ("Converting Preview", 4),
1100
+ ("Finalizing", 1)
1101
+ ])
1102
+ tracker.start()
1103
+
1104
  if not audio_path:
1105
  raise gr.Error("Please provide an audio file for preview!")
1106
 
1107
+ tracker.next_step(detail="Reading audio file...")
1108
 
1109
  # Load audio
1110
  audio, sr = librosa.load(audio_path, sr=16000, mono=True)
 
1121
  preview_input_path = os.path.join(preview_dir, 'preview_input.wav')
1122
  sf.write(preview_input_path, preview_audio, 16000)
1123
 
1124
+ tracker.next_step(detail="Preview extracted, loading model...")
1125
 
1126
  # If no model specified, return original audio
1127
  if not voice_model:
1128
+ tracker.complete("Returning original audio preview.")
1129
  return preview_input_path
1130
 
1131
  # Load model
 
1134
 
1135
  global hubert_model
1136
 
1137
+ tracker.next_step(detail="Generating voice conversion preview...")
1138
+
1139
  # Convert preview
1140
  preview_output_path = os.path.join(preview_dir, 'preview_output.wav')
1141
  rvc_infer(
 
1147
  del cpt
1148
  gc.collect()
1149
 
1150
+ tracker.complete("Preview generated successfully!")
1151
 
1152
  return preview_output_path
1153
 
1154
 
1155
+ # =============================================================================
1156
+ # GLOBAL INITIALIZATION & CLI
1157
+ # =============================================================================
1158
+
1159
+ device = "cuda:0" if torch.cuda.is_available() else "cpu"
1160
+ compute_half = True if torch.cuda.is_available() else False
1161
+ config = Config(device, compute_half)
1162
+ hubert_model = load_hubert("cuda", config.is_half, None)
1163
+
1164
+ print(f"Device: {device}, Half precision: {config.is_half}")
1165
+ print("RVC models and Hubert loaded successfully.")
1166
+
1167
+
1168
  if __name__ == '__main__':
1169
+ parser = argparse.ArgumentParser(
1170
+ description='Generate an AI cover song in the song_output/id directory.',
1171
+ add_help=True
1172
+ )
1173
+ parser.add_argument('-i', '--song-input', type=str, required=True, help='Link to a YouTube video or filepath to local mp3/wav')
1174
+ parser.add_argument('-dir', '--rvc-dirname', type=str, required=True, help='Name of folder in rvc_models containing RVC model')
1175
+ parser.add_argument('-p', '--pitch-change', type=int, required=True, help='Pitch change in octaves (+1 M→F, -1 F→M)')
1176
+ parser.add_argument('-k', '--keep-files', action=argparse.BooleanOptionalAction, help='Keep intermediate audio files')
1177
+ parser.add_argument('-ir', '--index-rate', type=float, default=0.5, help='Index rate for timbre control (0-1)')
1178
+ parser.add_argument('-fr', '--filter-radius', type=int, default=3, help='Filter radius for median filtering (0-7)')
1179
+ parser.add_argument('-rms', '--rms-mix-rate', type=float, default=0.25, help='RMS mix rate (0=original loudness, 1=fixed)')
1180
+ parser.add_argument('-palgo', '--pitch-detection-algo', type=str, default='rmvpe', help='Pitch detection algorithm')
1181
+ parser.add_argument('-hop', '--crepe-hop-length', type=int, default=128, help='Crepe hop length for mangio-crepe')
1182
+ parser.add_argument('-pro', '--protect', type=float, default=0.33, help='Protect voiceless consonants (0-0.5)')
1183
+ parser.add_argument('-mv', '--main-vol', type=int, default=0, help='Main vocals volume (dB)')
1184
+ parser.add_argument('-bv', '--backup-vol', type=int, default=0, help='Backup vocals volume (dB)')
1185
+ parser.add_argument('-iv', '--inst-vol', type=int, default=0, help='Instrumentals volume (dB)')
1186
+ parser.add_argument('-pall', '--pitch-change-all', type=int, default=0, help='Overall pitch change (semitones)')
1187
+ parser.add_argument('-rsize', '--reverb-size', type=float, default=0.15, help='Reverb room size (0-1)')
1188
+ parser.add_argument('-rwet', '--reverb-wetness', type=float, default=0.2, help='Reverb wet level (0-1)')
1189
+ parser.add_argument('-rdry', '--reverb-dryness', type=float, default=0.8, help='Reverb dry level (0-1)')
1190
+ parser.add_argument('-rdamp', '--reverb-damping', type=float, default=0.7, help='Reverb damping (0-1)')
1191
+ parser.add_argument('-oformat', '--output-format', type=str, default='mp3', help='Output format (mp3/wav)')
1192
  args = parser.parse_args()
1193
 
1194
  rvc_dirname = args.rvc_dirname
1195
  if not os.path.exists(os.path.join(rvc_models_dir, rvc_dirname)):
1196
  raise Exception(f'The folder {os.path.join(rvc_models_dir, rvc_dirname)} does not exist.')
1197
 
1198
+ cover_path = song_cover_pipeline(
1199
+ args.song_input, rvc_dirname, args.pitch_change, args.keep_files,
1200
+ main_gain=args.main_vol, backup_gain=args.backup_vol, inst_gain=args.inst_vol,
1201
+ index_rate=args.index_rate, filter_radius=args.filter_radius,
1202
+ rms_mix_rate=args.rms_mix_rate, f0_method=args.pitch_detection_algo,
1203
+ crepe_hop_length=args.crepe_hop_length, protect=args.protect,
1204
+ pitch_change_all=args.pitch_change_all,
1205
+ reverb_rm_size=args.reverb_size, reverb_wet=args.reverb_wetness,
1206
+ reverb_dry=args.reverb_dryness, reverb_damping=args.reverb_damping,
1207
+ output_format=args.output_format
1208
+ )
1209
+ print(f'[+] Cover generated at {cover_path}')