| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """ |
| Calculate UTMOS score with automatic Mean Opinion Score (MOS) prediction system |
| """ |
| import argparse |
| import logging |
| import multiprocessing as mp |
| import os |
| import sys |
| import traceback |
| import warnings |
| from concurrent.futures import ProcessPoolExecutor, as_completed |
|
|
| import numpy as np |
| import torch |
| from tqdm import tqdm |
|
|
| from omnivoice.eval.models.utmos import UTMOS22Strong |
| from omnivoice.eval.utils import load_eval_waveform |
| from omnivoice.utils.data_utils import read_test_list |
|
|
| warnings.filterwarnings("ignore") |
|
|
| |
| worker_model = None |
| worker_device = None |
| worker_sr = 16000 |
|
|
|
|
| def get_parser() -> argparse.ArgumentParser: |
| parser = argparse.ArgumentParser( |
| description="Calculate UTMOS score using UTMOS22Strong model." |
| ) |
| parser.add_argument( |
| "--wav-path", |
| type=str, |
| required=True, |
| help="Path to the directory containing evaluated speech files.", |
| ) |
| parser.add_argument( |
| "--test-list", |
| type=str, |
| required=True, |
| help="Path to the JSONL test list. Each line is a JSON object " |
| "with fields: id, text, ref_audio, ref_text, language_id, language_name.", |
| ) |
| parser.add_argument( |
| "--model-dir", |
| type=str, |
| required=True, |
| help="Local path of our evaluation model repository." |
| "Download from https://huggingface.co/k2-fsa/TTS_eval_models." |
| "Will use 'tts_eval_models/mos/utmos22_strong_step7459_v1.pt'" |
| " in this script", |
| ) |
| parser.add_argument( |
| "--extension", |
| type=str, |
| default="wav", |
| help="Extension of the speech files. Default: wav", |
| ) |
| parser.add_argument( |
| "--decode-path", |
| type=str, |
| default=None, |
| help="Path to the output file where UTMOS information will be saved. " |
| "If not provided, results are only printed to console.", |
| ) |
| parser.add_argument( |
| "--nj-per-gpu", |
| type=int, |
| default=1, |
| help="Number of worker processes to spawn per GPU.", |
| ) |
| return parser |
|
|
|
|
| def get_device(rank: int = 0) -> torch.device: |
| assert torch.cuda.is_available(), "CUDA is required but not available." |
| device = torch.device(f"cuda:{rank}") |
| torch.cuda.set_device(rank) |
| return device |
|
|
|
|
| def worker_init( |
| rank_queue, |
| model_path, |
| ): |
| """Initialize worker process with model and device.""" |
| global worker_model, worker_device, worker_sr |
|
|
| |
| torch.set_num_threads(2) |
|
|
| formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] [Worker %(process)d] %(message)s" |
| logging.basicConfig(format=formatter, level=logging.INFO, force=True) |
|
|
| rank = rank_queue.get() if rank_queue else -1 |
|
|
| worker_device = get_device(rank) |
| worker_sr = 16000 |
|
|
| logging.debug(f"Initializing UTMOS worker on {worker_device}") |
|
|
| |
| worker_model = UTMOS22Strong() |
| try: |
| |
| state_dict = torch.load(model_path, map_location="cpu") |
| worker_model.load_state_dict(state_dict) |
| except Exception as e: |
| logging.error(f"Failed to load model from {model_path}: {e}") |
| raise |
|
|
| worker_model.to(worker_device) |
| worker_model.eval() |
|
|
|
|
| @torch.no_grad() |
| def run_utmos_worker(file_idx, wav_path, language_name): |
| """Worker function to process a single audio file.""" |
| try: |
| if not os.path.exists(wav_path): |
| return file_idx, wav_path, language_name, f"File not found: {wav_path}", "error" |
|
|
| |
| speech = load_eval_waveform(wav_path, worker_sr, device=worker_device) |
|
|
| |
| |
| score = worker_model(speech.unsqueeze(0), worker_sr) |
|
|
| return file_idx, wav_path, language_name, score.item(), "success" |
|
|
| except Exception as e: |
| error_detail = ( |
| f"Error processing {wav_path}: {str(e)}\n" |
| f"Traceback:\n{traceback.format_exc()}" |
| ) |
| return file_idx, wav_path, language_name, error_detail, "error" |
|
|
|
|
| def main(): |
| parser = get_parser() |
| args = parser.parse_args() |
|
|
| |
| torch.set_num_threads(2) |
|
|
| mp.set_start_method("spawn", force=True) |
|
|
| formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" |
| logging.basicConfig(format=formatter, level=logging.INFO, force=True) |
|
|
| |
| if not os.path.isdir(args.wav_path): |
| logging.error(f"Invalid directory: {args.wav_path}") |
| sys.exit(1) |
|
|
| model_path = os.path.join(args.model_dir, "mos/utmos22_strong_step7459_v1.pt") |
| if not os.path.exists(model_path): |
| logging.error(f"Model file not found at {model_path}") |
| sys.exit(1) |
|
|
| |
| logging.info(f"Calculating UTMOS for {args.wav_path}") |
|
|
| wav_files = [] |
| try: |
| samples = read_test_list(args.test_list) |
| for s in samples: |
| language_name = s.get("language_name") or "unknown" |
| eval_wav_path = os.path.join(args.wav_path, f"{s['id']}.{args.extension}") |
| wav_files.append((eval_wav_path, language_name)) |
| except Exception as e: |
| raise ValueError(f"Error reading test list {args.test_list}: {e}") |
|
|
| |
| num_gpus = torch.cuda.device_count() |
| assert num_gpus > 0, "No GPU found. GPU is required." |
| total_procs = num_gpus * args.nj_per_gpu |
|
|
| logging.info( |
| f"Starting evaluation with {total_procs} processes on {num_gpus} GPUs." |
| ) |
|
|
| manager = mp.Manager() |
| rank_queue = manager.Queue() |
|
|
| for rank in list(range(num_gpus)) * args.nj_per_gpu: |
| rank_queue.put(rank) |
|
|
| scores = [] |
|
|
| fout = None |
| if args.decode_path: |
| os.makedirs(os.path.dirname(args.decode_path), exist_ok=True) |
| fout = open(args.decode_path, "w", encoding="utf8") |
| logging.info(f"Saving detailed UTMOS results to: {args.decode_path}") |
| fout.write("Name\tUTMOS\n") |
|
|
| try: |
| with ProcessPoolExecutor( |
| max_workers=total_procs, |
| initializer=worker_init, |
| initargs=( |
| rank_queue, |
| model_path, |
| ), |
| ) as executor: |
| futures = [] |
| for i, (wav_path, language_name) in enumerate(wav_files): |
| futures.append( |
| executor.submit(run_utmos_worker, i, wav_path, language_name) |
| ) |
|
|
| pbar = tqdm( |
| as_completed(futures), total=len(wav_files), desc="Evaluating UTMOS" |
| ) |
| lang_stats = {} |
| for future in pbar: |
| idx, path, language_name, result, status = future.result() |
| if status == "success": |
| if language_name not in lang_stats: |
| lang_stats[language_name] = [] |
| lang_stats[language_name].append(result) |
| scores.append(result) |
| if fout: |
| if language_name == "unknown": |
| fout.write(f"{os.path.basename(path)}\t{result:.2f}\n") |
| else: |
| fout.write( |
| f"{language_name}\t{os.path.basename(path)}\t{result:.2f}\n" |
| ) |
| else: |
| pbar.write(f"!!! FAILED [File {idx}]: {path} | {result}") |
|
|
| except (Exception, KeyboardInterrupt) as e: |
| logging.critical( |
| f"An unrecoverable error occurred: {e}. Terminating all processes." |
| ) |
| detailed_error_info = traceback.format_exc() |
| logging.error(f"--- DETAILED TRACEBACK ---\n{detailed_error_info}") |
| sys.exit(1) |
|
|
| print("-" * 50) |
|
|
| if len(lang_stats) > 1: |
| lang_scores = [] |
| for lang in sorted(lang_stats.keys()): |
| l_scores = lang_stats[lang] |
| l_avg = np.mean(l_scores) |
| lang_scores.append(l_scores) |
| l_count = len(l_scores) |
| logging.info(f"[{lang}] UTMOS score: {l_avg:.3f} ({l_count} samples)") |
| if fout: |
| fout.write(f"[{lang}] UTMOS: {l_avg:.3f} ({l_count} samples)\n") |
| logging.info( |
| f"Macro-average UTMOS over {len(lang_stats)} languages: " |
| f"{np.mean([np.mean(ls) for ls in lang_scores]):.3f}" |
| ) |
| if fout: |
| fout.write( |
| f"\nMacro-average UTMOS over {len(lang_stats)} languages: " |
| f"{np.mean([np.mean(ls) for ls in lang_scores]):.3f}\n" |
| ) |
|
|
| if scores: |
| avg_score = np.mean(scores) |
| logging.info(f"Processed {len(scores)}/{len(wav_files)} files.") |
| logging.info(f"UTMOS score: {avg_score:.2f}") |
| if fout: |
| fout.write(f"\nAverage UTMOS: {avg_score:.2f}\n") |
| else: |
| logging.error("No valid scores computed.") |
| print("-" * 50) |
|
|
| if fout: |
| fout.close() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|