please modify my uvicorn server to only output wav format, return full script fixed save tokens by not explaining

# server.py
import os
import platform
import argparse
import subprocess
import shutil
import tempfile
import torch
import torchaudio
import librosa
import numpy as np
import yaml
from pydub import AudioSegment
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
import uvicorn

# ─────────────────────────────────────────────────────────────────────────────
# NOTE: All of your “Seed-VC” code (model loading, voice_conversion, etc.) lives here.
# I simply copied your entire script (including imports) but removed Gradio bits
# and refactored it into functions that can be called from FastAPI.
# ─────────────────────────────────────────────────────────────────────────────

# Ensure HF cache points inside your WSL folder structure
os.environ['HF_HUB_CACHE'] = './checkpoints/hf_cache'

# -------------------------
#   Imports from original
# -------------------------
import torch
import torchaudio
import librosa
from modules.commons import build_model, load_checkpoint, recursive_munch, str2bool
from hf_utils import load_custom_model_from_hf
from modules.audio import mel_spectrogram
from modules.rmvpe import RMVPE
from transformers import AutoFeatureExtractor, WhisperModel, Wav2Vec2FeatureExtractor, Wav2Vec2Model, HubertModel
import argparse

# ─────────────────────────────────────────────────────────────────────────────

app = FastAPI()

# Allow CORS if you want to call from a browser/another machine:
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["POST", "GET"],
    allow_headers=["*"],
)


def str2bool(v):
    if isinstance(v, bool):
        return v
    if v.lower() in ('yes', 'true', 't', 'y', '1'):
        return True
    elif v.lower() in ('no', 'false', 'f', 'n', '0'):
        return False
    else:
        raise argparse.ArgumentTypeError('Boolean value expected.')


# ─────────────────────────────────────────────────────────────────────────────
#  Helper functions to map WSL ↔ Windows AppData\Roaming\seed-vc path
# ─────────────────────────────────────────────────────────────────────────────

def get_windows_appdata_seed_vc_folder() -> str:
    """
    Returns the WSL path equivalent of Windows:
        %APPDATA%\seed-vc
    Creates that folder if needed.
    """
    # If running inside WSL, “platform.release()” contains “microsoft”
    is_wsl = (platform.system() == "Linux" and "microsoft" in platform.release().lower())

    if is_wsl:
        try:
            # 1) Ask PowerShell (Windows side) for %APPDATA%
            result = subprocess.run(
                ["powershell.exe", "-NoProfile", "-Command", "[Environment]::GetFolderPath('ApplicationData')"],
                capture_output=True,
                text=True
            )
            win_appdata = result.stdout.strip()  # e.g. "C:\Users\<User>\AppData\Roaming"

            # 2) Convert that Windows path into WSL path with `wslpath -u`
            to_wsl = subprocess.run(
                ["wslpath", win_appdata],
                capture_output=True,
                text=True
            )
            wsl_appdata = to_wsl.stdout.strip()

            # 3) Append “seed-vc” and create it
            seed_vc_wsl = os.path.join(wsl_appdata, "seed-vc")
            os.makedirs(seed_vc_wsl, exist_ok=True)
            return seed_vc_wsl

        except Exception as e:
            # Fallback if something fails: put in ~/seed-vc
            fallback = os.path.expanduser("~/seed-vc")
            os.makedirs(fallback, exist_ok=True)
            return fallback

    else:
        # Not WSL → just put under $HOME/seed-vc
        fallback = os.path.expanduser("~/seed-vc")
        os.makedirs(fallback, exist_ok=True)
        return fallback


def wsl_to_windows_path(wsl_path: str) -> str:
    """
    Convert a single WSL path into Windows format via `wslpath -w`.
    Returns a string like "C:\\Users\\<User>\\AppData\\Roaming\\seed-vc\\...".
    If conversion fails, returns the original path.
    """
    try:
        is_wsl = (platform.system() == "Linux" and "microsoft" in platform.release().lower())
        if is_wsl:
            result = subprocess.run(
                ["wslpath", "-w", wsl_path],
                capture_output=True,
                text=True
            )
            return result.stdout.strip().replace("\r", "").replace("\n", "")
    except Exception:
        pass
    return wsl_path


