meet_models / code /predict.py
meet5568's picture
Add HOW_TO_USE guide, predict.py and requirements; fix encoder usage examples
29f7657 verified
Raw History Blame Contribute Delete
6.2 kB
#!/usr/bin/env python3
"""
Predict the theme of Hindi songs (romance / sadness / celebration / motivational) with the models
of meet5568/meet_models.
For a NEW song, every one of the 5 cross-validation folds has its own lyrics encoder and its own
classifier; the script runs all 5 and averages their probabilities.
Examples
# lyrics only (downloads the models from Hugging Face the first time; needs access to the repo)
python predict.py --lyrics "tujhe dekha toh yeh jaana sanam, pyaar hota hai deewana sanam"
python predict.py --lyrics-file song.txt
# use an already downloaded copy of the repo
python predict.py --model-dir /path/to/meet_models --lyrics-file song.txt
# lyrics + audio: an .npz with arrays "instrument" and "mix" (and/or "vocal"), each (n_songs, 9984)
python predict.py --lyrics-file song.txt --audio-npz song_audio.npz --combination lyrics+instrument+mix
Python API
from predict import ThemePredictor
p = ThemePredictor() # or ThemePredictor(model_dir="...")
p.predict(["lyrics of song 1", "lyrics of song 2"]) # DataFrame of probabilities
p.predict(lyrics, audio={"instrument": A, "mix": B}, combination="lyrics+instrument+mix")
"""
import argparse
import json
import os
import sys
import numpy as np
import pandas as pd
import torch
REPO = "meet5568/meet_models"
THEMES = ["romance", "sadness", "celebration", "motivational"]
AUDIO = ("vocal", "instrument", "mix")
def get_model_dir(model_dir=None, need_encoders=True, need_classifiers=True):
"""A local copy of the repo; downloads (only the needed parts) if no path is given."""
if model_dir:
return model_dir
from huggingface_hub import snapshot_download
patterns = ["code/*", "README.md"]
if need_encoders:
patterns.append("lyrics_encoder/theme_tuned/*")
if need_classifiers:
patterns.append("classification/*")
# private repo: run `huggingface-cli login` once, or set the HF_TOKEN environment variable
return snapshot_download(REPO, allow_patterns=patterns, token=os.environ.get("HF_TOKEN"))
class ThemePredictor:
def __init__(self, model_dir=None, device=None):
self.dir = get_model_dir(model_dir)
sys.path.insert(0, os.path.join(self.dir, "code"))
import classify # model definitions (same code that trained the models)
self.C = classify
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
def encode_lyrics(self, texts, fold):
from sentence_transformers import SentenceTransformer
enc = SentenceTransformer(os.path.join(self.dir, "lyrics_encoder", "theme_tuned", f"fold_{fold}"),
device=self.device)
enc.max_seq_length = 400
return enc.encode(texts, batch_size=16, normalize_embeddings=True, convert_to_numpy=True)
def _fold_proba(self, combination, fold, inputs):
d = os.path.join(self.dir, "classification", combination, f"fold_{fold}")
info = json.load(open(os.path.join(d, "config.json")))
if info["method"] == "late": # average the single-modality models
return np.mean([self._fold_proba(m, fold, inputs) for m in info["modalities"]], axis=0)
norm = np.load(os.path.join(d, "norm.npz"))
model = self.C.build_model(info["run"], info["modalities"], info["in_dims"], info["model_config"])
model.load_state_dict(torch.load(os.path.join(d, "model.pt"), map_location="cpu"))
model = model.to(self.device).eval()
xs = [torch.tensor((inputs[m] - norm[f"{m}_mean"]) / norm[f"{m}_std"], dtype=torch.float32,
device=self.device) for m in info["modalities"]]
with torch.no_grad():
return self.C.predict_proba(model, xs)
def predict(self, lyrics=None, audio=None, combination=None):
"""lyrics: list of strings (or None); audio: dict modality -> (n, 9984) array (or None).
combination: e.g. "lyrics", "lyrics+instrument+mix", "mix"; default = all given inputs."""
audio = {k: np.asarray(v, dtype=np.float32) for k, v in (audio or {}).items()}
if lyrics is not None and isinstance(lyrics, str):
lyrics = [lyrics]
given = (["lyrics"] if lyrics is not None else []) + [m for m in AUDIO if m in audio]
combination = combination or "+".join(given)
mods = combination.split("+")
missing = [m for m in mods if m not in given]
if missing:
raise ValueError(f"combination {combination} needs inputs {missing}")
n = len(lyrics) if lyrics is not None else len(next(iter(audio.values())))
probs = []
for k in range(5):
inputs = dict(audio)
if "lyrics" in mods:
inputs["lyrics"] = self.encode_lyrics(lyrics, k)
probs.append(self._fold_proba(combination, k, inputs))
p = np.mean(probs, axis=0)
out = pd.DataFrame(p, columns=THEMES)
out.insert(0, "predicted_theme", [THEMES[i] for i in p.argmax(1)])
assert len(out) == n
return out
def main():
ap = argparse.ArgumentParser(description="Predict the theme of Hindi songs")
ap.add_argument("--lyrics", help="lyrics text of one song")
ap.add_argument("--lyrics-file", help="text file with the lyrics of one song")
ap.add_argument("--audio-npz", help=".npz with arrays vocal / instrument / mix, each (n_songs, 9984)")
ap.add_argument("--combination", default=None, help="e.g. lyrics, lyrics+instrument+mix (default: all inputs given)")
ap.add_argument("--model-dir", default=None, help="local copy of meet5568/meet_models (default: download)")
args = ap.parse_args()
lyrics = args.lyrics or (open(args.lyrics_file, encoding="utf-8").read() if args.lyrics_file else None)
audio = dict(np.load(args.audio_npz)) if args.audio_npz else None
if lyrics is None and audio is None:
ap.error("give --lyrics / --lyrics-file and/or --audio-npz")
p = ThemePredictor(args.model_dir).predict([lyrics] if lyrics else None, audio, args.combination)
print(p.round(3).to_string(index=False))
if __name__ == "__main__":
main()