Abdelrahman2922's picture
Add files using upload-large-folder tool
8d6afae verified
Raw
History Blame Contribute Delete
9.77 kB
#!/usr/bin/env python3
# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
#
# See ../../LICENSE for clarification regarding multiple authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
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")
# Global variables for workers
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
# Limit CPU threads per worker
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}")
# Initialize Model
worker_model = UTMOS22Strong()
try:
# Load weights to CPU first, then move to device
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"
# Load and preprocess waveform
speech = load_eval_waveform(wav_path, worker_sr, device=worker_device)
# Compute score
# UTMOS expects input shape (Batch, Time)
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()
# Main process thread setting
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)
# Validate inputs
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)
# Scan directory for files
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}")
# Setup Parallel Processing
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()