OUTPUT_FOLDER = get_windows_appdata_seed_vc_folder()
# ─────────────────────────────────────────────────────────────────────────────

# ─────────────────────────────────────────────────────────────────────────────
#  Copy‐pasted model‐loading and voice_conversion from your script, but adapted
#  to live in a FastAPI context instead of Gradio. On startup, we call load_models().
# ─────────────────────────────────────────────────────────────────────────────

fp16 = False
device = None

def load_models(args):
    global sr, hop_length, fp16, device
    fp16 = args.fp16

    # f0-conditioned model
    if args.checkpoint is None or args.checkpoint == "":
        dit_checkpoint_path, dit_config_path = load_custom_model_from_hf(
            "Plachta/Seed-VC",
            "DiT_seed_v2_uvit_whisper_base_f0_44k_bigvgan_pruned_ft_ema_v2.pth",
            "config_dit_mel_seed_uvit_whisper_base_f0_44k.yml"
        )
    else:
        dit_checkpoint_path = args.checkpoint
        dit_config_path = args.config

    config = yaml.safe_load(open(dit_config_path, "r"))
    model_params = recursive_munch(config["model_params"])
    model_params.dit_type = 'DiT'
    model = build_model(model_params, stage="DiT")

    hop_length = config["preprocess_params"]["spect_params"]["hop_length"]
    sr = config["preprocess_params"]["sr"]

    # Load checkpoint into model
    model, _, _, _ = load_checkpoint(
        model,
        None,
        dit_checkpoint_path,
        load_only_params=True,
        ignore_modules=[],
        is_distributed=False,
    )
    for key in model:
        model[key].eval()
        model[key].to(device)

    model.cfm.estimator.setup_caches(max_batch_size=1, max_seq_length=8192)

    # campplus
    from modules.campplus.DTDNN import CAMPPlus
    campplus_ckpt_path = load_custom_model_from_hf("funasr/campplus", "campplus_cn_common.bin", None)
    campplus_model = CAMPPlus(feat_dim=80, embedding_size=192)
    campplus_model.load_state_dict(torch.load(campplus_ckpt_path, map_location="cpu"))
    campplus_model.eval()
    campplus_model.to(device)

    # Vocoder
    vocoder_type = model_params.vocoder.type
    if vocoder_type == 'bigvgan':
        from modules.bigvgan import bigvgan
        bigvgan_name = model_params.vocoder.name
        bigvgan_model = bigvgan.BigVGAN.from_pretrained(bigvgan_name, use_cuda_kernel=False)
        bigvgan_model.remove_weight_norm()
        bigvgan_model = bigvgan_model.eval().to(device)
        vocoder_fn = bigvgan_model

    elif vocoder_type == 'hifigan':
        from modules.hifigan.generator import HiFTGenerator
        from modules.hifigan.f0_predictor import ConvRNNF0Predictor
        hift_config = yaml.safe_load(open('configs/hifigan.yml', 'r'))
        hift_gen = HiFTGenerator(
            **hift_config['hift'],
            f0_predictor=ConvRNNF0Predictor(**hift_config['f0_predictor'])
        )
        hift_path = load_custom_model_from_hf("FunAudioLLM/CosyVoice-300M", 'hift.pt', None)
        hift_gen.load_state_dict(torch.load(hift_path, map_location='cpu'))
        hift_gen.eval()
        hift_gen.to(device)
        vocoder_fn = hift_gen

    elif vocoder_type == "vocos":
        vocos_config = yaml.safe_load(open(model_params.vocoder.vocos.config, 'r'))
        vocos_path = model_params.vocoder.vocos.path
        vocos_model_params = recursive_munch(vocos_config['model_params'])
        vocos = build_model(vocos_model_params, stage='mel_vocos')
        vocos_checkpoint_path = vocos_path
        vocos, _, _, _ = load_checkpoint(
            vocos, None, vocos_checkpoint_path,
            load_only_params=True, ignore_modules=[], is_distributed=False
        )
        _ = [vocos[key].eval().to(device) for key in vocos]
        _ = [vocos[key].to(device) for key in vocos]
        vocoder_fn = vocos.decoder

    else:
        raise ValueError(f"Unknown vocoder type: {vocoder_type}")

    # Speech tokenizer
    speech_tokenizer_type = model_params.speech_tokenizer.type

    if speech_tokenizer_type == 'whisper':
        whisper_name = model_params.speech_tokenizer.name
        whisper_model = WhisperModel.from_pretrained(whisper_name, torch_dtype=torch.float16).to(device)
        del whisper_model.decoder
        whisper_feature_extractor = AutoFeatureExtractor.from_pretrained(whisper_name)

        def semantic_fn(waves_16k):
            ori_inputs = whisper_feature_extractor(
                [waves_16k.squeeze(0).cpu().numpy()],
                return_tensors="pt",
                return_attention_mask=True
            )
            ori_input_features = whisper_model._mask_input_features(
                ori_inputs.input_features, attention_mask=ori_inputs.attention_mask
            ).to(device)
            with torch.no_grad():
                ori_outputs = whisper_model.encoder(
                    ori_input_features.to(whisper_model.encoder.dtype),
                    head_mask=None,
                    output_attentions=False,
                    output_hidden_states=False,
                    return_dict=True,
                )
            S_ori = ori_outputs.last_hidden_state.to(torch.float32)
            S_ori = S_ori[:, :waves_16k.size(-1) // 320 + 1]
            return S_ori

    elif speech_tokenizer_type == 'cnhubert':
        hubert_model_name = config['model_params']['speech_tokenizer']['name']
        hubert_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(hubert_model_name)
        hubert_model = HubertModel.from_pretrained(hubert_model_name)
        hubert_model = hubert_model.to(device).eval().half()

        def semantic_fn(waves_16k):
            ori_list = [waves_16k[bib].cpu().numpy() for bib in range(len(waves_16k))]
            ori_inputs = hubert_feature_extractor(
                ori_list,
                return_tensors="pt",
                return_attention_mask=True,
                padding=True,
                sampling_rate=16000
            ).to(device)
            with torch.no_grad():
                ori_outputs = hubert_model(ori_inputs.input_values.half())
            S_ori = ori_outputs.last_hidden_state.float()
            return S_ori

    elif speech_tokenizer_type == 'xlsr':
        model_name = config['model_params']['speech_tokenizer']['name']
        output_layer = config['model_params']['speech_tokenizer']['output_layer']
        wav2vec_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model_name)
        wav2vec_model = Wav2Vec2Model.from_pretrained(model_name)
        wav2vec_model.encoder.layers = wav2vec_model.encoder.layers[:output_layer]
        wav2vec_model = wav2vec_model.to(device).eval().half()

        def semantic_fn(waves_16k):
            ori_list = [waves_16k[bib].cpu().numpy() for bib in range(len(waves_16k))]
            ori_inputs = wav2vec_feature_extractor(
                ori_list,
                return_tensors="pt",
                return_attention_mask=True,
                padding=True,
                sampling_rate=16000
            ).to(device)
            with torch.no_grad():
                ori_outputs = wav2vec_model(ori_inputs.input_values.half())
            S_ori = ori_outputs.last_hidden_state.float()
            return S_ori

    else:
        raise ValueError(f"Unknown speech tokenizer type: {speech_tokenizer_type}")

    # Mel spectrogram helper
    mel_fn_args = {
        "n_fft": config['preprocess_params']['spect_params']['n_fft'],
        "win_size": config['preprocess_params']['spect_params']['win_length'],
        "hop_size": config['preprocess_params']['spect_params']['hop_length'],
        "num_mels": config['preprocess_params']['spect_params']['n_mels'],
        "sampling_rate": sr,
        "fmin": config['preprocess_params']['spect_params'].get('fmin', 0),
        "fmax": None if config['preprocess_params']['spect_params'].get('fmax', "None") == "None" else 8000,
        "center": False
    }
    to_mel = lambda x: mel_spectrogram(x, **mel_fn_args)

    # F0 extractor (RMVPE)
    model_path = load_custom_model_from_hf("lj1995/VoiceConversionWebUI", "rmvpe.pt", None)
    rmvpe = RMVPE(model_path, is_half=False, device=device)
    f0_fn = rmvpe.infer_from_audio

    return (
        model,
        semantic_fn,
        vocoder_fn,
        campplus_model,
        to_mel,
        mel_fn_args,
        f0_fn,
        sr,
        hop_length
    )


