CoolFace
Apppublic

ManojINaik/TheoremExplainAgent

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
evaluate.py475 linesDownload Raw Back to root
1import os2import json3import argparse4import tempfile5from typing import Dict, List, Union6from datetime import datetime7 8from dotenv import load_dotenv9from moviepy import VideoFileClip10 11from mllm_tools.litellm import LiteLLMWrapper12from mllm_tools.gemini import GeminiWrapper13from eval_suite.utils import calculate_geometric_mean14from eval_suite.text_utils import parse_srt_to_text, fix_transcript, evaluate_text15from eval_suite.video_utils import evaluate_video_chunk_new16from eval_suite.image_utils import evaluate_sampled_images17 18load_dotenv()19 20with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "src", "utils", "allowed_models.json")) as f:21    ALLOWED_MODELS = json.load(f)["allowed_models"]22 23 24def combine_results(output_folder: str, combined_file: str, results: Dict[str, Dict]) -> None:25    """26    Combine all evaluation results into a single file.27 28    Args:29        output_folder (str): Directory to store the combined file.30        combined_file (str): Name of the combined file.31        results (Dict[str, Dict]): Dictionary of evaluation results with file names as keys.32 33    Returns:34        None35    """36    combined_path = os.path.join(output_folder, combined_file)37    with open(combined_path, 'w') as output_file:38        json.dump(results, output_file, indent=4)39 40 41def save_individual_result(output_folder: str, file_name: str, result: Dict) -> None:42    """43    Save individual evaluation result to a file.44 45    Args:46        output_folder (str): Directory to store the evaluation file.47        file_name (str): Name of the file.48        result (Dict): Evaluation result.49 50    Returns:51        None52    """53    current_time = datetime.now().strftime("%Y%m%d_%H%M%S")54    result_file = f"evaluation_{file_name}_{current_time}.json"55    os.makedirs(output_folder, exist_ok=True)56    result_path = os.path.join(output_folder, result_file)57    with open(result_path, 'w') as output_file:58        json.dump(result, output_file, indent=4)59 60 61def evaluate_text_file(model, transcript_path, retry_limit):62    """63    Evaluate a text file using the provided model.64 65    Args:66        model: The model to use for evaluation.67        transcript_path (str): Path to the transcript file (.srt or .txt).68        retry_limit (int): Number of retry attempts for evaluation.69 70    Returns:71        Dict or None: Evaluation results if successful, None if file format unsupported.72    """73    if not transcript_path.endswith(('.srt', '.txt')):74        print(f"Skipping {transcript_path}: Unsupported file format for text evaluation.")75        return None76 77    if transcript_path.endswith(".srt"):78        transcript = parse_srt_to_text(transcript_path)79    elif transcript_path.endswith(".txt"):80        with open(transcript_path) as f:81            transcript = f.read().strip()82    else:83        raise ValueError("Unrecognized transcript file format.")84 85    capital_letter_proportion = sum(1 for c in transcript if c.isupper()) / sum(1 for c in transcript if c.isalpha())86    if capital_letter_proportion < 0.01:87        transcript = fix_transcript(model, transcript)88 89    print(f"Performing text evaluation: {os.path.basename(transcript_path)}")90    result = evaluate_text(model, transcript, retry_limit)91    return result92 93 94def evaluate_video_file(model, video_path, transcript_path, description_path, target_fps=None, output_folder=None):95    """96    Evaluate a video file using the provided model.97 98    Args:99        model: The model to use for evaluation.100        video_path (str): Path to the video file.101        transcript_path (str): Path to the transcript file.102        description_path (str): Path to the description file.103        target_fps (int, optional): Target frames per second for video processing.104        output_folder (str, optional): Directory to store output files.105 106    Returns:107        Dict or None: Evaluation results if successful, None if file format unsupported.108    """109    if not video_path.endswith(('.mp4', '.mkv')):110        print(f"Skipping {video_path}: Unsupported file format for video evaluation.")111        return None112 113    moviepy_temp_dir = os.path.join(output_folder, "moviepy_temp")114 115    # Chunking116    num_chunks = 10117    with VideoFileClip(video_path) as clip:118        duration = clip.duration119        chunk_duration = duration / num_chunks120        results = []121        122        # Create a temporary directory in the output_folder123        temp_dir_parent = output_folder or os.getcwd()124        with tempfile.TemporaryDirectory(dir=temp_dir_parent) as temp_dir:125            for i in range(10):126                start = i * chunk_duration127                end = min(start + chunk_duration, duration)128                chunk = clip.subclipped(start, end)129                chunk_path = os.path.join(temp_dir, f"chunk_{i+1}.mp4")130                # Explicitly set the temp_audiofile path with matching codec131                temp_audiofile = os.path.join(moviepy_temp_dir, f"temp_audio_chunk_{i+1}.m4a")132                chunk.write_videofile(133                    chunk_path,134                    codec="libx264",135                    audio_codec="aac",136                    temp_audiofile=temp_audiofile,137                    audio_bitrate="192k",138                    preset="ultrafast",  # Speed up encoding139                    logger=None140                )141                # Create processed videos folder inside output_folder142                processed_videos_dir = os.path.join(output_folder, "processed_videos")143                save_path = os.path.join(processed_videos_dir, f"processed_chunk_{i+1}.mp4")144                result = evaluate_video_chunk_new(145                    model,146                    chunk_path,147                    transcript_path,148                    description_path,149                    target_fps=target_fps,150                    save_processed_video=save_path151                )152                results.append(result)153 154    score_dict = {}155    for key in results[0]["evaluation"].keys():156        score_dict[key] = []157        for result in results:158            score_dict[key].append(result["evaluation"][key]["score"])159 160    evaluation = {}161    for key, scores in score_dict.items():162        evaluation[key] = {"score": calculate_geometric_mean(scores)}163 164    result_json = {165        "evaluation": evaluation,166        "video_chunks": results167    }168    return result_json169 170 171def extract_scores(data: Union[Dict, List]) -> List[int]:172    """173    Extract all score values from a nested dictionary or list structure.174 175    Args:176        data (Union[Dict, List]): The data structure to extract scores from.177 178    Returns:179        List[int]: List of extracted score values.180    """181    scores = []182    if isinstance(data, dict):183        for key, value in data.items():184            if "chunks" in key:185                continue186            elif isinstance(value, dict) or isinstance(value, list):187                scores.extend(extract_scores(value))188            elif key == 'score':189                scores.append(value)190    elif isinstance(data, list):191        for item in data:192            scores.extend(extract_scores(item))193    return scores194 195 196def calculate_overall_score(result: Dict) -> float:197    """198    Calculate the overall score from evaluation results.199 200    Args:201        result (Dict): Dictionary containing evaluation results.202 203    Returns:204        float: The calculated overall score.205    """206    scores = extract_scores(result)207    overall_score = calculate_geometric_mean(scores)208    return overall_score209 210 211def process_topic_name(topic_name: str) -> str:212    """213    Process a topic name by capitalizing words and handling special characters.214 215    Args:216        topic_name (str): The topic name to process.217 218    Returns:219        str: The processed topic name.220    """221    words = topic_name.replace("_s_", "'s_").split("_")222    return " ".join([word.capitalize() for word in words])223 224 225def merge_dicts(dict1: dict, dict2: dict) -> dict:226    """227    Recursively merge two dictionaries.228 229    Args:230        dict1 (dict): First dictionary.231        dict2 (dict): Second dictionary.232 233    Returns:234        dict: Merged dictionary.235    """236    merged = dict1.copy()237    for key, value in dict2.items():238        if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):239            merged[key] = merge_dicts(merged[key], value)240        else:241            merged[key] = value242    return merged243 244 245def process_theorem(models, file_path: str, eval_type: str, retry_limit: int,246                    target_fps: int = None, use_parent_folder_as_topic: bool = False,247                    output_folder: str = None) -> tuple[str, dict]:248    """249    Process a theorem file or directory for evaluation.250 251    Args:252        models: Dictionary of models for different evaluation types.253        file_path (str): Path to the file or directory to evaluate.254        eval_type (str): Type of evaluation to perform.255        retry_limit (int): Number of retry attempts.256        target_fps (int, optional): Target frames per second for video processing.257        use_parent_folder_as_topic (bool, optional): Use parent folder name as topic.258        output_folder (str, optional): Directory to store output files.259 260    Returns:261        tuple[str, dict]: Tuple of file name and evaluation results.262    """263    ext_map = {264        'text': ('.txt', '.srt'),265        'video': ('.mp4', '.mkv')266    }267 268    # Handle single file evaluation269    if os.path.isfile(file_path):270        file_ext = os.path.splitext(file_path)[1].lower()271        file_name = os.path.basename(file_path)272 273        if eval_type == "text" and file_ext in ext_map['text']:274            return file_name, evaluate_text_file(models['text'], file_path, retry_limit)275        elif eval_type == "video" and file_ext in ext_map['video']:276            if use_parent_folder_as_topic:277                topic_name = os.path.basename(os.path.dirname(file_path))278            else:279                topic_name = None280            topic_name = process_topic_name(topic_name)281            return file_name, evaluate_video_file(models['video'], file_path, None, topic_name, target_fps, output_folder)282        elif eval_type == "image" and file_ext in ext_map['video']:283            if use_parent_folder_as_topic:284                topic_name = os.path.basename(os.path.dirname(file_path))285            else:286                topic_name = None287            topic_name = process_topic_name(topic_name)288            return file_name, evaluate_sampled_images(models['image'], file_path, topic_name, num_chunks=10, output_folder=output_folder)289        elif eval_type == "all":290            raise ValueError("Evaluation type 'all' is not supported for a single file. Try passing a folder with both a video and a subtitle file.")291        else:292            raise ValueError(f"File type of {file_path} does not match evaluation type {eval_type!r}")293 294    # Handle directory evaluation295    theorem_dir = file_path296    all_files = os.listdir(theorem_dir)297 298    # Look for transcript files, prioritizing .srt over .txt if both exist299    transcript_file_candidates = [f for f in all_files if f.endswith(ext_map['text']) and not f.endswith('_scene_outline.txt')]300    srt_files = [f for f in transcript_file_candidates if f.endswith('.srt')]301    txt_files = [f for f in transcript_file_candidates if f.endswith('.txt')]302 303    transcript_path = None304    if srt_files:305        transcript_path = os.path.join(theorem_dir, srt_files[0])306    elif txt_files:307        transcript_path = os.path.join(theorem_dir, txt_files[0])308 309    video_file_candidates = [f for f in all_files if f.endswith(ext_map['video'])]310    video_path = os.path.join(theorem_dir, video_file_candidates[0]) if len(video_file_candidates) == 1 else None311 312    topic_name = os.path.basename(theorem_dir)313    topic_name = process_topic_name(topic_name)314 315    if not video_path:316        print(f"Skipping {theorem_dir}: No video file found")317        return None, None318 319    text_result = video_result = image_result = None320    if eval_type == "text" or eval_type == "all":321        if transcript_path is None:322            print(f"Warning: No suitable transcript file found in {theorem_dir}")323        else:324            text_result = evaluate_text_file(models['text'], transcript_path, retry_limit)325    if eval_type == "video" or eval_type == "all":326        assert video_path is not None, f"Expected 1 video file, got {len(video_file_candidates)} for {theorem_dir}"327        video_result = evaluate_video_file(models['video'], video_path, transcript_path, topic_name, target_fps, output_folder)328    if eval_type == "image" or eval_type == "all":329        assert video_path is not None, f"Expected 1 video file, got {len(video_file_candidates)} for {theorem_dir}"330        image_result = evaluate_sampled_images(models['image'], video_path, topic_name, num_chunks=10, output_folder=output_folder)331    332    if eval_type == "all":333        result = {}334        if text_result:335            result = merge_dicts(result, text_result)336        if video_result:337            result = merge_dicts(result, video_result)338        if image_result:339            result = merge_dicts(result, image_result)340        if result:341            result["evaluation"]["overall_score"] = calculate_overall_score(result)342    else:343        result = text_result if eval_type == "text" else video_result if eval_type == "video" else image_result if eval_type == "image" else None344 345    file_name = os.path.basename(theorem_dir)346    return file_name, result347 348 349def main():350    """351    Main function to run the evaluation script.352 353    Parses command line arguments and orchestrates the evaluation process354    for text, video, and image content using specified AI models.355    """356    parser = argparse.ArgumentParser(description='Automatic evaluation of theorem explanation videos with LLMs')357    parser.add_argument('--model_text', type=str, 358                       choices=ALLOWED_MODELS,359                       default='azure/gpt-4o',360                       help='Select the AI model to use for text evaluation')361    parser.add_argument('--model_video', type=str,362                       choices=['gemini/gemini-1.5-pro-002',363                                'gemini/gemini-2.0-flash-exp',364                                'gemini/gemini-2.0-pro-exp-02-05'],365                       default='gemini/gemini-1.5-pro-002',366                       help='Select the AI model to use for video evaluation')367    parser.add_argument('--model_image', type=str,368                       choices=ALLOWED_MODELS,369                       default='azure/gpt-4o',370                       help='Select the AI model to use for image evaluation')371    parser.add_argument('--eval_type', type=str, choices=['text', 'video', 'image', 'all'], default='all', help='Type of evaluation to perform')372    parser.add_argument('--file_path', type=str, help='Path to a file or a theorem folder', required=True)373    parser.add_argument('--output_folder', type=str, help='Directory to store the evaluation files', required=True)374    parser.add_argument('--retry_limit', type=int, default=3, help='Number of retry attempts for each inference')375    parser.add_argument('--combine', action='store_true', help='Combine all results into a single JSON file')376    parser.add_argument('--bulk_evaluate', action='store_true', help='Evaluate a folder of theorems together', default=False)377    parser.add_argument('--target_fps', type=int, help='Target FPS for video processing. If not set, original video FPS will be used', required=False)378    parser.add_argument('--use_parent_folder_as_topic', action='store_true', help='Use parent folder name as topic name for single file evaluation', default=True)379    parser.add_argument('--max_workers', type=int, default=4, help='Maximum number of concurrent workers for parallel processing')380 381    args = parser.parse_args()382 383    # Initialize separate models384    text_model = LiteLLMWrapper(385        model_name=args.model_text,386        temperature=0.0,387    )388    video_model = GeminiWrapper(389        model_name=args.model_video,390        temperature=0.0,391    )392    image_model = LiteLLMWrapper(393        model_name=args.model_image,394        temperature=0.0,395    )396 397    models = {398        'text': text_model,399        'video': video_model,400        'image': image_model401    }402 403    theorem_dirs = []404    if args.bulk_evaluate:405        assert os.path.isdir(args.file_path), "File path must be a folder for --bulk_evaluate"406        for root, dirnames, _ in os.walk(args.file_path):407            if not any(f.endswith(".mp4") for f in os.listdir(root)):408                continue409 410            theorem_dirs.append(root)411    elif os.path.isdir(args.file_path):412        assert any(f.endswith(".mp4") for f in os.listdir(args.file_path)), "The provided folder must contain a video file"413 414        theorem_dirs.append(args.file_path)415 416    # Create output directory and its temp subdirectories if it doesn't exist417    os.makedirs(args.output_folder, exist_ok=True)418    moviepy_temp_dir = os.path.join(args.output_folder, "moviepy_temp")419    os.makedirs(moviepy_temp_dir, exist_ok=True)420    VideoFileClip.DEFAULT_TEMP_DIR = moviepy_temp_dir421 422    processed_videos_dir = os.path.join(args.output_folder, "processed_videos")423    os.makedirs(processed_videos_dir, exist_ok=True)424 425    results = {}426    if theorem_dirs:427        for theorem_dir in theorem_dirs:428            file_name, result = process_theorem(429                models,430                theorem_dir,431                args.eval_type,432                args.retry_limit,433                args.target_fps,434                args.use_parent_folder_as_topic,435                args.output_folder436            )437 438            if result is not None:439                results[file_name] = result440 441                if not args.combine:442                    save_individual_result(args.output_folder, file_name, result)443    else:444        file_name, result = process_theorem(445            models, 446            args.file_path, 447            args.eval_type, 448            args.retry_limit,449            args.target_fps,450            args.use_parent_folder_as_topic,451            args.output_folder452        )453        454        if result is not None:455            results[file_name] = result456 457            if not args.combine:458                save_individual_result(args.output_folder, file_name, result)459 460    if args.combine:461        if len(results) > 1:462            current_time = datetime.now().strftime("%Y%m%d_%H%M%S")463            combined_file = f"evaluation_{current_time}.json"464            combine_results(args.output_folder, combined_file, results)465            print("Combining results completed.")466        else:467            for file_name, result in results.items():468                save_individual_result(args.output_folder, file_name, result)469    470    os.rmdir(moviepy_temp_dir)471 472 473if __name__ == "__main__":474    main()475