Download inference/additional_entrypoints/call_api.py from zsy814/EchoLoc: direct link, hf CLI and curl.
- Browser
- Download file 31.6 kB
-
https://huggingface.co/zsy814/EchoLoc/resolve/main/inference/additional_entrypoints/call_api.py
- Command line
-
hf download hf://zsy814/EchoLoc/inference/additional_entrypoints/call_api.py
-
curl -L -o call_api.py https://huggingface.co/zsy814/EchoLoc/resolve/main/inference/additional_entrypoints/call_api.py
31.6 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| import argparse | |
| import base64 | |
| import json | |
| import os | |
| import re | |
| import time | |
| from collections import defaultdict | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Tuple | |
| from openai import OpenAI | |
| from tqdm import tqdm | |
| try: | |
| import matplotlib.pyplot as plt | |
| except ImportError: | |
| plt = None | |
| try: | |
| import pandas as pd | |
| except ImportError: | |
| pd = None | |
| if plt is not None: | |
| plt.rcParams.update({"font.size": 10}) | |
| SUB_CATS = { | |
| "acoustic_attributes": [ | |
| "acoustic_attributes/age", | |
| "acoustic_attributes/speed", | |
| "acoustic_attributes/gender", | |
| "acoustic_attributes/emotion", | |
| "acoustic_attributes/pitch", | |
| "acoustic_attributes/volume", | |
| "acoustic_attributes/composite_properties", | |
| ], | |
| "instruction": [ | |
| "instruction/emotion", | |
| "instruction/variation", | |
| "instruction/style", | |
| ], | |
| "role_play": [ | |
| "role_play/character", | |
| "role_play/scenario", | |
| ], | |
| "empathy": [ | |
| "empathy/anger", | |
| "empathy/sadness_disappointment", | |
| "empathy/anxiety_fear", | |
| "empathy/joy_excitement", | |
| ], | |
| } | |
| # --------------------------- Utility Functions -------------------------------- | |
| def load_prompts(prompts_dir: Path) -> Dict[str, str]: | |
| mapping = {} | |
| for file in prompts_dir.glob("*.txt"): | |
| mapping[file.stem] = file.read_text(encoding="utf-8") | |
| if not mapping: | |
| raise RuntimeError(f"No *.txt prompt files found in {prompts_dir}") | |
| return mapping | |
| def construct_prompt(template: str, instruction: str, ability: str) -> str: | |
| return template.format(instruction_type=ability, input_instruction=instruction) | |
| def debug_print_input(sample, root_dir, prompts): | |
| """跑之前调用这个,肉眼确认输入""" | |
| big_cat = sample["ability"].split("/")[0] | |
| prompt_text = construct_prompt( | |
| prompts[big_cat], | |
| instruction=sample.get("instruct_text", ""), | |
| ability=sample["ability"], | |
| ) | |
| audio_path = Path(sample.get("final_audio_path") or root_dir / sample.get("response_audio_path", "")) | |
| print("=" * 60) | |
| print(f"[id] {sample['id']}") | |
| print(f"[ability] {sample['ability']}") | |
| print(f"[instruct_text]{sample['instruct_text']}") | |
| print(f"[audio_path] {audio_path} exists={audio_path.exists()}") | |
| print(f"[prompt末尾50字]{prompt_text[-200:]}") | |
| print("=" * 60) | |
| def ensure_dir(p: Path): | |
| p.mkdir(parents=True, exist_ok=True) | |
| def safe_float(x: Any): | |
| try: | |
| return float(x) | |
| except (TypeError, ValueError): | |
| return None | |
| def normalize_score_str(x: Any) -> str: | |
| v = safe_float(x) | |
| if v is None: | |
| return "" | |
| if 1 <= v <= 5: | |
| if float(v).is_integer(): | |
| return str(int(v)) | |
| return str(v).rstrip("0").rstrip(".") | |
| return "" | |
| def parse_score(model_reply: str) -> str: | |
| if not model_reply: | |
| return "" | |
| text = str(model_reply).strip() | |
| # 1) 优先匹配 [[4]] / [[4.0]] | |
| matches = re.findall(r"\[\[\s*([0-9]+(?:\.[0-9]+)?)\s*]]", text, flags=re.S) | |
| for s in reversed(matches): | |
| score = normalize_score_str(s) | |
| if score: | |
| return score | |
| # 2) 整体就是数字 | |
| score = normalize_score_str(text) | |
| if score: | |
| return score | |
| # 3) 只匹配明确的最终分数字段,避免把 0-100 的风格强度误读成 1-5 分 | |
| matches = re.findall( | |
| r"(?:final\s+score|score|rating|gemini_score|分数|评分|最终分数)" | |
| r"\s*[::=]?\s*\[?\[?\s*([1-5](?:\.[0-9]+)?)(?!\d)\s*\]?\]?", | |
| text, | |
| flags=re.I, | |
| ) | |
| for s in reversed(matches): | |
| score = normalize_score_str(s) | |
| if score: | |
| return score | |
| return "" | |
| def encode_audio_base64(audio_path: Path) -> str: | |
| with audio_path.open("rb") as f: | |
| return base64.b64encode(f.read()).decode("utf-8") | |
| def build_retry_prompt(original_prompt: str, attempt: int) -> str: | |
| return f""" | |
| {original_prompt} | |
| IMPORTANT FORMAT REQUIREMENT: | |
| Your previous response did not contain a valid score. | |
| You MUST now output exactly ONE score between 1 and 5. | |
| Do not explain. | |
| Do not output any extra words. | |
| Preferred format: [[score]] | |
| Valid examples: | |
| [[1]] | |
| [[2]] | |
| [[3]] | |
| [[4]] | |
| [[5]] | |
| This is retry attempt #{attempt}. | |
| """.strip() | |
| def normalize_id(value: Any): | |
| if value is None: | |
| return None | |
| try: | |
| return int(value) | |
| except (TypeError, ValueError): | |
| return str(value) | |
| def load_dataset_metadata(dataset_metadata_path: Path) -> Dict[Any, Dict[str, Any]]: | |
| dataset_map: Dict[Any, Dict[str, Any]] = {} | |
| if not dataset_metadata_path or not Path(dataset_metadata_path).exists(): | |
| return dataset_map | |
| with Path(dataset_metadata_path).open(encoding="utf-8") as f: | |
| for line in f: | |
| try: | |
| d = json.loads(line) | |
| except json.JSONDecodeError: | |
| continue | |
| instruct_id = d.get("instruct_id", d.get("id")) | |
| item = { | |
| "id": d.get("id", instruct_id), | |
| "instruct_id": instruct_id, | |
| "ability": d.get("ability", ""), | |
| "instruct_text": d.get("instruct_text") or d.get("audio_content", ""), | |
| "response_audio_path": d.get("response_audio_path") or d.get("file_name", ""), | |
| } | |
| for key in (d.get("id"), instruct_id): | |
| norm = normalize_id(key) | |
| if norm is not None: | |
| dataset_map[norm] = item | |
| return dataset_map | |
| def candidate_path(root_dir: Path, path_str: Any) -> Path: | |
| if not path_str: | |
| return None | |
| path = Path(str(path_str).strip()) | |
| if path.is_absolute(): | |
| return path | |
| return root_dir / path | |
| def resolve_audio_path(root_dir: Path, sample: Dict[str, Any], dataset_entry: Dict[str, Any] = None) -> str: | |
| iid = sample.get("instruct_id", sample.get("id")) | |
| candidates = [] | |
| for key in ("final_audio_path", "response_audio_path", "file_name", "response_wav"): | |
| path = candidate_path(root_dir, sample.get(key)) | |
| if path: | |
| candidates.append(path) | |
| if dataset_entry: | |
| path = candidate_path(root_dir, dataset_entry.get("response_audio_path")) | |
| if path: | |
| candidates.append(path) | |
| if iid is not None: | |
| candidates.append(root_dir / f"{iid}.wav") | |
| seen = set() | |
| for path in candidates: | |
| key = str(path) | |
| if key in seen: | |
| continue | |
| seen.add(key) | |
| if path.exists(): | |
| return str(path) | |
| return "" | |
| # --------------------------- Proxy Gemini (OpenAI-compatible) ----------------- | |
| def call_proxy_gemini_api( | |
| prompt_text: str, | |
| response_audio_path: Path, | |
| model_name: str, | |
| max_retry: int, | |
| sleep_between_retry: int, | |
| api_key: str, | |
| base_url: str, | |
| temperature: float = 1.0, | |
| top_p: float = 0.7, | |
| max_tokens: int = 4096, | |
| verbose: bool = False, | |
| ) -> Tuple[bool, str, str]: | |
| client = OpenAI(api_key=api_key, base_url=base_url) | |
| last_err_msg = "" | |
| last_reply_text = "" | |
| audio_b64 = encode_audio_base64(response_audio_path) | |
| audio_format = response_audio_path.suffix.replace(".", "").lower() or "wav" | |
| for attempt in range(1, max_retry + 1): | |
| try: | |
| current_prompt = prompt_text if attempt == 1 else build_retry_prompt(prompt_text, attempt) | |
| content = [{"type": "text", "text": current_prompt}] | |
| content.append( | |
| { | |
| "type": "input_audio", | |
| "input_audio": { | |
| "data": audio_b64, | |
| "format": audio_format, | |
| }, | |
| } | |
| ) | |
| resp = client.chat.completions.create( | |
| model=model_name, | |
| messages=[{"role": "user", "content": content}], | |
| max_tokens=max_tokens, | |
| temperature=temperature, | |
| top_p=top_p, | |
| ) | |
| if not hasattr(resp, "choices") or not resp.choices: | |
| last_err_msg = f"Proxy API bad response: {resp}" | |
| print(f"[Attempt {attempt}/{max_retry}] bad response: {last_err_msg}") | |
| else: | |
| reply_text = resp.choices[0].message.content or "" | |
| last_reply_text = reply_text | |
| score_str = parse_score(reply_text) | |
| if verbose: | |
| print(f"[Attempt {attempt}/{max_retry}] raw reply: {reply_text[:200]}") | |
| if score_str: | |
| return True, reply_text, score_str | |
| last_err_msg = f"Unable to parse score from reply: {reply_text[:200]}" | |
| print(f"[Attempt {attempt}/{max_retry}] parse failed") | |
| except Exception as e: | |
| last_err_msg = str(e) | |
| print(f"[Attempt {attempt}/{max_retry}] API call error: {last_err_msg}") | |
| if attempt < max_retry: | |
| time.sleep(sleep_between_retry) | |
| return False, (last_reply_text or last_err_msg), "" | |
| # --------------------------- Evaluation Core -------------------------------- | |
| def evaluate_one( | |
| sample: dict, | |
| root_dir: Path, | |
| prompts: Dict[str, str], | |
| out_dir: Path, | |
| model_name: str, | |
| max_retry: int, | |
| sleep_between_retry: int, | |
| api_key: str, | |
| base_url: str, | |
| fallback_score: str, | |
| temperature: float, | |
| top_p: float, | |
| max_tokens: int, | |
| verbose: bool, | |
| ) -> dict: | |
| ability = sample["ability"] | |
| big_cat, small_cat = ability.split("/", 1) | |
| sample_model_name = sample.get("model_name", "model_eval") | |
| sample_id = sample.get("id") or sample.get("instruct_id") or "unknown_id" | |
| sample["id"] = sample_id | |
| if big_cat not in prompts: | |
| error_msg = f"Prompt template not found for category {big_cat}" | |
| sample["gemini_score"] = fallback_score | |
| sample["gemini_status"] = "fallback_prompt_missing" | |
| sample["gemini_error"] = error_msg | |
| sample["gemini_raw"] = error_msg | |
| return sample | |
| response_audio_path_str = sample.get("final_audio_path", "") | |
| if not response_audio_path_str: | |
| error_msg = f"Audio file not found for ID {sample_id} in any expected location." | |
| sample["gemini_score"] = fallback_score | |
| sample["gemini_status"] = "fallback_audio_not_found" | |
| sample["gemini_error"] = error_msg | |
| sample["gemini_raw"] = error_msg | |
| return sample | |
| response_audio_path = Path(response_audio_path_str) # 此时已经是探测好的绝对路径了 | |
| if not response_audio_path.exists(): | |
| error_msg = f"Audio file not found: {response_audio_path}" | |
| sample["gemini_score"] = fallback_score | |
| sample["gemini_status"] = "fallback_audio_not_found" | |
| sample["gemini_error"] = error_msg | |
| sample["gemini_raw"] = error_msg | |
| return sample | |
| prompt_text = construct_prompt( | |
| prompts[big_cat], | |
| instruction=sample.get("audio_content", "") or sample.get("instruct_text", ""), | |
| ability=sample["ability"], | |
| ) | |
| success, reply, score = call_proxy_gemini_api( | |
| prompt_text=prompt_text, | |
| response_audio_path=response_audio_path, | |
| model_name=model_name, | |
| max_retry=min(max_retry, 5), | |
| sleep_between_retry=sleep_between_retry, | |
| api_key=api_key, | |
| base_url=base_url, | |
| temperature=temperature, | |
| top_p=top_p, | |
| max_tokens=max_tokens, | |
| verbose=verbose, | |
| ) | |
| if success: | |
| sample["gemini_score"] = score | |
| sample["gemini_raw"] = reply | |
| sample["gemini_status"] = "success" | |
| else: | |
| sample["gemini_score"] = fallback_score | |
| sample["gemini_raw"] = reply | |
| sample["gemini_status"] = "fallback_after_5_failures" | |
| sample["gemini_error"] = f"Failed after {min(max_retry, 5)} attempts" | |
| # ← debug 日志必须放在这里,不能放在上面 | |
| debug_path = out_dir / "debug_inputs_outputs.txt" | |
| with debug_path.open("a", encoding="utf-8") as dbg: | |
| dbg.write("=" * 80 + "\n") | |
| dbg.write(f"[ID] {sample_id}\n") | |
| dbg.write(f"[ABILITY] {ability}\n") | |
| dbg.write(f"[AUDIO] {response_audio_path}\n") | |
| dbg.write(f"[INSTRUCT] {sample.get('instruct_text', '')}\n") | |
| dbg.write(f"[PROMPT]\n{prompt_text}\n") | |
| dbg.write(f"[RAW OUTPUT]\n{sample.get('gemini_raw', '')}\n") | |
| dbg.write(f"[SCORE] {sample.get('gemini_score', '')}\n") | |
| dbg.write(f"[STATUS] {sample.get('gemini_status', '')}\n") | |
| dbg.write("=" * 80 + "\n\n") | |
| save_path = ( | |
| out_dir | |
| / sample_model_name | |
| / "gemini_proxy_res" | |
| / big_cat | |
| / small_cat | |
| / f"{sample_id}.txt" | |
| ) | |
| ensure_dir(save_path.parent) | |
| save_path.write_text(sample.get("gemini_raw", ""), encoding="utf-8") | |
| return sample | |
| def load_processed_ids_and_clean_failures(scored_jsonl: Path, overwrite: bool) -> Tuple[set, int]: | |
| processed = set() | |
| cleaned_count = 0 | |
| if not scored_jsonl.exists() or overwrite: | |
| return processed, cleaned_count | |
| valid_lines = [] | |
| total_lines = 0 | |
| with scored_jsonl.open("r", encoding="utf-8") as f: | |
| for line in f: | |
| total_lines += 1 | |
| line = line.strip() | |
| if not line: | |
| continue | |
| try: | |
| rec = json.loads(line) | |
| gemini_score = rec.get("gemini_score", "") | |
| if safe_float(gemini_score) is not None: | |
| processed.add(normalize_id(rec.get("id"))) | |
| valid_lines.append(line) | |
| else: | |
| cleaned_count += 1 | |
| print(f"🗑️ Removing failed record ID {rec.get('id')}: {gemini_score}") | |
| except Exception as e: | |
| cleaned_count += 1 | |
| print(f"🗑️ Removing invalid JSON line: {str(e)}") | |
| if cleaned_count > 0: | |
| print(f"📝 Cleaning {cleaned_count} failed records from {scored_jsonl}") | |
| with scored_jsonl.open("w", encoding="utf-8") as f: | |
| for line in valid_lines: | |
| f.write(line + "\n") | |
| print(f"✅ Cleaned file saved. Kept {len(valid_lines)} valid records out of {total_lines} total lines.") | |
| return processed, cleaned_count | |
| def remove_duplicates_from_tasks(tasks: List[dict]) -> Tuple[List[dict], Dict[str, int]]: | |
| seen_ids = set() | |
| unique_tasks = [] | |
| duplicate_count = 0 | |
| duplicate_details = defaultdict(int) | |
| for task in tasks: | |
| task_id = normalize_id(task.get("id")) | |
| ability = task.get("ability", "unknown") | |
| if task_id not in seen_ids: | |
| seen_ids.add(task_id) | |
| unique_tasks.append(task) | |
| else: | |
| duplicate_count += 1 | |
| duplicate_details[ability] += 1 | |
| print(f"🔄 Removing duplicate sample ID: {task_id} (ability: {ability})") | |
| stats = { | |
| "total_duplicates": duplicate_count, | |
| "by_ability": dict(duplicate_details), | |
| } | |
| return unique_tasks, stats | |
| def eval_all_samples( | |
| root_dir: Path, | |
| metadata_path: Path, | |
| prompts_dir: Path, | |
| out_dir: Path, | |
| max_per_ability: int, | |
| concurrency: int, | |
| overwrite: bool, | |
| model_name: str, | |
| max_retry: int, | |
| sleep_between_retry: int, | |
| api_key: str, | |
| base_url: str, | |
| fallback_score: str, | |
| dataset_metadata_path: Path = None, | |
| temperature: float = 1.0, | |
| top_p: float = 0.7, | |
| max_tokens: int = 4096, | |
| verbose: bool = False, | |
| ) -> Path: | |
| prompts = load_prompts(prompts_dir) | |
| dataset_map = load_dataset_metadata(dataset_metadata_path) if dataset_metadata_path else {} | |
| if dataset_map: | |
| print(f"📚 Loaded {len(dataset_map)} entries from dataset: {dataset_metadata_path}") | |
| scored_jsonl = out_dir / "metadata_with_score.jsonl" | |
| processed_ids, _ = load_processed_ids_and_clean_failures(scored_jsonl, overwrite) | |
| tasks: List[dict] = [] | |
| per_ability_counter = defaultdict(int) | |
| skipped_missing_dataset = 0 | |
| skipped_missing_required = 0 | |
| print(f"📖 Reading samples from: {metadata_path}") | |
| with metadata_path.open(encoding="utf-8") as fin: | |
| for line in fin: | |
| try: | |
| sample = json.loads(line) | |
| except json.JSONDecodeError: | |
| continue | |
| iid = sample.get("instruct_id", sample.get("id")) | |
| iid_norm = normalize_id(iid) | |
| dataset_entry = dataset_map.get(iid_norm) if dataset_map else None | |
| # 传入官方 dataset metadata 时,用官方文本/类别覆盖 result.jsonl, | |
| # 这样评分严格对齐 VStyle 的 ground-truth instruction。 | |
| if dataset_map: | |
| if not dataset_entry: | |
| skipped_missing_dataset += 1 | |
| continue | |
| entry = dataset_entry | |
| sample["id"] = entry["id"] | |
| sample["instruct_id"] = entry["instruct_id"] | |
| sample["ability"] = entry["ability"] | |
| sample["instruct_text"] = entry["instruct_text"] | |
| else: | |
| sample["id"] = sample.get("id", iid) | |
| sample["instruct_id"] = sample.get("instruct_id", iid) | |
| sample["instruct_text"] = sample.get("instruct_text") or sample.get("audio_content", "") | |
| if not sample.get("id") or not sample.get("ability") or not sample.get("instruct_text"): | |
| skipped_missing_required += 1 | |
| continue | |
| sample["final_audio_path"] = resolve_audio_path(root_dir, sample, dataset_entry) | |
| if not str(sample.get("model_name", "")).strip(): | |
| sample["model_name"] = "unknown_model" | |
| ability = sample["ability"] | |
| if normalize_id(sample["id"]) in processed_ids: | |
| continue | |
| if per_ability_counter[ability] >= max_per_ability: | |
| continue | |
| per_ability_counter[ability] += 1 | |
| tasks.append(sample) | |
| print(f"📊 Step 1 - After initial filtering: {len(tasks)} samples") | |
| if skipped_missing_dataset: | |
| print(f"📊 Skipped {skipped_missing_dataset} samples not found in dataset metadata") | |
| if skipped_missing_required: | |
| print(f"📊 Skipped {skipped_missing_required} samples missing id/ability/instruction") | |
| print(f"📊 Step 2 - Before deduplication: {len(tasks)} samples") | |
| tasks, dup_stats = remove_duplicates_from_tasks(tasks) | |
| print(f"📊 Step 3 - After deduplication: {len(tasks)} samples (removed {dup_stats['total_duplicates']} duplicates)") | |
| # for s in tasks[:3]: | |
| # debug_print_input(s, root_dir, prompts) | |
| # input("按回车继续全量评测...") | |
| if not tasks: | |
| print("\n✅ No new samples to process, program ended.") | |
| return scored_jsonl | |
| ensure_dir(scored_jsonl.parent) | |
| write_mode = "a" if scored_jsonl.exists() and not overwrite else "w" | |
| print(f"\n🎯 Starting evaluation of {len(tasks)} samples...") | |
| print("=" * 80) | |
| with scored_jsonl.open(write_mode, encoding="utf-8") as fout, \ | |
| ThreadPoolExecutor(max_workers=concurrency) as executor, \ | |
| tqdm(total=len(tasks), desc="Evaluating") as pbar: | |
| futures = { | |
| executor.submit( | |
| evaluate_one, | |
| s, | |
| root_dir, | |
| prompts, | |
| out_dir, | |
| model_name, | |
| max_retry, | |
| sleep_between_retry, | |
| api_key, | |
| base_url, | |
| fallback_score, | |
| temperature, | |
| top_p, | |
| max_tokens, | |
| verbose, | |
| ): s | |
| for s in tasks | |
| } | |
| for fut in as_completed(futures): | |
| res = fut.result() | |
| fout.write(json.dumps(res, ensure_ascii=False) + "\n") | |
| fout.flush() | |
| pbar.update(1) | |
| print(f"\n✅ All processing complete, wrote {len(tasks)} new results -> {scored_jsonl}") | |
| return scored_jsonl | |
| # --------------------------- Analysis and Visualization ------------------------------ | |
| def get_ordered_abilities(): | |
| ordered_abilities = [] | |
| for big_cat in ["acoustic_attributes", "instruction", "role_play", "empathy"]: | |
| ordered_abilities.extend(SUB_CATS[big_cat]) | |
| return ordered_abilities | |
| def analyze_scores(scored_jsonl: Path, out_dir: Path): | |
| if pd is None or plt is None: | |
| missing = [] | |
| if pd is None: | |
| missing.append("pandas") | |
| if plt is None: | |
| missing.append("matplotlib") | |
| print(f"⚠️ Skipping score analysis/plots because missing dependency: {', '.join(missing)}") | |
| print(" Install them if you need summary plots: pip install pandas matplotlib") | |
| return | |
| records = [ | |
| json.loads(l) | |
| for l in scored_jsonl.read_text(encoding="utf-8").splitlines() | |
| if l.strip() | |
| ] | |
| rows, parse_fail_ids = [], [] | |
| for rec in records: | |
| score = safe_float(rec.get("gemini_score")) | |
| if score is None: | |
| parse_fail_ids.append(rec.get("id")) | |
| continue | |
| rows.append( | |
| { | |
| "id": rec.get("id"), | |
| "model": rec.get("model_name"), | |
| "ability": rec.get("ability"), | |
| "score": score, | |
| } | |
| ) | |
| print("len(rows):", len(rows)) | |
| if not rows: | |
| print("❌ No usable score data, script terminated.") | |
| return | |
| df = pd.DataFrame(rows) | |
| BIG_CAT_W = 0.25 | |
| WEIGHTS = {} | |
| ap_comp = "acoustic_attributes/composite_properties" | |
| ap_comp_w = BIG_CAT_W * 0.5 | |
| ap_rem_each = (BIG_CAT_W - ap_comp_w) / (len(SUB_CATS["acoustic_attributes"]) - 1) | |
| for ab in SUB_CATS["acoustic_attributes"]: | |
| WEIGHTS[ab] = ap_comp_w if ab == ap_comp else ap_rem_each | |
| for big in ("instruction", "role_play", "empathy"): | |
| each = BIG_CAT_W / len(SUB_CATS[big]) | |
| for ab in SUB_CATS[big]: | |
| WEIGHTS[ab] = each | |
| ordered_abilities = get_ordered_abilities() | |
| print("\n================= Statistical Results =================") | |
| for model, g in df.groupby("model"): | |
| print(f"\nModel: {model}") | |
| sub_means = g.groupby("ability")["score"].mean().to_dict() | |
| weighted_sum, used_w = 0.0, 0.0 | |
| for ab, w in WEIGHTS.items(): | |
| if ab in sub_means: | |
| weighted_sum += sub_means[ab] * w | |
| used_w += w | |
| weighted_mean = weighted_sum / used_w if used_w else float("nan") | |
| print(f" Weighted overall average score: {weighted_mean:.2f} (weight coverage={used_w:.1%})") | |
| print(" Major category average scores:") | |
| for big_cat in ["acoustic_attributes", "instruction", "role_play", "empathy"]: | |
| big_cat_weighted_sum = 0.0 | |
| big_cat_used_w = 0.0 | |
| big_cat_sample_count = 0 | |
| for ability in SUB_CATS[big_cat]: | |
| if ability in sub_means: | |
| weight = WEIGHTS[ability] | |
| big_cat_weighted_sum += sub_means[ability] * weight | |
| big_cat_used_w += weight | |
| big_cat_sample_count += g[g["ability"] == ability].shape[0] | |
| if big_cat_used_w > 0: | |
| big_cat_avg = big_cat_weighted_sum / big_cat_used_w | |
| coverage = big_cat_used_w / BIG_CAT_W | |
| print(f" {big_cat:<20s}: {big_cat_avg:.2f} (n={big_cat_sample_count}, coverage={coverage:.1%})") | |
| else: | |
| print(f" {big_cat:<20s}: N/A (no data)") | |
| print(" Ability average scores:") | |
| ability_means = g.groupby("ability")["score"].mean().to_dict() | |
| for ab in ordered_abilities: | |
| if ab in ability_means: | |
| cnt = g[g["ability"] == ab].shape[0] | |
| weight = WEIGHTS.get(ab, 0.0) | |
| print(f" {ab:<40s}: {ability_means[ab]:.2f} (n={cnt}, weight={weight:.3f})") | |
| print("\nStarting visualization...") | |
| for model, g in df.groupby("model"): | |
| model_dir = out_dir / model | |
| ensure_dir(model_dir) | |
| sub_means = g.groupby("ability")["score"].mean().to_dict() | |
| big_cat_scores = [] | |
| big_cat_labels = [] | |
| for big_cat in ["acoustic_attributes", "instruction", "role_play", "empathy"]: | |
| big_cat_weighted_sum = 0.0 | |
| big_cat_used_w = 0.0 | |
| for ability in SUB_CATS[big_cat]: | |
| if ability in sub_means: | |
| weight = WEIGHTS[ability] | |
| big_cat_weighted_sum += sub_means[ability] * weight | |
| big_cat_used_w += weight | |
| if big_cat_used_w > 0: | |
| big_cat_avg = big_cat_weighted_sum / big_cat_used_w | |
| big_cat_scores.append(big_cat_avg) | |
| big_cat_labels.append(big_cat.replace("_", " ").title()) | |
| if big_cat_scores: | |
| plt.figure(figsize=(10, 6)) | |
| bars = plt.bar(range(len(big_cat_scores)), big_cat_scores) | |
| plt.xticks(range(len(big_cat_labels)), big_cat_labels, rotation=45, ha="right") | |
| for bar, score in zip(bars, big_cat_scores): | |
| plt.text( | |
| bar.get_x() + bar.get_width() / 2, | |
| bar.get_height() + 0.01, | |
| f"{score:.2f}", | |
| ha="center", | |
| va="bottom", | |
| ) | |
| w_sum = sum(sub_means[ab] * w for ab, w in WEIGHTS.items() if ab in sub_means) | |
| w_used = sum(w for ab, w in WEIGHTS.items() if ab in sub_means) | |
| w_avg = w_sum / w_used if w_used else float("nan") | |
| plt.axhline(w_avg, linestyle="--", linewidth=2, label=f"Overall average: {w_avg:.2f}") | |
| plt.ylabel("Average Score") | |
| plt.title(f"{model} - Major Category Scores") | |
| plt.legend() | |
| plt.grid(True, alpha=0.3) | |
| plt.tight_layout() | |
| plt.savefig(model_dir / "major_category_scores.png", dpi=200, bbox_inches="tight") | |
| plt.close() | |
| ability_mean = g.groupby("ability")["score"].mean() | |
| ordered_data, ordered_labels = [], [] | |
| for ab in ordered_abilities: | |
| if ab in ability_mean.index: | |
| ordered_data.append(ability_mean[ab]) | |
| ordered_labels.append(ab) | |
| plt.figure(figsize=(8, 4 + 0.25 * len(ordered_data))) | |
| y_pos = range(len(ordered_data)) | |
| plt.barh(y_pos, ordered_data) | |
| plt.yticks(y_pos, ordered_labels) | |
| sub_means = g.groupby("ability")["score"].mean().to_dict() | |
| w_sum = sum(sub_means[ab] * w for ab, w in WEIGHTS.items() if ab in sub_means) | |
| w_used = sum(w for ab, w in WEIGHTS.items() if ab in sub_means) | |
| w_avg = w_sum / w_used if w_used else float("nan") | |
| plt.axvline(w_avg, linestyle="--", label="Weighted overall average") | |
| plt.xlabel("Average score") | |
| plt.title(f"{model} - Ability average scores") | |
| plt.legend() | |
| plt.tight_layout() | |
| plt.savefig(model_dir / "ability_scores.png", dpi=200) | |
| plt.close() | |
| print(f"✅ Visualization complete, saved to folder: {out_dir}") | |
| print("\n================= Parse Failure Information =================") | |
| if parse_fail_ids: | |
| print(f"Total of {len(parse_fail_ids)} records failed gemini_score parsing.") | |
| else: | |
| print("All records' gemini_score parsed successfully.") | |
| # ----------------------------- CLI ---------------------------------- | |
| def main(): | |
| root = "./data/examples" | |
| parser = argparse.ArgumentParser( | |
| description="Concurrent proxy scoring for voice results with forced score retries" | |
| ) | |
| parser.add_argument("--root_dir", default=f"{root}/model_res/en/wav", help="wav directory path") | |
| parser.add_argument("--metadata_path", default=f"{root}/model_res/en/metadata.jsonl", help="metadata.jsonl path") | |
| parser.add_argument("--prompts_dir", default="lalm_eval/eval_prompts/en", help="Directory containing prompt templates") | |
| parser.add_argument("--out_dir", default=f"{root}/eval_res/en", help="Output root directory") | |
| parser.add_argument("--temperature", type=float, default=1.0) | |
| parser.add_argument("--top_p", type=float, default=0.7) | |
| parser.add_argument("--max_tokens", type=int, default=4096) | |
| parser.add_argument( | |
| "--api_key", | |
| default=os.environ.get("VSTYLE_API_KEY") or os.environ.get("OPENAI_API_KEY"), | |
| help="Proxy API Key; defaults to VSTYLE_API_KEY or OPENAI_API_KEY", | |
| ) | |
| parser.add_argument( | |
| "--base_url", | |
| default=os.environ.get("VSTYLE_BASE_URL"), | |
| help="Proxy base_url, e.g. https://xxx/v1; defaults to VSTYLE_BASE_URL", | |
| ) | |
| parser.add_argument( | |
| "--model_name", | |
| default=os.environ.get("VSTYLE_JUDGE_MODEL", "gemini-2.5-pro"), | |
| help="Model name on proxy; defaults to VSTYLE_JUDGE_MODEL or gemini-2.5-pro", | |
| ) | |
| parser.add_argument( | |
| "--max_retry_api", | |
| type=int, | |
| default=5, | |
| help="Maximum number of retry attempts, capped at 5 in scoring logic", | |
| ) | |
| parser.add_argument( | |
| "--sleep_between_retry", | |
| type=int, | |
| default=5, | |
| help="Sleep time between retries (seconds)", | |
| ) | |
| parser.add_argument( | |
| "--max_per_ability", | |
| type=int, | |
| default=100000, | |
| help="Maximum number of evaluations per ability", | |
| ) | |
| parser.add_argument("--concurrency", type=int, default=4, help="Number of concurrent threads") | |
| parser.add_argument("--overwrite", action="store_true", help="Overwrite existing results") | |
| parser.add_argument( | |
| "--fallback_score", | |
| type=str, | |
| default="3", | |
| help="Fallback score if 5 attempts all fail; should be between 1 and 5", | |
| ) | |
| parser.add_argument( | |
| "--dataset_metadata_path", | |
| type=str, | |
| default=None, | |
| help="原始 VStyle 数据集的 metadata.jsonl 路径,用于补充 instruct_text", | |
| ) | |
| parser.add_argument("--verbose", action="store_true", help="Print raw judge replies during scoring") | |
| args = parser.parse_args() | |
| if not args.api_key: | |
| parser.error("--api_key is required, or set VSTYLE_API_KEY / OPENAI_API_KEY") | |
| if not args.base_url: | |
| parser.error("--base_url is required, or set VSTYLE_BASE_URL") | |
| if normalize_score_str(args.fallback_score) == "": | |
| raise ValueError("--fallback_score must be a number between 1 and 5") | |
| args.fallback_score = normalize_score_str(args.fallback_score) | |
| print(f"args: {args}") | |
| out_dir = Path(args.out_dir) | |
| ensure_dir(out_dir) | |
| scored_jsonl = eval_all_samples( | |
| root_dir=Path(args.root_dir), | |
| metadata_path=Path(args.metadata_path), | |
| prompts_dir=Path(args.prompts_dir), | |
| out_dir=out_dir, | |
| max_per_ability=args.max_per_ability, | |
| concurrency=args.concurrency, | |
| overwrite=args.overwrite, | |
| model_name=args.model_name, | |
| max_retry=args.max_retry_api, | |
| sleep_between_retry=args.sleep_between_retry, | |
| api_key=args.api_key, | |
| base_url=args.base_url, | |
| fallback_score=args.fallback_score, | |
| dataset_metadata_path=args.dataset_metadata_path, | |
| temperature=args.temperature, | |
| top_p=args.top_p, | |
| max_tokens=args.max_tokens, | |
| verbose=args.verbose, | |
| ) | |
| analyze_scores(scored_jsonl, out_dir) | |
| if __name__ == "__main__": | |
| main() | |