def adjust_f0_semitones(f0_sequence, n_semitones):
    factor = 2 ** (n_semitones / 12)
    return f0_sequence * factor


def crossfade(chunk1, chunk2, overlap):
    fade_out = np.cos(np.linspace(0, np.pi / 2, overlap)) ** 2
    fade_in = np.cos(np.linspace(np.pi / 2, 0, overlap)) ** 2
    chunk2[:overlap] = chunk2[:overlap] * fade_in + chunk1[-overlap:] * fade_out
    return chunk2


# Streaming/chunking parameters (these will be set in main())
model_f0 = None
semantic_fn = None
vocoder_fn = None
campplus_model = None
to_mel_f0 = None
mel_fn_args = None
f0_fn = None
overlap_wave_len = None
max_context_window = None
sr = None
hop_length = None
overlap_frame_len = 16
bitrate = "320k"


@torch.no_grad()
@torch.inference_mode()
def voice_conversion(
    source_path: str,
    target_path: str,
    diffusion_steps: int,
    length_adjust: float,
    inference_cfg_rate: float,
    auto_f0_adjust: bool,
    pitch_shift: int
):
    """
    Exactly your original Gradio-based voice_conversion, except now it is a pure generator
    that yields (mp3_bytes, None) for intermediate chunks, and yields (mp3_bytes, (sr, full_wave))
    on the final chunk. We will consume it end-to-end in our FastAPI handler.
    """
    inference_module = model_f0
    mel_fn = to_mel_f0

    # Load audio files
    source_audio = librosa.load(source_path, sr=sr)[0]
    ref_audio = librosa.load(target_path, sr=sr)[0]

    source_audio = torch.tensor(source_audio).unsqueeze(0).float().to(device)
    ref_audio = torch.tensor(ref_audio[:sr * 25]).unsqueeze(0).float().to(device)

    ref_waves_16k = torchaudio.functional.resample(ref_audio, sr, 16000)
    converted_waves_16k = torchaudio.functional.resample(source_audio, sr, 16000)

    # Compute semantic embeddings (possibly chunked if >30s)
    if converted_waves_16k.size(-1) <= 16000 * 30:
        S_alt = semantic_fn(converted_waves_16k)
    else:
        overlapping_time = 5
        S_alt_list = []
        buffer_tensor = None
        traversed_time = 0
        while traversed_time < converted_waves_16k.size(-1):
            if buffer_tensor is None:
                chunk = converted_waves_16k[:, traversed_time:traversed_time + 16000 * 30]
            else:
                chunk = torch.cat([
                    buffer_tensor,
                    converted_waves_16k[:, traversed_time:traversed_time + 16000 * (30 - overlapping_time)]
                ], dim=-1)
            S_alt_chunk = semantic_fn(chunk)
            if traversed_time == 0:
                S_alt_list.append(S_alt_chunk)
            else:
                S_alt_list.append(S_alt_chunk[:, 50 * overlapping_time:])
            buffer_tensor = chunk[:, -16000 * overlapping_time:]
            traversed_time += 30 * 16000 if traversed_time == 0 else chunk.size(-1) - 16000 * overlapping_time
        S_alt = torch.cat(S_alt_list, dim=1)

    ori_waves_16k = torchaudio.functional.resample(ref_audio, sr, 16000)
    S_ori = semantic_fn(ori_waves_16k)

    mel = mel_fn(source_audio.to(device).float())
    mel2 = mel_fn(ref_audio.to(device).float())

    target_lengths = torch.LongTensor([int(mel.size(2) * length_adjust)]).to(mel.device)
    target2_lengths = torch.LongTensor([mel2.size(2)]).to(mel2.device)

    feat2 = torchaudio.compliance.kaldi.fbank(
        ref_waves_16k,
        num_mel_bins=80,
        dither=0,
        sample_frequency=16000
    )
    feat2 = feat2 - feat2.mean(dim=0, keepdim=True)
    style2 = campplus_model(feat2.unsqueeze(0))

    F0_ori = f0_fn(ori_waves_16k[0], thred=0.03)
    F0_alt = f0_fn(converted_waves_16k[0], thred=0.03)

    if device.type == "mps":
        F0_ori = torch.from_numpy(F0_ori).float().to(device)[None]
        F0_alt = torch.from_numpy(F0_alt).float().to(device)[None]
    else:
        F0_ori = torch.from_numpy(F0_ori).to(device)[None]
        F0_alt = torch.from_numpy(F0_alt).to(device)[None]

    voiced_F0_ori = F0_ori[F0_ori > 1]
    voiced_F0_alt = F0_alt[F0_alt > 1]

    log_f0_alt = torch.log(F0_alt + 1e-5)
    voiced_log_f0_ori = torch.log(voiced_F0_ori + 1e-5)
    voiced_log_f0_alt = torch.log(voiced_F0_alt + 1e-5)
    median_log_f0_ori = torch.median(voiced_log_f0_ori)
    median_log_f0_alt = torch.median(voiced_log_f0_alt)

    shifted_log_f0_alt = log_f0_alt.clone()
    if auto_f0_adjust:
        shifted_log_f0_alt[F0_alt > 1] = (
            log_f0_alt[F0_alt > 1] - median_log_f0_alt + median_log_f0_ori
        )
    if pitch_shift != 0:
        shifted_log_f0_alt[F0_alt > 1] = adjust_f0_semitones(
            torch.exp(shifted_log_f0_alt[F0_alt > 1]), pitch_shift
        )
        shifted_log_f0_alt = torch.log(shifted_log_f0_alt + 1e-5)

    shifted_f0_alt = torch.exp(shifted_log_f0_alt)

    # Length regulation
    cond, _, codes, commitment_loss, codebook_loss = inference_module.length_regulator(
        S_alt, ylens=target_lengths, n_quantizers=3, f0=shifted_f0_alt
    )
    prompt_condition, _, codes, commitment_loss, codebook_loss = inference_module.length_regulator(
        S_ori, ylens=target2_lengths, n_quantizers=3, f0=F0_ori
    )

    interpolated_shifted_f0_alt = torch.nn.functional.interpolate(
        shifted_f0_alt.unsqueeze(1), size=cond.size(1), mode='nearest'
    ).squeeze(1)

    max_source_window = max_context_window - mel2.size(2)

    processed_frames = 0
    generated_wave_chunks = []

    while processed_frames < cond.size(1):
        chunk_cond = cond[:, processed_frames:processed_frames + max_source_window]
        chunk_f0 = interpolated_shifted_f0_alt[:, processed_frames:processed_frames + max_source_window]
        is_last_chunk = (processed_frames + max_source_window) >= cond.size(1)
        cat_condition = torch.cat([prompt_condition, chunk_cond], dim=1)

        with torch.autocast(device_type=device.type, dtype=torch.float16 if fp16 else torch.float32):
            vc_target = inference_module.cfm.inference(
                cat_condition,
                torch.LongTensor([cat_condition.size(1)]).to(mel2.device),
                mel2,
                style2,
                None,
                diffusion_steps,
                inference_cfg_rate=inference_cfg_rate
            )
            vc_target = vc_target[:, :, mel2.size(-1):]

        vc_wave = vocoder_fn(vc_target.float()).squeeze().cpu()
        if vc_wave.ndim == 1:
            vc_wave = vc_wave.unsqueeze(0)

        if processed_frames == 0:
            if is_last_chunk:
                output_wave = vc_wave[0].cpu().numpy()
                generated_wave_chunks.append(output_wave)
                full_concat = np.concatenate(generated_wave_chunks)
                wav_int16 = (full_concat * 32768.0).astype(np.int16)

                # last mp3 bytes from full waveform
                mp3_bytes = AudioSegment(
                    wav_int16.tobytes(),
                    frame_rate=sr,
                    sample_width=wav_int16.dtype.itemsize,
                    channels=1
                ).export(format="mp3", bitrate=bitrate).read()
                yield mp3_bytes, (sr, full_concat)
                break

            output_wave = vc_wave[0, :-overlap_wave_len].cpu().numpy()
            generated_wave_chunks.append(output_wave)
            previous_chunk = vc_wave[0, -overlap_wave_len:]
            processed_frames += (vc_target.size(2) - overlap_frame_len)

            wav_int16 = (output_wave * 32768.0).astype(np.int16)
            mp3_bytes = AudioSegment(
                wav_int16.tobytes(),
                frame_rate=sr,
                sample_width=wav_int16.dtype.itemsize,
                channels=1
            ).export(format="mp3", bitrate=bitrate).read()
            yield mp3_bytes, None

        elif is_last_chunk:
            output_wave = crossfade(
                previous_chunk.cpu().numpy(),
                vc_wave[0].cpu().numpy(),
                overlap_wave_len
            )
            generated_wave_chunks.append(output_wave)
            full_concat = np.concatenate(generated_wave_chunks)
            wav_int16 = (full_concat * 32768.0).astype(np.int16)

            mp3_bytes = AudioSegment(
                wav_int16.tobytes(),
                frame_rate=sr,
                sample_width=wav_int16.dtype.itemsize,
                channels=1
            ).export(format="mp3", bitrate=bitrate).read()
            yield mp3_bytes, (sr, full_concat)
            break

        else:
            output_wave = crossfade(
                previous_chunk.cpu().numpy(),
                vc_wave[0, :-overlap_wave_len].cpu().numpy(),
                overlap_wave_len
            )
            generated_wave_chunks.append(output_wave)
            previous_chunk = vc_wave[0, -overlap_wave_len:]
            processed_frames += (vc_target.size(2) - overlap_frame_len)

            wav_int16 = (output_wave * 32768.0).astype(np.int16)
            mp3_bytes = AudioSegment(
                wav_int16.tobytes(),
                frame_rate=sr,
                sample_width=wav_int16.dtype.itemsize,
                channels=1
            ).export(format="mp3", bitrate=bitrate).read()
            yield mp3_bytes, None


