llm_judge.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import os, openai, json, functools, argparse
  2. try:
  3. from datasets import load_dataset # optional, only needed by some official flows
  4. except ImportError:
  5. load_dataset = None
  6. try:
  7. from proactvl.utils.multiprocessor import local_mt
  8. except ImportError:
  9. try:
  10. from proactvl_eval.multiprocessor import local_mt
  11. except ImportError:
  12. from multiprocessor import local_mt
  13. # ========================= Initialize GPT Client =========================
  14. try:
  15. from evaluation.gaming.gpt_client_utils import GPTClientFactory
  16. except ImportError:
  17. try:
  18. from proactvl_eval.gpt_client_utils import GPTClientFactory
  19. except ImportError:
  20. from gpt_client_utils import GPTClientFactory
  21. # deployment_name = "qwen/qwen3.6-plus"
  22. deployment_name = os.getenv("OPENAI_JUDGE_MODEL", "gpt-5.1_2025-11-13")
  23. try:
  24. gpt = GPTClientFactory().build()
  25. except Exception as e:
  26. raise RuntimeError(f"Error: failed to initialize GPT client. {e}") from e
  27. def judge_ab(a_id_with_pred, b_id_with_pred, gt_asr):
  28. a_id, a_pred = a_id_with_pred
  29. b_id, b_pred = b_id_with_pred
  30. ab_prompt = (
  31. 'You are an expert in video commentary. '
  32. 'Your task is to review two commentaries (Commentary A and Commentary B), and select the one that better aligns with the human commentary. '
  33. 'You should consider the criteria:\n'
  34. '1. Semantic Alignment: The commentary should convey the same meaning, details, and key points as the human commentary.\n'
  35. 'If the above criteria is not enough to judge, then consider:\n'
  36. '2. Stylistic Consistency: The commentary should maintain a tone, word choice, and structure similar to the human commentary.\n'
  37. f'\n---Commentary A---\n{a_pred}\n----------\n'
  38. f'\n---Commentary B---\n{b_pred}\n----------\n'
  39. f'\n---Human Commentary---\n{gt_asr}\n----------\n'
  40. '\nYour response should be "Commentary A is better aligned with the human commentary" or "Commentary B is better aligned with the human commentary".\n'
  41. )
  42. while True:
  43. try:
  44. ab_resp = gpt.chat.completions.create(
  45. # model='gpt-4o-2024-08-06',
  46. # model='gpt-4.1',
  47. model=deployment_name,
  48. messages=[{"role": "user", "content": [{'type': 'text', 'text': ab_prompt}]}],
  49. seed=42,
  50. temperature=0,
  51. ).choices[0].message.content
  52. break
  53. except Exception as e:
  54. print('Failed to get response...', e)
  55. if 'Commentary A' in ab_resp:
  56. ab_winner = a_id
  57. elif 'Commentary B' in ab_resp:
  58. ab_winner = b_id
  59. else:
  60. ab_winner = 'tie'
  61. # print(ab_winner)
  62. return ab_winner
  63. def judge(item, model_id):
  64. # print('--------call judge once--------')
  65. video_event_id, model_pred = item
  66. gt_asr = video_event_id_to_gt_asr[video_event_id]
  67. baseline_pred = video_event_id_to_baseline_pred[video_event_id]
  68. dataset_name = video_event_id_to_dataset_name[video_event_id]
  69. tag = video_event_id_to_tag[video_event_id]
  70. return {
  71. 'video_event_id': video_event_id,
  72. 'ab_winner': judge_ab([model_id, model_pred], [baseline_id, baseline_pred], gt_asr),
  73. 'ba_winner': judge_ab([baseline_id, baseline_pred], [model_id, model_pred], gt_asr),
  74. 'dataset_name': dataset_name,
  75. 'tag': tag
  76. }
  77. def merge_commentary(commentary_dict):
  78. sorted_seconds = sorted(commentary_dict.keys(), key=int)
  79. merged_commentary = ' '.join([commentary_dict[sec].replace(' ...', '') for sec in sorted_seconds])
  80. return merged_commentary.strip()
  81. def calculate_proactive_metrics(label_dict, pred_dict, video_duration):
  82. label_events = set(label_dict.keys())
  83. pred_events = set(pred_dict.keys())
  84. true_positives = len(label_events.intersection(pred_events))
  85. false_positives = len(pred_events - label_events)
  86. false_negatives = len(label_events - pred_events)
  87. proactive_rate = true_positives / video_duration
  88. precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0.0
  89. recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0.0
  90. return proactive_rate, precision, recall
  91. def pred2standard(pred):
  92. if type(pred) == str:
  93. return pred, pred
  94. sorted_seconds = sorted(pred.keys(), key=int)
  95. # print(sorted_seconds)
  96. asrs = []
  97. asr_lines = []
  98. for sec in sorted_seconds:
  99. line = pred[sec]
  100. # Trim trailing ' ...'
  101. if line.endswith(' ...'):
  102. line = line[:-4]
  103. if line.strip() == '':
  104. continue
  105. else:
  106. asr_lines.append(f'[t={sec}] {line}')
  107. asrs.append(line)
  108. return '\n'.join(asr_lines), ' '.join(asrs)
  109. def gt2standard(annotations):
  110. active_speaker = annotations['active_speaker']['name']
  111. asrs = []
  112. asr_lines = []
  113. for item in annotations['annotations']:
  114. # if item['speaker'] != active_speaker:
  115. # continue
  116. sec = item['start']
  117. line = item['text'] if 'text' in item else item['query']
  118. if line.strip() == '':
  119. continue
  120. else:
  121. if item['speaker'] == active_speaker:
  122. asrs.append(line)
  123. asr_lines.append(f'[t={sec}][ASSISTANT]:{line}')
  124. elif item['speaker'] == 'user':
  125. asr_lines.append(f'[t={sec}][USER]:{line}')
  126. else:
  127. asr_lines.append(f'[t={sec}][{item["speaker"]}]:{line}')
  128. return '\n'.join(asr_lines), ' '.join(asrs)
  129. if __name__ == '__main__':
  130. parser = argparse.ArgumentParser()
  131. parser.add_argument('--model_id', type=str, required=True, help='Model name to compare against baseline')
  132. parser.add_argument('--prediction_jsonl', type=str, required=True, help='Path to model predictions in JSONL format')
  133. parser.add_argument('--output_dir', type=str, default='evaluation/gaming/judges/', help='Directory to save judgment results')
  134. parser.add_argument('--num_workers', type=int, default=1)
  135. parser.add_argument('--baseline_id', type=str, default='gpt-4o-3', help='Baseline model name')
  136. parser.add_argument('--baseline_jsonl', type=str, default='evaluation/gaming/captions/gpt-4o-3-caption.jsonl', help='Path to baseline predictions in JSONL format')
  137. parser.add_argument('--asr_jsonl', type=str, default=None, help='Path to ASR transcriptions in JSONL format')
  138. parser.add_argument('--max_video_length', type=int, default=86400, help='Maximum video length to consider for evaluation (in seconds)')
  139. # parser.add_argument('--dataset_name_list', type=str, nargs='+', default=['lol'], help='List of dataset names to evaluate on')
  140. args = parser.parse_args()
  141. model_id = args.model_id
  142. prediction_jsonl = args.prediction_jsonl
  143. output_dir = args.output_dir
  144. baseline_id = args.baseline_id
  145. baseline_jsonl = args.baseline_jsonl
  146. os.makedirs(output_dir, exist_ok=True)
  147. save_path = os.path.join(output_dir, f'{baseline_id}_{model_id}.jsonl')
  148. print(f'{model_id} vs. {baseline_id}')
  149. # prediction_jsonl = 'evaluation/gaming/gaming/Qwen2.5-Omni-7B.jsonl'
  150. prediction_jsonl = args.prediction_jsonl
  151. asr_jsonl = args.asr_jsonl
  152. if args.asr_jsonl is None:
  153. asr_jsonl = prediction_jsonl
  154. else:
  155. asr_jsonl = args.asr_jsonl
  156. video_event_id_to_baseline_pred, video_event_id_to_gt_asr, video_start_end_to_video_event_id, video_event_id_to_model_pred = {}, {}, {}, {}
  157. video_event_id_to_dataset_name = {}
  158. video_event_id_to_tag = {}
  159. for line in open(baseline_jsonl):
  160. datum = json.loads(line)
  161. dataset_name = datum['dataset_name']
  162. if datum['end'] > args.max_video_length:
  163. continue
  164. # if datum['dataset_name'] not in args.dataset_name_list:
  165. # continue
  166. video_event_id = f'{datum["video_id"]}_{datum["begin"]}_{datum["end"]}_{dataset_name}_{datum["idx"]}'
  167. video_start_end = (datum['video_id'], datum['begin'], datum['end'])
  168. video_start_end_to_video_event_id[video_start_end] = video_event_id
  169. video_event_id_to_baseline_pred[video_event_id] = datum['pred']
  170. video_event_id_to_dataset_name[video_event_id] = datum['dataset_name']
  171. video_event_id_to_tag[video_event_id] = datum.get('tag', None)
  172. for line in open(prediction_jsonl):
  173. datum = json.loads(line)
  174. dataset_name = datum['dataset_name']
  175. if datum['end'] > args.max_video_length:
  176. continue
  177. # if datum['tag'] not in ['Solo commentators', "Multiple commentators", 'Guidance']:
  178. # continue
  179. # if datum['dataset_name'] not in args.dataset_name_list:
  180. # continue
  181. video_event_id = f'{datum["video_id"]}_{datum["begin"]}_{datum["end"]}_{dataset_name}_{datum["idx"]}'
  182. if video_event_id in video_event_id_to_model_pred:
  183. print(f'Warning: duplicate video_event_id {video_event_id} in predictions')
  184. video_event_id_to_model_pred[video_event_id] = pred2standard(datum['pred'])[1]
  185. # if type(datum['pred']) == str:
  186. # video_event_id_to_model_pred[video_event_id] = datum['pred']
  187. # else:
  188. # video_event_id_to_model_pred[video_event_id] = merge_commentary(datum['pred'])
  189. for line in open(asr_jsonl):
  190. datum = json.loads(line)
  191. if datum['video_end'] > args.max_video_length:
  192. continue
  193. video_event_id = f'{os.path.basename(datum["video_path"])}_{datum["video_begin"]}_{datum["video_end"]}_{datum["dataset_name"]}_{datum["idx"]}'
  194. # video_event_id_to_gt_asr[video_event_id] = merge_commentary(datum['label'])
  195. video_event_id_to_gt_asr[video_event_id] = gt2standard(datum)[1]
  196. winner_results = local_mt(
  197. video_event_id_to_model_pred.items(),
  198. functools.partial(judge, model_id=model_id),
  199. desc=f'Call gpt-5.1_2025-11-13 for {model_id} vs. {baseline_id}',
  200. num_workers=args.num_workers
  201. )
  202. with open(save_path, 'w') as f:
  203. for winner_result in winner_results:
  204. f.write(json.dumps(winner_result) + '\n')
  205. # Report win rates by game, by tag, and overall.
  206. summary_by_game = {}
  207. for winner_result in winner_results:
  208. dataset_name = winner_result['dataset_name']
  209. if dataset_name not in summary_by_game:
  210. summary_by_game[dataset_name] = {'win_count': 0, 'count': 0}
  211. if winner_result['ab_winner'] == model_id:
  212. summary_by_game[dataset_name]['win_count'] += 1
  213. if winner_result['ba_winner'] == model_id:
  214. summary_by_game[dataset_name]['win_count'] += 1
  215. summary_by_game[dataset_name]['count'] += 2
  216. for dataset_name, summary in summary_by_game.items():
  217. win_rate = summary['win_count'] / summary['count'] * 100
  218. output = f'Winning Rate for {model_id} vs. {baseline_id} on {dataset_name}: {win_rate:.2f}%'
  219. print(output)
  220. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  221. f.write(output + '\n')
  222. summary_by_tag = {}
  223. for winner_result in winner_results:
  224. tag = winner_result['tag']
  225. if tag not in summary_by_tag:
  226. summary_by_tag[tag] = {'win_count': 0, 'count': 0}
  227. if winner_result['ab_winner'] == model_id:
  228. summary_by_tag[tag]['win_count'] += 1
  229. if winner_result['ba_winner'] == model_id:
  230. summary_by_tag[tag]['win_count'] += 1
  231. summary_by_tag[tag]['count'] += 2
  232. for tag, summary in summary_by_tag.items():
  233. win_rate = summary['win_count'] / summary['count'] * 100
  234. output = f'Winning Rate for {model_id} vs. {baseline_id} on tag {tag}: {win_rate:.2f}%'
  235. print(output)
  236. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  237. f.write(output + '\n')
  238. win_count, count = 0, 0
  239. for winner_result in winner_results:
  240. if winner_result['ab_winner'] == model_id:
  241. win_count += 1
  242. if winner_result['ba_winner'] == model_id:
  243. win_count += 1
  244. count += 2
  245. win_rate = win_count / count * 100
  246. output = f'Winning Rate for {model_id} vs. {baseline_id}: {win_rate:.2f}%'
  247. print(output)
  248. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  249. f.write(output + '\n')
  250. # python -m evaluation.gaming.llm_judge --model_id qwen25omni_7game_s3156_threshold_50.0 --prediction_jsonl evaluation/gaming/gaming/qwen25omni_7game_s3156_threshold_50.0.jsonl --num_workers 4