# Copyright 2025 Xiaomi Corporation. import os import json import tqdm from pathlib import Path import json from typing import Literal, Any from pydantic import BaseModel from abc import ABC, abstractmethod from openai import OpenAI import whisper class OpenAIModel: def __init__(self, model: str, temp: float, response_format: Any = None): api_key = os.environ.get("OPENAI_API_KEY") if not api_key: raise ValueError("Please set OPENAI_API_KEY environment variable") self.client = OpenAI( base_url="https://api.openai.com/v1", api_key=api_key, ) self.model = 'gpt-4o' self.temp = float(temp) self.response_format = response_format or False def generate(self, prompt:Any): if type(prompt) == str: prompt = [{"role": "user", "content": prompt}] elif isinstance(prompt, list) and all(isinstance(item, dict) and 'role' in item and item['role'] in ['user', 'assistant'] for item in prompt): pass else: raise ValueError("Prompt must be a string or a list of dictionaries with 'role' keys as 'user' or 'assistant'.") if self.response_format: response = self.client.beta.chat.completions.parse( model=self.model, messages=prompt, temperature=self.temp, response_format=self.response_format, timeout=100.0 ) return response.choices[0].message.parsed else: response = self.client.chat.completions.create( model = self.model, messages = prompt, temperature = self.temp, timeout=100.0 ) return response.choices[0].message.content class JudgeResponse(BaseModel): reasoning: str verdict: Literal["YES", "NO"] JUDGE_PROMPT = '''You are tasked with evaluating a model response to see if it meets a specific criteria. The criteria will always be YES/NO evaluation. The model response is as follows: {} The criteria that the model response must meet is as follows. Be VERY STRICT!: {} Print your reasoning followed by your verdict, either "YES" or "NO".''' class MultiChallengeEvaluator: def __init__(self, model, dataset, task, device=None, model_type='base', n_few_shots=5, exec_mode='infer', thinking=False): self.model = model self.dataset = dataset self.device = device self.model_type = model_type self.thinking = thinking root_path = os.getcwd() self.prompt_root_path = os.path.join(root_path, "data/multi_challenge/prompt_wav") self.evaluation_model = OpenAIModel( model="gpt-4o", temp=0, response_format=JudgeResponse ) self.task_type = task.split("_")[-1] if self.task_type == 's2s': self.asr_model = self.load_en_asr_model() def load_en_asr_model(self): model = whisper.load_model("large-v3").to(self.device) return model def asr(self, audio_path): if self.dataset.lang == "en": transcription = self.asr_model.transcribe(audio_path)["text"] return transcription.strip() def evaluate(self, output_dir, rank=0, world_size=1): if not self.thinking: output_dir = Path(output_dir) else: output_dir = Path(str(output_dir) + "_thinking") output_dir.mkdir(parents=True, exist_ok=True) predictions_file_path_rank = output_dir / f"predictions_rank_{rank}.json" predictions_this_rank = [] processed_item_ids_this_rank = set() if predictions_file_path_rank.exists(): try: with open(predictions_file_path_rank, "r", encoding="utf-8") as f: predictions_this_rank = json.load(f) for pred_item in predictions_this_rank: processed_item_ids_this_rank.add(pred_item["uid"]) if processed_item_ids_this_rank: print(f"Rank {rank} resumed from checkpoint: {predictions_file_path_rank}. {len(processed_item_ids_this_rank)} items already processed for this rank.") except (json.JSONDecodeError, IOError) as e: print(f"Warning: Could not load predictions file {predictions_file_path_rank} for rank {rank}: {e}. Starting fresh inference for this rank.") predictions_this_rank = [] processed_item_ids_this_rank = set() dataset_shard = list(self.dataset)[rank::world_size] items_to_process_this_run = [] for idx, item in enumerate(dataset_shard): item_id = rank + idx * world_size if item_id not in processed_item_ids_this_rank: item['uid'] = item_id items_to_process_this_run.append(item) if rank == 0: print(f"\n========== Running Multi-Challenge Evaluation ==========\n") progress_bar = tqdm.tqdm(items_to_process_this_run, desc=f"Running Multi-Challenge Evaluation (Rank {rank})", disable=(rank != 0)) for item in progress_bar: print(f"\n----- Sample {item['uid']+1} (Rank {rank}) -----") assistant_voice_id = item['assistant_voice_id'] prompt_speech_path = os.path.join(self.prompt_root_path, assistant_voice_id + ".wav") if self.task_type == 's2s': try: speech_dialogue = item['speech_dialogue'] text_dialogue = item["CONVERSATION"] output_audio_path = str(output_dir / f"audio_{item['uid']}.wav") text_response = self.model.spoken_dialogue_sft_multiturn(speech_dialogue, text_dialogue, output_audio_path=output_audio_path, prompt_speech=prompt_speech_path) text_response = text_response.strip().split('<|eot|>')[0].replace(".....", "").replace("...", "").replace("..", "") except Exception as e: print(e) text_response = "" output_audio_path = "" print('exception in s2s dialogue') elif self.task_type == 't2t': text_dialogue = item["CONVERSATION"] text_response = self.model.text_dialogue_sft_multiturn(text_dialogue) text_response = text_response.strip().split('<|eot|>')[0].replace(".....", "").replace("...", "").replace("..", "") output_audio_path = None elif self.task_type == 's2t': speech_dialogue = item['speech_dialogue'] text_dialogue = item["CONVERSATION"] text_response = self.model.speech2text_dialogue_sft_multiturn(speech_dialogue, text_dialogue) text_response = text_response.strip().split('<|eot|>')[0].replace(".....", "").replace("...", "").replace("..", "") output_audio_path = None result_dict = {k: v for k, v in item.items() if k != 'audio'} result_dict['response'] = text_response result_dict["speech_response"] = output_audio_path if output_audio_path is not None and os.path.exists(output_audio_path): result_dict['asr_response'] = self.asr(output_audio_path) else: result_dict['asr_response'] = "" print("----------Text Response: ", text_response) print("----------Speech Response: ", output_audio_path) print("-" * 70) predictions_this_rank.append(result_dict) temp_file_path_rank = output_dir / f"predictions_rank_{rank}.json.tmp" with open(temp_file_path_rank, "w", encoding="utf-8") as f_tmp: json.dump(predictions_this_rank, f_tmp, indent=4, ensure_ascii=False) os.replace(temp_file_path_rank, predictions_file_path_rank) print(f"Rank {rank} Multi-Challenge Evaluation complete. Processed {len(items_to_process_this_run)} new items. Results saved to {predictions_file_path_rank}") def calculate_metrics(self, output_dir, rank=0, world_size=1): if rank != 0: return None if not self.thinking: output_dir = Path(output_dir) else: output_dir = Path(output_dir + "_thinking") all_predictions = [] temp_files = [] pattern = f"predictions_rank_*.json" import glob temp_file_pattern = str(output_dir / pattern) found_temp_files = glob.glob(temp_file_pattern) if not found_temp_files: print(f"Warning: No temporary files found in {output_dir} (pattern: {pattern})") return None, [] found_temp_files.sort(key=lambda x: int(Path(x).stem.split('_rank_')[-1])) for temp_file_path_str in found_temp_files: temp_file_path = Path(temp_file_path_str) temp_files.append(temp_file_path) rank_num = temp_file_path.stem.split('_rank_')[-1] if temp_file_path.exists(): with open(temp_file_path, "r", encoding="utf-8") as f: all_predictions.extend(json.load(f)) print(f"Collected rank {rank_num} results: {temp_file_path}") else: print(f"Warning: File not found: {temp_file_path}") predictions_file_path = output_dir / "results.json" with open(predictions_file_path, "w", encoding="utf-8") as f: json.dump(all_predictions, f, indent=4, ensure_ascii=False) print(f"\n========== Multi-Challenge Evaluation Summary ==========\n") print(f"Total items processed: {len(all_predictions)}") print(f"Results saved to: {predictions_file_path}") results_dict = {} total_score = 0 if 'asr_response' in all_predictions[0] and all_predictions[0]['asr_response'] is not None: for i in range(len(all_predictions)): response = all_predictions[i]['asr_response'] TARGET_QUESTION = all_predictions[i]['TARGET_QUESTION'] PASS_CRITERIA = all_predictions[i]['PASS_CRITERIA'] prompt = JUDGE_PROMPT.format(response, TARGET_QUESTION) print("----------{}---Prompt: ".format(i), prompt) try: judgement = self.evaluation_model.generate(prompt) all_predictions[i]['asr_verdict'] = judgement.verdict if judgement.verdict == PASS_CRITERIA and response != "": all_predictions[i]['asr_score'] = 1 else: all_predictions[i]['asr_score'] = 0 all_predictions[i]['asr_reasoning'] = judgement.reasoning except Exception as e: print(e) all_predictions[i]['asr_verdict'] = '' all_predictions[i]['asr_score'] = 0 all_predictions[i]['asr_reasoning'] = 'Error during evaluation' total_score += all_predictions[i]['asr_score'] results_dict['ASR average_score'] = total_score / len(all_predictions) * 100 if 'response' in all_predictions[0] and all_predictions[0]['response'] is not None: total_score = 0 for i in range(len(all_predictions)): response = all_predictions[i]['response'] TARGET_QUESTION = all_predictions[i]['TARGET_QUESTION'] PASS_CRITERIA = all_predictions[i]['PASS_CRITERIA'] prompt = JUDGE_PROMPT.format(response, TARGET_QUESTION) print("----------{}---Prompt: ".format(i), prompt) try: judgement = self.evaluation_model.generate(prompt) all_predictions[i]['verdict'] = judgement.verdict if judgement.verdict == PASS_CRITERIA and response != "": all_predictions[i]['score'] = 1 else: all_predictions[i]['score'] = 0 all_predictions[i]['reasoning'] = judgement.reasoning except Exception as e: print(e) all_predictions[i]['verdict'] = '' all_predictions[i]['score'] = 0 all_predictions[i]['reasoning'] = 'Error during evaluation' total_score += all_predictions[i]['score'] results_dict['Text average_score'] = total_score / len(all_predictions) * 100 results_dict['ASR Results'] = all_predictions with open(output_dir / ("score_4o.json"), "w", encoding="utf-8") as f: json.dump(results_dict, f, indent=4, ensure_ascii=False) return results_dict