# ─────────────────────────────────────────────────────────────────────────────
#  End of “Seed-VC” logic
# ─────────────────────────────────────────────────────────────────────────────


@app.post("/convert")
async def convert_endpoint(
    source_file: UploadFile = File(...),
    reference_file: UploadFile = File(...),
    diffusion_steps: int = Form(10),
    length_adjust: float = Form(1.0),
    inference_cfg_rate: float = Form(0.7),
    auto_f0_adjust: bool = Form(True),
    pitch_shift: int = Form(0)
):
    """
    Expects a multipart/form with:
      - source_file: UploadFile (wav)
      - reference_file: UploadFile (wav)
      - diffusion_steps: int
      - length_adjust: float
      - inference_cfg_rate: float
      - auto_f0_adjust: bool
      - pitch_shift: int

    Runs voice_conversion end-to-end. Captures the final MP3 bytes
    (the last yield of voice_conversion) and writes that full MP3 into
    %APPDATA%\seed-vc\converted_<timestamp>.mp3, then returns the WINDOWS
    path to that file.
    """
    try:
        # 1) Save uploaded files to a temporary WSL folder
        tmp_dir = tempfile.mkdtemp(prefix="seedvc_")
        source_path = os.path.join(tmp_dir, "source.wav")
        reference_path = os.path.join(tmp_dir, "reference.wav")

        with open(source_path, "wb") as f_out:
            f_out.write(await source_file.read())
        with open(reference_path, "wb") as f_out:
            f_out.write(await reference_file.read())

        # 2) Run voice_conversion(...) generator to get final_mp3_bytes
        final_mp3_bytes = None
        for mp3_chunk, maybe_full in voice_conversion(
            source_path,
            reference_path,
            diffusion_steps,
            length_adjust,
            inference_cfg_rate,
            auto_f0_adjust,
            pitch_shift
        ):
            # We only care about the last yield where maybe_full is not None
            if maybe_full is not None:
                final_mp3_bytes = mp3_chunk

        if final_mp3_bytes is None:
            return JSONResponse({"error": "Conversion did not produce a final MP3."}, status_code=500)

        # 3) Write final_mp3_bytes into the OUTPUT_FOLDER
        import time
        timestamp = int(time.time())
        out_filename = f"converted_{timestamp}.mp3"
        wsl_out_path = os.path.join(OUTPUT_FOLDER, out_filename)
        with open(wsl_out_path, "wb") as f_out:
            f_out.write(final_mp3_bytes)

        # 4) Convert that WSL path back to Windows path so the client can open it directly
        win_out_path = wsl_to_windows_path(wsl_out_path)
        # Cleanup tmp_dir
        shutil.rmtree(tmp_dir, ignore_errors=True)

        return JSONResponse({
            "message": "Conversion succeeded",
            "windows_path": win_out_path
        })

    except Exception as e:
        return JSONResponse({"error": str(e)}, status_code=500)


