llm_score.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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. import re
  14. from typing import Dict, Union
  15. def extract_scores(text: str) -> Dict[str, Union[int, float]]:
  16. # Each line should look like: key: value
  17. pattern = re.compile(r'^\s*([^:\n]+?)\s*:\s*([+-]?\d+(?:\.\d+)?)\s*$', re.M)
  18. out = {}
  19. for k, v in pattern.findall(text):
  20. v_num = float(v)
  21. out[k.strip()] = int(v_num) if v_num.is_integer() else v_num
  22. return out
  23. # ========================= Initialize GPT Client =========================
  24. model_id = ''
  25. try:
  26. from evaluation.gaming.gpt_client_utils import GPTClientFactory
  27. except ImportError:
  28. try:
  29. from proactvl_eval.gpt_client_utils import GPTClientFactory
  30. except ImportError:
  31. from gpt_client_utils import GPTClientFactory
  32. deployment_name = os.getenv("OPENAI_JUDGE_MODEL", "gpt-5.1_2025-11-13")
  33. try:
  34. gpt = GPTClientFactory().build()
  35. except Exception as e:
  36. raise RuntimeError(f"Error: failed to initialize GPT client. {e}") from e
  37. # ======================== End Initialize GPT Client =========================
  38. video_event_id_to_gt_asr, video_start_end_to_video_event_id, video_event_id_to_model_pred = {}, {}, {}
  39. video_event_id_to_dataset_name = {}
  40. video_event_id_to_tag = {}
  41. video_event_id_to_context = {}
  42. video_event_id_to_gt_asr_clip = {}
  43. video_event_id_to_model_pred_clip = {}
  44. def judge_liveu(context, model_response, ground_truth):
  45. prompt = r"""Task Description:
  46. You are an expert in evaluating the **Delivery Dynamics** of real-time, second-by-second streaming commentary for video clips.
  47. Assess the model based on scenario constraints, timing/rhythm, and real-time listenability.
  48. You will be provided:
  49. 1) Context Text (previous commentary or background context; may also contain prior lines from the same commentator)
  50. 2) Label Timeline Text (time-aligned reference timeline; includes all on-clip information such as [USER] questions, [SPEAKER*] commentary, and the current commentator’s lines; use mainly as a reference for rhythm and salient windows)
  51. 3) Prediction Timeline Text (the current commentator’s output to be evaluated)
  52. Speaker tags (apply to all inputs):
  53. - [ASSISTANT] = the current commentator’s lines (may appear in Context as prior commentary; and appears in Label/Prediction as on-clip current commentator)
  54. - [USER] = user questions (reference/context only; may appear in Label Timeline)
  55. - [SPEAKER*] = other commentators (reference/context only; may appear in Label Timeline)
  56. What to score:
  57. - ONLY score the content in Prediction Timeline Text.
  58. - Any tagged lines in Context or Label Timeline are reference/context signals only and must NOT be scored.
  59. Scenario & speaking constraints (infer from Label Timeline):
  60. - Solo: normal streaming commentary flow.
  61. - Multi-commentator: generally avoid speaking when [SPEAKER*] is speaking (avoid overlap), unless clearly necessary for a key moment. A brief 1-second overlap is acceptable; penalize overlap mainly when it is sustained (e.g., 2+ consecutive seconds), frequent (repeated across the clip), or clearly disruptive.
  62. - Guidance: when [USER] asks a question, provide step-by-step guidance; avoid "one huge dump" of info.
  63. Important notes:
  64. - Prediction may be event-triggered: it may speak only on some seconds; missing seconds are silence. "SILENCE"/empty = silence.
  65. - Label Timeline is a reference only; no verbatim match required. Use it mainly to judge speaking rhythm, salient windows, and whether Prediction contradicts key events.
  66. - Subjective/expressive commentary is acceptable (brief opinions, reactions, humor, hype, atmosphere-building), as long as it does not contradict key events and does not cause disruptive chattering.
  67. Scoring rules:
  68. - Output integer scores in [1..10] (no 0).
  69. - Score bands: 10 excellent; 7–9 good; 4–6 mixed; 2–3 very poor but somewhat usable; 1 unusable.
  70. Dimension 1. Time (Synchronicity: timing + coverage + overlap etiquette):
  71. Scoring criteria:
  72. - 10: Excellent rhythm; enters within salient windows; reacts promptly to key moments and [USER] questions; stays silent when appropriate; strong coverage; overlap is rare and not sustained/disruptive.
  73. - 7-9: Generally good timing; minor drift or small gaps; occasional brief overlap is acceptable; sustained overlap is rare.
  74. - 4-6: Unstable rhythm; noticeable late/early speaking; multiple unnecessary lines or noticeable gaps; partial coverage; overlap becomes noticeable (e.g., repeated or sometimes sustained).
  75. - 2-3: Poor; frequently misses obvious speaking moments or speaks when silence/etiquette requires not speaking; overlap is frequent or often sustained/disruptive.
  76. - 1: Unusable timing; chaotic speaking/silence patterns or near-constant disruptive overlap that breaks the live experience.
  77. Coverage tolerance rule (apply to all bands):
  78. - Window shift is acceptable: if Label indicates a salient speaking window (e.g., 3–7s), speaking in any reasonably overlapping window such as 4–6, 4–7, 4–8, or 2–5 can be considered acceptable (exact boundaries not required).
  79. Dimension 2. Rate (Cadence: density + pacing + anti-dump):
  80. Scoring criteria:
  81. - 10: Perfect real-time pacing; short and listenable per-second/burst outputs; no dense dumps; minimal filler; high signal-to-noise; Guidance answers delivered step-by-step (not a single large dump).
  82. - 7-9: Mostly well paced; occasional slightly long bursts or mild redundancy/info-clumping, but still listenable.
  83. - 4-6: Noticeable pacing issues; multiple long/dense bursts, repetitive filler, or fragmented delivery; Guidance sometimes turns into a dump; signal-to-noise often low.
  84. - 2-3: Very poor pacing; frequent extremely long/dense outputs OR clearly under-speaking such that salient moments are missed.
  85. - 1: Unusable pacing; consistently unlistenable due to extreme length/density or severe fragmentation.
  86. Dimension 3. TextU (Streaming usability: spoken-form clarity):
  87. Scoring criteria:
  88. - 10: Consistently clear and well-formed as live speech; natural spoken wording; minimal grammar issues; easy to follow in real time; Guidance is step-by-step and actionable in delivery.
  89. - 7-9: Mostly clear; minor awkwardness; occasional light fragmentation but overall usable as live speech.
  90. - 4-6: Mixed; frequent awkward/incomplete phrasing or noticeable fragmentation; live usability is noticeably harmed.
  91. - 2-3: Poor; many lines hard to understand, overly fragmented, or off-scenario (e.g., does not engage a [USER] question in Guidance); live usability is low.
  92. - 1: Unusable; mostly incoherent/garbled or consistently impossible to follow as live speech.
  93. STRICT output format (no extra characters):
  94. Time: <1-10>
  95. Rate: <1-10>
  96. TextU: <1-10>
  97. Context: {context}
  98. Label Timeline: {label_timeline}
  99. Prediction Timeline: {prediction_timeline}
  100. """.format(context=context, label_timeline=ground_truth, prediction_timeline=model_response)
  101. return prompt
  102. def judge_finalq(context, model_response, ground_truth):
  103. prompt = r"""Task Description:
  104. You are an expert in evaluating the **Narrative Integrity** and overall quality of consolidated video commentary.
  105. Assess the final script’s readability, coherence, and usefulness, while only penalizing direct event contradictions.
  106. You will be provided:
  107. 1) Context Text (previous commentary or background context; may also contain prior lines from the same commentator)
  108. 2) Label Final Text (raw ASR final text from the original video; reference only)
  109. 3) Prediction Text (the final model output to be evaluated)
  110. Speaker definitions (apply to Context, Label Final, and Prediction):
  111. - [ASSISTANT] = current commentator’s lines (may appear in Context as prior commentary)
  112. - [USER] = user questions (reference/context only; may appear in Context and/or Label Final)
  113. - [SPEAKER*] = other commentators (reference/context only; may appear in Context and/or Label Final)
  114. What to score:
  115. - ONLY score the content in Prediction Text.
  116. - Any tagged lines in Context / Label Final are reference/context signals only and must NOT be scored.
  117. Evaluation principles:
  118. 1) Scenario alignment: in Multi-commentator mode, check whether [ASSISTANT] complements [SPEAKER*] logically; in Guidance mode, check whether the [USER] query is actually addressed/resolved.
  119. 2) Directional compatibility: use Label Final as a reference for major events; do NOT require word-by-word match; penalize **direct contradictions** (major event reversal, incompatible outcomes, or clearly wrong situation claims).
  120. 3) Fidelity (lightweight): expressive commentary is allowed, but avoid unjustified concrete specifics (e.g., names, numbers, causes, outcomes) that would mislead; major invented events that conflict with Label should be penalized.
  121. Scoring rules:
  122. - Output integer scores in [1..10] (no 0).
  123. - Score bands: 10 excellent; 7–9 good; 4–6 mixed; 2–3 very poor but somewhat usable; 1 unusable.
  124. Dimension 1. Fidelity (Event compatibility / non-contradiction):
  125. Criteria: compatibility with Label Final and avoidance of misleading concrete claims.
  126. - 10: Fully compatible; captures core events accurately; no major contradictions; any added specifics remain non-misleading and do not conflict with Context/Label.
  127. - 7-9: Generally compatible; core events align; some added detail/emphasis is acceptable and does not contradict the situation.
  128. - 4-6: Mixed; includes notable mismatches or unjustified concrete specifics that could mislead; weakens trust.
  129. - 2-3: Largely incompatible; many statements contradict the reference or introduce incompatible events/claims.
  130. - 1: Unusable; completely unrelated or fundamentally opposite to the reference facts.
  131. Dimension 2. Continuity (Coherence with Context, co-hosts, and user thread):
  132. Criteria: logical flow and referential integrity across entities, stance, and conversation.
  133. - 10: Seamless continuity; consistent topics/entities/stance; complements [SPEAKER*] logically; clear referents (no confusing pronouns); smooth transitions; avoids obvious stitched repetition (e.g., repeating the same conclusion multiple times).
  134. - 7-9: Mostly consistent; follows the narrative direction; minor awkwardness in transitions or small continuity slips; limited repetition.
  135. - 4-6: Noticeable logic drift; repeats points already established; confusing references to entities; clunky transitions; coherence is weakened.
  136. - 2-3: Poor; ignores [USER] questions in Guidance or contradicts important prior context; heavy repetition/looping; hard to follow logically.
  137. - 1: Unusable; fundamental breakdown of context awareness or narrative logic.
  138. Dimension 3. Substance (Text quality + usefulness + redundancy control):
  139. Criteria: informational value, guidance effectiveness, and consolidated-script usability (structure, readability, low redundancy).
  140. - 10: High value; clear structure; strong readability; minimal redundancy; provides helpful insights; directly and effectively addresses [USER] queries when present; adds value beyond generic filler.
  141. - 7-9: Clear and informative; engaging voice; minor redundancy; covers the [USER] request reasonably well; mostly avoids clichés and empty looping.
  142. - 4-6: Mediocre; generic/cliché; partially answers [USER] queries; noticeable repetition or bloated sections; usefulness is limited.
  143. - 2-3: Low value; hollow/robotic; fails to address the [USER] or provides disjoint segments; heavy looping/redundancy; mostly filler.
  144. - 1: Unusable due to severe readability failure or near-total lack of substance.
  145. STRICT output format (no extra characters):
  146. Fidelity: <1-10>
  147. Continuity: <1-10>
  148. Substance: <1-10>
  149. Context: {context}
  150. Label Final: {label_final}
  151. Prediction: {prediction}
  152. """.format(context=context, label_final=ground_truth, prediction=model_response)
  153. return prompt
  154. def llm_score(video_event_id):
  155. model_pred_clip = video_event_id_to_model_pred_clip[video_event_id]
  156. model_pred = video_event_id_to_model_pred[video_event_id]
  157. gt_asr_clip = video_event_id_to_gt_asr_clip[video_event_id]
  158. gt_asr = video_event_id_to_gt_asr[video_event_id]
  159. context = video_event_id_to_context[video_event_id]
  160. score_map = {}
  161. prompt_liveu = judge_liveu(context, model_pred_clip, gt_asr_clip)
  162. prompt_final = judge_finalq(context, model_pred, gt_asr)
  163. # score_map_finalq = {}
  164. while True:
  165. try:
  166. resp = gpt.chat.completions.create(
  167. # model='gpt-4o-2024-08-06',
  168. # model='gpt-4.1',
  169. model=deployment_name,
  170. messages=[{"role": "user", "content": [{'type': 'text', 'text': prompt_liveu}]}],
  171. seed=42,
  172. temperature=0,
  173. ).choices[0].message.content
  174. liveU_score = extract_scores(resp)
  175. if 'Time' in liveU_score and 'Rate' in liveU_score and 'TextU' in liveU_score:
  176. break
  177. else:
  178. print(f'Incomplete scores received: {liveU_score}, retrying...')
  179. except Exception as e:
  180. print('Failed to get response...', e)
  181. while True:
  182. try:
  183. resp_final = gpt.chat.completions.create(
  184. # model='gpt-4o-2024-08-06',
  185. # model='gpt-4.1',
  186. model=deployment_name,
  187. messages=[{"role": "user", "content": [{'type': 'text', 'text': prompt_final}]}],
  188. seed=42,
  189. temperature=0,
  190. ).choices[0].message.content
  191. finalQ_score = extract_scores(resp_final)
  192. if 'Fidelity' in finalQ_score and 'Continuity' in finalQ_score and 'Substance' in finalQ_score:
  193. break
  194. else:
  195. print(f'Incomplete scores received: {finalQ_score}, retrying...')
  196. except Exception as e:
  197. print('Failed to get response...', e)
  198. # Merge the two scoring results
  199. score_map.update(liveU_score)
  200. score_map.update(finalQ_score)
  201. return score_map
  202. def judge(video_event_id, model_id):
  203. gt_asr = video_event_id_to_gt_asr[video_event_id]
  204. dataset_name = video_event_id_to_dataset_name[video_event_id]
  205. tag = video_event_id_to_tag[video_event_id]
  206. return {
  207. 'video_event_id': video_event_id,
  208. 'score': llm_score(video_event_id),
  209. 'dataset_name': dataset_name,
  210. 'tag': tag
  211. }
  212. def merge_commentary(commentary_dict):
  213. sorted_seconds = sorted(commentary_dict.keys(), key=int)
  214. merged_commentary = ' '.join([commentary_dict[sec].replace(' ...', '') for sec in sorted_seconds])
  215. return merged_commentary.strip()
  216. def pred2standard(pred):
  217. sorted_seconds = sorted(pred.keys(), key=int)
  218. asrs = []
  219. asr_lines = []
  220. for sec in sorted_seconds:
  221. line = pred[sec]
  222. # Remove trailing ' ...'
  223. if line.endswith(' ...'):
  224. line = line[:-4]
  225. if line.strip() == '':
  226. continue
  227. else:
  228. asr_lines.append(f'[t={sec}] {line}')
  229. asrs.append(line)
  230. return '\n'.join(asr_lines), ' '.join(asrs)
  231. def gt2standard(annotations):
  232. active_speaker = annotations['active_speaker']['name']
  233. asrs = []
  234. asr_lines = []
  235. for item in annotations['annotations']:
  236. # if item['speaker'] != active_speaker:
  237. # continue
  238. sec = item['start']
  239. line = item['text'] if 'text' in item else item['query']
  240. if line.strip() == '':
  241. continue
  242. else:
  243. if item['speaker'] == active_speaker:
  244. asr_lines.append(f'[t={sec}][ASSISTANT]:{line}')
  245. elif item['speaker'] == 'user':
  246. asr_lines.append(f'[t={sec}][USER]:{line}')
  247. else:
  248. asr_lines.append(f'[t={sec}][{item["speaker"]}]:{line}')
  249. asrs.append(line)
  250. return '\n'.join(asr_lines), ' '.join(asrs)
  251. if __name__ == '__main__':
  252. parser = argparse.ArgumentParser()
  253. parser.add_argument('--model_id', type=str, required=True, help='Model name to compare against baseline')
  254. parser.add_argument('--prediction_jsonl', type=str, required=True, help='Path to model predictions in JSONL format')
  255. parser.add_argument('--output_dir', type=str, default='evaluation/gaming/judges/', help='Directory to save judgment results')
  256. parser.add_argument('--num_workers', type=int, default=1)
  257. parser.add_argument('--asr_jsonl', type=str, default=None, help='Path to ASR transcriptions in JSONL format')
  258. args = parser.parse_args()
  259. model_id = args.model_id
  260. prediction_jsonl = args.prediction_jsonl
  261. output_dir = args.output_dir
  262. os.makedirs(output_dir, exist_ok=True)
  263. save_path = os.path.join(output_dir, f'llm_score_{model_id}.jsonl')
  264. # prediction_jsonl = 'evaluation/gaming/gaming/Qwen2.5-Omni-7B.jsonl'
  265. prediction_jsonl = args.prediction_jsonl
  266. for line in open(prediction_jsonl):
  267. datum = json.loads(line)
  268. # if datum['dataset_name'] not in args.dataset_name_list:
  269. # continue
  270. dataset_name = datum['dataset_name']
  271. if dataset_name == 'ego4d_goal_step':
  272. dataset_name = 'ego4d'
  273. video_event_id = f'{datum["video_id"]}_{datum["begin"]}_{datum["end"]}_{dataset_name}_{datum["idx"]}'
  274. if video_event_id in video_event_id_to_model_pred:
  275. print(f'Warning: duplicate video_event_id {video_event_id} in predictions')
  276. pred_clip, pred = pred2standard(datum['pred'])
  277. video_event_id_to_model_pred_clip[video_event_id] = pred_clip
  278. video_event_id_to_model_pred[video_event_id] = pred
  279. video_event_id_to_dataset_name[video_event_id] = datum['dataset_name']
  280. video_event_id_to_tag[video_event_id] = datum['tag']
  281. for line in open(args.asr_jsonl):
  282. datum = json.loads(line)
  283. video_event_id = f'{os.path.basename(datum["video_path"])}_{datum["video_begin"]}_{datum["video_end"]}_{datum["dataset_name"]}_{datum["idx"]}'
  284. pred_clip, pred = gt2standard(datum)
  285. video_event_id_to_gt_asr_clip[video_event_id] = pred_clip
  286. video_event_id_to_gt_asr[video_event_id] = pred
  287. video_event_id_to_context[video_event_id] = datum['history'] if 'history' in datum else ''
  288. # assert len(video_event_id_to_model_pred) == len(video_event_id_to_gt_asr), "Mismatch between predictions and ASR ground truths"
  289. if len(video_event_id_to_model_pred) != len(video_event_id_to_context):
  290. print(f'Length of predictions: {len(video_event_id_to_model_pred)}, Length of contexts: {len(video_event_id_to_context)}')
  291. # Iterate and print missing video_event_id values
  292. missing_ids = set(video_event_id_to_context.keys()) - set(video_event_id_to_model_pred.keys())
  293. print(f'Number of missing contexts: {len(missing_ids)}')
  294. for missing_id in missing_ids:
  295. print(f'Missing context for video_event_id: {missing_id}')
  296. # raise ValueError("Mismatch between predictions and contexts")
  297. winner_results = local_mt(
  298. video_event_id_to_model_pred_clip.keys(),
  299. functools.partial(judge, model_id=model_id),
  300. desc=f'LLM Score Call gpt4o for {model_id}',
  301. num_workers=args.num_workers
  302. )
  303. # For liveu, aggregate results by game, by tag, and overall
  304. with open(os.path.join(output_dir, f'llm_score_liveu_{model_id}.jsonl'), 'w') as f:
  305. for winner_result in winner_results:
  306. liveu_score = {k: v for k, v in winner_result['score'].items() if k in ['Time', 'Rate', 'TextU']}
  307. output_dict = {
  308. 'video_event_id': winner_result['video_event_id'],
  309. 'liveu_score': liveu_score,
  310. 'dataset_name': winner_result['dataset_name'],
  311. 'tag': winner_result['tag']
  312. }
  313. f.write(json.dumps(output_dict) + '\n')
  314. # For finalq, aggregate results by game, by tag, and overall
  315. with open(os.path.join(output_dir, f'llm_score_finalq_{model_id}.jsonl'), 'w') as f:
  316. for winner_result in winner_results:
  317. finalq_score = {k: v for k, v in winner_result['score'].items() if k in ['Fidelity', 'Continuity', 'Substance']}
  318. output_dict = {
  319. 'video_event_id': winner_result['video_event_id'],
  320. 'finalq_score': finalq_score,
  321. 'dataset_name': winner_result['dataset_name'],
  322. 'tag': winner_result['tag']
  323. }
  324. f.write(json.dumps(output_dict) + '\n')
  325. # Aggregate liveu_score and finalq_score by game, by tag, then overall
  326. summary_by_game = {}
  327. for winner_result in winner_results:
  328. dataset_name = winner_result['dataset_name']
  329. if dataset_name not in summary_by_game:
  330. summary_by_game[dataset_name] = {
  331. 'liveu_score': {'Time': 0, 'Rate': 0, 'TextU': 0},
  332. 'finalq_score': {'Fidelity': 0, 'Continuity': 0, 'Substance': 0},
  333. 'count': 0
  334. }
  335. liveu_score = {k: v for k, v in winner_result['score'].items() if k in ['Time', 'Rate', 'TextU']}
  336. finalq_score = {k: v for k, v in winner_result['score'].items() if k in ['Fidelity', 'Continuity', 'Substance']}
  337. for k in liveu_score:
  338. summary_by_game[dataset_name]['liveu_score'][k] += liveu_score[k]
  339. for k in finalq_score:
  340. summary_by_game[dataset_name]['finalq_score'][k] += finalq_score[k]
  341. summary_by_game[dataset_name]['count'] += 1
  342. # summary by game: output mean scores
  343. for dataset_name, summary in summary_by_game.items():
  344. count = summary['count']
  345. liveu_avg = {k: v / count for k, v in summary['liveu_score'].items()}
  346. finalq_avg = {k: v / count for k, v in summary['finalq_score'].items()}
  347. output = f'Average LiveU Score for {model_id} on {dataset_name}: ' + ', '.join([f'{k}: {v:.2f}' for k, v in liveu_avg.items()])
  348. print(output)
  349. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  350. f.write(output + '\n')
  351. output = f'Average FinalQ Score for {model_id} on {dataset_name}: ' + ', '.join([f'{k}: {v:.2f}' for k, v in finalq_avg.items()])
  352. print(output)
  353. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  354. f.write(output + '\n')
  355. # summary by tag
  356. summary_by_tag = {}
  357. for winner_result in winner_results:
  358. tag = winner_result['tag']
  359. if tag not in summary_by_tag:
  360. summary_by_tag[tag] = {
  361. 'liveu_score': {'Time': 0, 'Rate': 0, 'TextU': 0},
  362. 'finalq_score': {'Fidelity': 0, 'Continuity': 0, 'Substance': 0},
  363. 'count': 0
  364. }
  365. liveu_score = {k: v for k, v in winner_result['score'].items() if k in ['Time', 'Rate', 'TextU']}
  366. finalq_score = {k: v for k, v in winner_result['score'].items() if k in ['Fidelity', 'Continuity', 'Substance']}
  367. for k in liveu_score:
  368. summary_by_tag[tag]['liveu_score'][k] += liveu_score[k]
  369. for k in finalq_score:
  370. summary_by_tag[tag]['finalq_score'][k] += finalq_score[k]
  371. summary_by_tag[tag]['count'] += 1
  372. # summary by tag: output mean scores
  373. for tag, summary in summary_by_tag.items():
  374. count = summary['count']
  375. liveu_avg = {k: v / count for k, v in summary['liveu_score'].items()}
  376. finalq_avg = {k: v / count for k, v in summary['finalq_score'].items()}
  377. output = f'Average LiveU Score for {model_id} on tag {tag}: ' + ', '.join([f'{k}: {v:.2f}' for k, v in liveu_avg.items()])
  378. print(output)
  379. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  380. f.write(output + '\n')
  381. output = f'Average FinalQ Score for {model_id} on tag {tag}: ' + ', '.join([f'{k}: {v:.2f}' for k, v in finalq_avg.items()])
  382. print(output)
  383. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  384. f.write(output + '\n')
  385. # overall average
  386. total_count = len(winner_results)
  387. total_liveu_score = {'Time': 0, 'Rate': 0, 'TextU': 0}
  388. total_finalq_score = {'Fidelity': 0, 'Continuity': 0, 'Substance': 0}
  389. for winner_result in winner_results:
  390. liveu_score = {k: v for k, v in winner_result['score'].items() if k in ['Time', 'Rate', 'TextU']}
  391. finalq_score = {k: v for k, v in winner_result['score'].items() if k in ['Fidelity', 'Continuity', 'Substance']}
  392. for k in liveu_score:
  393. total_liveu_score[k] += liveu_score[k]
  394. for k in finalq_score:
  395. total_finalq_score[k] += finalq_score[k]
  396. overall_liveu_avg = {k: v / total_count for k, v in total_liveu_score.items()}
  397. overall_finalq_avg = {k: v / total_count for k, v in total_finalq_score.items()}
  398. output = f'Overall Average LiveU Score for {model_id}: ' + ', '.join([f'{k}: {v:.2f}' for k, v in overall_liveu_avg.items()])
  399. print(output)
  400. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  401. f.write(output + '\n')
  402. output = f'Overall Average FinalQ Score for {model_id}: ' + ', '.join([f'{k}: {v:.2f}' for k, v in overall_finalq_avg.items()])
  403. print(output)
  404. with open(os.path.join(output_dir, 'log.txt'), 'a') as f:
  405. f.write(output + '\n')