Spaces:
Running
Running
| import sys | |
| sys.stdout.reconfigure(line_buffering=True) | |
| import threading | |
| import torch | |
| import torchaudio | |
| import soundfile as sf | |
| import numpy as np | |
| import gradio as gr | |
| from transformers import RobertaTokenizer | |
| from huggingface_hub import hf_hub_download | |
| from pyharp import ModelCard, build_endpoint | |
| from caco_torch.caco import create_caco_model | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| model = None | |
| tokenizer = None | |
| model_loading = True | |
| model_error = None | |
| def load_model(): | |
| global model, tokenizer, model_loading, model_error | |
| try: | |
| m = create_caco_model() | |
| ckpt_path = hf_hub_download(repo_id="teamup-tech/cacophony-weights", filename="Cacophony_torch.pt") | |
| ckpt = torch.load(ckpt_path, map_location=DEVICE, weights_only=False) | |
| if "model_state_dict" in ckpt: | |
| m.load_state_dict(ckpt["model_state_dict"]) | |
| elif "state_dict" in ckpt: | |
| m.load_state_dict(ckpt["state_dict"]) | |
| else: | |
| m.load_state_dict(ckpt) | |
| model = m.to(DEVICE).eval() | |
| tokenizer = RobertaTokenizer.from_pretrained("roberta-base") | |
| print("Model loaded.", flush=True) | |
| except Exception as e: | |
| model_error = str(e) | |
| print(f"Load error: {e}", flush=True) | |
| finally: | |
| model_loading = False | |
| threading.Thread(target=load_model, daemon=True).start() | |
| def audio_to_patches(path): | |
| """Convert an audio file to the patch format expected by the model. | |
| Ported from eval_caco_torch.py: compute_mel_spectrogram, spectrogram_to_patches, | |
| and prepare_audio_batch. Audio loading uses soundfile instead of torchaudio.load. | |
| """ | |
| # Load, downmix to mono, resample to 16 kHz | |
| data, sr = sf.read(path, dtype='float32', always_2d=True) | |
| wav = torch.from_numpy(data.T) # (channels, samples) | |
| if wav.shape[0] > 1: | |
| wav = wav.mean(0, keepdim=True) | |
| if sr != 16000: | |
| wav = torchaudio.functional.resample(wav, sr, 16000) | |
| audio = wav.squeeze() | |
| hop, win, n_fft, n_mels = 160, 400, 512, 128 # from compute_mel_spectrogram | |
| # TF's STFT uses ceil(len/hop) frames; pad to match that before computing STFT. | |
| n_frames = (len(audio) + hop - 1) // hop | |
| required_len = (n_frames - 1) * hop + n_fft | |
| audio = torch.nn.functional.pad(audio, (0, max(0, required_len - len(audio)))) | |
| stft = torch.stft(audio, n_fft=n_fft, hop_length=hop, win_length=win, | |
| window=torch.hann_window(win), return_complex=True, center=False) | |
| spec = torch.abs(stft).T # (time, freq) | |
| mel_fb = torchaudio.functional.melscale_fbanks( | |
| n_freqs=n_fft // 2 + 1, f_min=0, f_max=8000, n_mels=n_mels, sample_rate=16000, norm=None) | |
| mel = torch.log(spec @ mel_fb + 1e-5).mul(0.2).add(0.9).numpy() | |
| pt, pf, max_patches = 16, 16, 512 # from spectrogram_to_patches | |
| mel = mel[:mel.shape[0] // pt * pt] | |
| nt, nf = mel.shape[0] // pt, n_mels // pf | |
| # reshape axes: (nt, pt, nf, pf) to (nt, nf, pt, pf) to (n_patches, patch_size) | |
| patches = mel.reshape(nt, pt, nf, pf).transpose(0, 2, 1, 3).reshape(-1, pt * pf) | |
| total = nt * nf | |
| if total > max_patches: # clip > ~83s: keep first 512 patches | |
| patches, mask = patches[:max_patches], np.ones(max_patches, dtype=np.float32) | |
| idx = np.arange(max_patches) | |
| else: | |
| mask = (np.arange(max_patches) < total).astype(np.float32) | |
| idx = (mask * np.arange(max_patches)).astype(np.int64) | |
| patches = np.pad(patches, [[0, max_patches - total], [0, 0]]) | |
| ti, fi = idx // nf, idx % nf | |
| def to_tensor(a): | |
| return torch.from_numpy(a.astype(np.float32)).unsqueeze(0).to(DEVICE) | |
| return { | |
| "audio_patches": to_tensor(patches), | |
| "audio_time_inds": to_tensor(ti.astype(np.float32)), | |
| "audio_freq_inds": to_tensor(fi.astype(np.float32)), | |
| "audio_mask": to_tensor(mask), | |
| } | |
| def process_fn(audio_path, temperature): | |
| if model_loading: | |
| raise gr.Error("Model is still loading, please try again in a moment.") | |
| if model is None: | |
| raise gr.Error(f"Model failed to load: {model_error}") | |
| batch = audio_to_patches(audio_path) | |
| _, audio_hidden = model.get_audio_embedding( | |
| audio_patches=batch["audio_patches"], | |
| audio_time_inds=batch["audio_time_inds"], | |
| audio_freq_inds=batch["audio_freq_inds"], | |
| audio_mask=batch["audio_mask"], | |
| deterministic=True, return_hidden_state=True, normalize=False, | |
| ) | |
| # Decode token by token: seed with BOS, stop at EOS or 100 tokens. | |
| generated = [tokenizer.bos_token_id] | |
| for _ in range(100): | |
| ids = torch.tensor([generated], dtype=torch.long, device=DEVICE) | |
| mask = torch.ones(1, len(generated), device=DEVICE) | |
| logits = model.get_decoder_logits( | |
| audio_hidden_state=audio_hidden, audio_mask=batch["audio_mask"], | |
| text_input_ids=ids, text_mask=mask, | |
| ) | |
| next_token = int(torch.multinomial( | |
| torch.softmax(logits[0, -1] / temperature, dim=-1), 1)) | |
| if next_token == tokenizer.eos_token_id: | |
| break | |
| generated.append(next_token) | |
| return tokenizer.decode(generated[1:], skip_special_tokens=True) # skip BOS | |
| model_card = ModelCard( | |
| name="Cacophony", | |
| description="Generates a text description of the input audio.", | |
| author="Ge Zhu, Jordan Darefsky, Zhiyao Duan", | |
| tags=["audio", "captioning"], | |
| ) | |
| with gr.Blocks() as demo: | |
| input_components = [ | |
| gr.Audio(type="filepath", label="Input Audio").harp_required(True), | |
| gr.Slider(minimum=0.1, maximum=1.0, step=0.05, value=0.1, | |
| label="Caption Creativity", | |
| info="Lower = more focused, higher = more varied"), | |
| ] | |
| output_components = [ | |
| gr.Textbox(label="Generated Caption"), | |
| ] | |
| build_endpoint( | |
| model_card=model_card, | |
| input_components=input_components, | |
| output_components=output_components, | |
| process_fn=process_fn, | |
| ) | |
| demo.queue().launch() | |