def main(args):
    global device
    cuda_target = f"cuda:{args.gpu}" if args.gpu else "cuda"

    if torch.cuda.is_available():
        device = torch.device(cuda_target)
    elif torch.backends.mps.is_available():
        device = torch.device("mps")
    else:
        device = torch.device("cpu")

    print(f"==> Using device: {device}")
    print(f"==> FP16 enabled: {args.fp16}")

    # Load all models once at startup
    (
        mdl,
        sem_fn,
        voc_fn,
        camp_mdl,
        mel_fn,
        mel_args,
        f0_f,
        rate,
        hop
    ) = load_models(args)

    # Save them into globals for the generator to see
    global model_f0, semantic_fn, vocoder_fn, campplus_model, to_mel_f0, mel_fn_args, f0_fn, sr, hop_length, max_context_window, overlap_wave_len
    model_f0 = mdl
    semantic_fn = sem_fn
    vocoder_fn = voc_fn
    campplus_model = camp_mdl
    to_mel_f0 = mel_fn
    mel_fn_args = mel_args
    f0_fn = f0_f
    sr = rate
    hop_length = hop

    max_context_window = sr // hop_length * 30
    overlap_wave_len = overlap_frame_len * hop_length

    # Finally: start Uvicorn
    uvicorn.run(app, host=args.host, port=args.port)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--checkpoint", type=str, help="Path to the checkpoint file", default=None)
    parser.add_argument("--config", type=str, help="Path to the config file", default=None)
    parser.add_argument("--share", type=str2bool, nargs="?", const=True, default=False, help="(unused)")
    parser.add_argument("--fp16", type=str2bool, nargs="?", const=True, default=True, help="Whether to use fp16")
    parser.add_argument("--gpu", type=int, help="Which GPU id to use", default=0)
    parser.add_argument("--host", type=str, help="Host IP to bind server", default="0.0.0.0")
    parser.add_argument("--port", type=int, help="Port to bind server", default=8000)
    args = parser.parse_args()

    main(args)