3D-animation-arena commited on
Commit
6c9ff9d
·
verified ·
1 Parent(s): cf77a31

Set up arena

Browse files
README.md CHANGED
@@ -1,13 +1,13 @@
1
  ---
2
  title: 3D Animation Arena
3
- emoji: 🌍
4
- colorFrom: gray
5
  colorTo: gray
6
  sdk: gradio
7
  sdk_version: 5.27.0
8
  app_file: app.py
9
  pinned: false
10
- short_description: Arena to rank human 3D Animation models
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: 3D Animation Arena
3
+ emoji: 🐠
4
+ colorFrom: green
5
  colorTo: gray
6
  sdk: gradio
7
  sdk_version: 5.27.0
8
  app_file: app.py
9
  pinned: false
10
+ short_description: Arena to rank 3D animation models
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from pathlib import Path
3
+ import uuid
4
+ import random
5
+
6
+ from utils.data_utils import generate_leaderboard
7
+ from utils.plot_utils import plot_ratings
8
+ from utils.utils import simulate, submit_rating, generate_matchup
9
+ from config import MODE, VIDEOS, MODELS, CRITERIA, default_beta
10
+
11
+
12
+ head = f"""
13
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
14
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.33.1/plotly.min.js"></script>
16
+ <script>{Path('static/modelViewer.js').read_text()}</script>
17
+ <script>{Path('static/popup.js').read_text()}</script>
18
+ <script>{Path('static/plots.js').read_text()}</script>
19
+ """
20
+
21
+ with gr.Blocks(title='3D Animation Arena', head=head, css_paths='static/style.css') as arena:
22
+
23
+ sessionState = gr.State({
24
+ 'video': None,
25
+ 'modelLeft': None,
26
+ 'modelRight': None,
27
+ 'darkMode': False,
28
+ 'videos': VIDEOS,
29
+ 'currentTab': CRITERIA[0],
30
+ 'uuid': None
31
+ })
32
+
33
+ frontState = gr.JSON(sessionState, visible=False)
34
+
35
+ with gr.Row():
36
+ with gr.Column(scale=1):
37
+ gr.HTML('')
38
+ with gr.Column(scale=12):
39
+ gr.HTML("<h1 style='text-align:center; font-size:50px'>3D Animation Arena</h1>")
40
+ with gr.Column(scale=1):
41
+ toggle_dark = gr.Button(value="Dark Mode")
42
+
43
+ def update_toggle_dark(state):
44
+ state['darkMode'] = not state['darkMode']
45
+ if state['darkMode']:
46
+ return gr.update(value="Light Mode"), state
47
+ else:
48
+ return gr.update(value="Dark Mode"), state
49
+
50
+ toggle_dark.click(
51
+ inputs=[sessionState],
52
+ js="""
53
+ () => {
54
+ document.body.classList.toggle('dark');
55
+ }
56
+ """,
57
+ fn=update_toggle_dark,
58
+ outputs=[toggle_dark, sessionState]
59
+ )
60
+
61
+ with gr.Tab(label='Arena'):
62
+ models = gr.HTML('''
63
+ <div class="viewer-container">
64
+ <iframe
65
+ id="modelViewerLeft"
66
+ src="https://d39vhmln1nnc4z.cloudfront.net/index.html"
67
+ width="100%"
68
+ height="100%"
69
+ allow="storage-access"
70
+ ></iframe>
71
+
72
+ <iframe
73
+ id="modelViewerRight"
74
+ src="https://d39vhmln1nnc4z.cloudfront.net/index.html"
75
+ width="100%"
76
+ height="100%"
77
+ allow="storage-access"
78
+ ></iframe>
79
+ </div>''',
80
+ render=False)
81
+
82
+ with gr.Row():
83
+ with gr.Column(scale=1):
84
+ gr.HTML(f"<h1>1. Choose a video below:</h1>")
85
+ video = gr.Video(
86
+ label='Input Video',
87
+ interactive=False,
88
+ autoplay=True,
89
+ show_download_button=False,
90
+ loop=True,
91
+ elem_id='gradioVideo',
92
+ )
93
+
94
+ triggerButtons = {}
95
+ for vid in sessionState.value['videos']:
96
+ triggerButtons[vid] = gr.Button(elem_id=f'triggerBtn_{vid}', visible=False)
97
+ triggerButtons[vid].click(
98
+ fn=lambda vid=vid: gr.update(value=f'https://gradio-model-viewer.s3.eu-west-1.amazonaws.com/sample+videos/{vid}.mp4'),
99
+ outputs=[video]
100
+ )
101
+ examples = gr.HTML(visible=False)
102
+
103
+ with gr.Column(scale=4):
104
+ gr.HTML("""
105
+ <h1>2. Play around with the models:
106
+ <span class="glyphicon glyphicon-question-sign popup-btn btn btn-info btn-lg" data-popup-id="instructionsPopup">
107
+ <span class="popup-text" id="instructionsPopup">You can control the playback in both viewers at the same time by using the video, or control both viewers independently by using mouse and GUI!</span>
108
+ </span>
109
+ </h1>
110
+ """)
111
+ with gr.Row():
112
+ models.render()
113
+
114
+ with gr.Row():
115
+ gr.HTML(f"<h1>3. Choose your favorite model for each criteria:</h1>")
116
+ ratingButtons = {}
117
+ for criteria in CRITERIA:
118
+ with gr.Row():
119
+ with gr.Column():
120
+ with gr.Row():
121
+ match criteria:
122
+ case 'Global_Appreciation':
123
+ instructions = "Your overall appreciation of the models, including general aesthetics and self-contacts if applicable."
124
+ case 'Ground_Contacts':
125
+ instructions = "The quality of the models' contacts with the ground, including ground penetration and foot sliding."
126
+ case 'Fidelity':
127
+ instructions = "The fidelity of the models compared to the motion of the original video."
128
+ case 'Fluidity':
129
+ instructions = "The smoothness and temporal coherence of the models."
130
+ gr.HTML(f"""
131
+ <h2 style='text-align:center;'>{criteria.replace('_', ' ')}
132
+ <span class="glyphicon glyphicon-question-sign popup-btn btn btn-info btn-lg" data-popup-id="{criteria}Popup">
133
+ <span class="popup-text" id="{criteria}Popup">{instructions}</span>
134
+ </span></h2>
135
+ """)
136
+ with gr.Row():
137
+ ratingButtons[criteria] = []
138
+ with gr.Column(scale=2):
139
+ ratingButtons[criteria].append(gr.Button('Left Model', variant='primary', interactive=False))
140
+ with gr.Column(scale=1, min_width=2):
141
+ ratingButtons[criteria].append(gr.Button('Skip', min_width=2, interactive=False))
142
+ with gr.Column(scale=2):
143
+ ratingButtons[criteria].append(gr.Button('Right Model', variant='primary', interactive=False))
144
+
145
+
146
+ # Leaderboard per criteria
147
+ with gr.Tab(label='Leaderboards') as leaderboard_tab:
148
+
149
+ if MODE == 'testing':
150
+ # Simulation controls
151
+ with gr.Row():
152
+ simulate_btn = gr.Button('Simulate Matches', variant='primary')
153
+ add_model_btn = gr.Button('Add Model', variant='secondary')
154
+ with gr.Row():
155
+ gr.Markdown('''
156
+ ## Probability of each model to be chosen is updated after each vote following: \
157
+ $$ p_i = \\frac{e^{-\\frac{Matches_i}{\\beta}}}{\\sum_{j=1}^{N} e^{-\\frac{Matches_j}{\\beta}}} $$
158
+ ''')
159
+ iterate = gr.Number(label='Number of iterations', value=100, minimum=1, maximum=2000, precision=0, interactive=True)
160
+ beta = gr.Number(label='Beta', value=default_beta, minimum=1, maximum=1000, precision=0, step=10, interactive=True)
161
+ else:
162
+ beta = gr.Number(label='Beta', value=default_beta, render=False)
163
+
164
+ leaderboards = {}
165
+ tabs = {}
166
+ for criteria in CRITERIA:
167
+ with gr.Tab(label=criteria.replace('_', ' ')) as tabs[criteria]:
168
+ with gr.Row():
169
+ gr.HTML(f"<h2 style='text-align:center;'>{criteria.replace('_', ' ')}</h2>")
170
+ with gr.Row():
171
+ leaderboards[criteria] = gr.Dataframe(value=None, row_count=(len(MODELS), 'fixed'), headers=['Model', 'Elo', 'Wins', 'Matches', 'Win Rate'], interactive=False)
172
+
173
+ # Plots
174
+ if MODE == 'testing':
175
+ with gr.Row():
176
+ elo_plot = gr.Plot(value=None, label='Elo Ratings', format='plotly', elem_id='plot')
177
+ with gr.Row():
178
+ wr_plot = gr.Plot(value=None, label='Win Rates', format='plotly', elem_id='plot')
179
+ with gr.Row():
180
+ matches_plot = gr.Plot(value=None, label='Matches played', format='plotly', elem_id='plot')
181
+ elif MODE == 'production':
182
+ elo_plot = gr.Plot(value=None, label='Elo Ratings', format='plotly', elem_id='plot', visible=False)
183
+ wr_plot = gr.Plot(value=None, label='Win Rates', format='plotly', elem_id='plot', visible=False)
184
+ matches_plot = gr.Plot(value=None, label='Matches played', format='plotly', elem_id='plot', visible=False)
185
+
186
+ with gr.Tab(label='About'):
187
+ gr.Markdown('''
188
+ ## Thank you for using the 3D Animation Arena!
189
+
190
+ This app is designed to compare different models based on human preferences, inspired by dylanebert's [3D Arena](https://huggingface.co/spaces/dylanebert/3d-arena) on Hugging Face.
191
+ Current rankings often use metrics to assess the quality of a model, but these metrics may not always reflect the complexity behind human preferences.
192
+
193
+ The current models competing in the arena are:
194
+ - 4DHumans (https://github.com/shubham-goel/4D-Humans)
195
+ - CLIFF (https://github.com/haofanwang/CLIFF)
196
+ - GVHMR (https://github.com/zju3dv/GVHMR)
197
+ - HybrIK (https://github.com/jeffffffli/HybrIK)
198
+ - WHAM (https://github.com/yohanshin/WHAM)
199
+
200
+ All inferences are precomputed following the code in the associated GitHub repository.
201
+ Some post-inference modifications have been made to some models in order to make the comparison possible.
202
+ These modifications include:
203
+ * Adjusting height to a common ground
204
+ * Fixing the root depth of certain models, when depth was extremely jittery
205
+ * Fixing the root position of certain models, when no root position was available
206
+
207
+ All models use the SMPL body model to discard the influence of the body model on the comparison.
208
+ These choices were made without any intention to favor or harm any model.
209
+ All matchups are generated randomly, don't hesitate to rate the same videos multiple times as the matchups will probably be different!
210
+
211
+ ---
212
+
213
+ If you have comments, complaints or suggestions, please contact me at [email protected].
214
+ New models and videos will be added over time, feel free to share your ideas! Keep in mind that I will not add raw inferences from other people to keep it fair.
215
+ ''')
216
+
217
+
218
+ # Event handlers
219
+ def randomize_videos(state):
220
+ state['uuid'] = str(uuid.uuid4())
221
+ random.shuffle(state['videos'])
222
+ gallery = "<div class='gallery'>"
223
+ for vid in state['videos']:
224
+ gallery += f"""
225
+ <button class="btn btn-info thumbnail-btn" onclick="(function() {{
226
+ let gradioVideo = document.getElementById('gradioVideo');
227
+ let videoComponent = gradioVideo ? gradioVideo.querySelector('video') : null;
228
+ if (videoComponent && !videoComponent.src.includes('{vid}')) {{
229
+ Array.from(document.getElementsByClassName('thumbnail-btn')).forEach(btn => btn.disabled = true);
230
+ }}
231
+ document.getElementById('triggerBtn_{vid}').click();
232
+ }})()">
233
+ <video class="thumbnail" preload="" loop muted onmouseenter="this.play()" onmouseleave="this.pause()">
234
+ <source src="https://gradio-model-viewer.s3.eu-west-1.amazonaws.com/sample+videos/{vid}.mp4">
235
+ </video>
236
+ </button>
237
+ """
238
+ gallery += "</div>"
239
+ return state, gallery
240
+
241
+ async def display_leaderboards():
242
+ return [await generate_leaderboard(criteria) for criteria in CRITERIA]
243
+
244
+ arena.load(
245
+ inputs=[sessionState],
246
+ fn=lambda state: randomize_videos(state),
247
+ outputs=[sessionState, examples],
248
+ ).then(
249
+ inputs=[],
250
+ fn=lambda: gr.update(visible=True),
251
+ outputs=[examples]
252
+ ).then(
253
+ inputs=[gr.State(CRITERIA[0])],
254
+ fn=plot_ratings,
255
+ outputs=[elo_plot, wr_plot, matches_plot]
256
+ ).then(
257
+ inputs=[],
258
+ fn=display_leaderboards,
259
+ outputs=[leaderboards[criteria] for criteria in CRITERIA]
260
+ )
261
+
262
+ async def update_models(video, state):
263
+ leaderboard = await generate_leaderboard(CRITERIA[0])
264
+ video_name = video.split('/')[-1].split('.')[0]
265
+ modelLeft, modelRight = generate_matchup(leaderboard=leaderboard, beta=beta.value)
266
+
267
+ state['video'] = video_name
268
+ state['modelLeft'] = MODELS[modelLeft]
269
+ state['modelRight'] = MODELS[modelRight]
270
+
271
+ return state, state
272
+
273
+ video.change(
274
+ inputs=[video, sessionState],
275
+ fn=update_models,
276
+ outputs=[sessionState, frontState]
277
+ )
278
+
279
+ # Weird workaround to run JS function on state change, from https://github.com/gradio-app/gradio/issues/3525#issuecomment-2348596861
280
+ frontState.change(
281
+ inputs=[frontState],
282
+ js='(state) => updateViewers(state)',
283
+ fn=lambda state: None,
284
+ ).then(
285
+ inputs=None,
286
+ fn=lambda: tuple(gr.update(interactive=True) for _ in sum(ratingButtons.values(), [])),
287
+ outputs= sum(ratingButtons.values(), [])
288
+ )
289
+
290
+ leaderboard_tab.select(
291
+ inputs=None,
292
+ js='() => resetPlots()',
293
+ fn=None,
294
+ ).then(
295
+ fn=lambda: [gr.update(value=None) for _ in range(3)],
296
+ outputs=[elo_plot, wr_plot, matches_plot]
297
+ ).then(
298
+ inputs=[sessionState],
299
+ fn=lambda state: plot_ratings(state['currentTab']),
300
+ outputs=[elo_plot, wr_plot, matches_plot]
301
+ )
302
+
303
+ async def process_rating(state, i, criteria):
304
+ return gr.update(value=await submit_rating(
305
+ criteria=criteria,
306
+ winner=state['modelLeft'] if i == 0 else state['modelRight'] if i == 2 else None,
307
+ loser=state['modelRight'] if i == 0 else state['modelLeft'] if i == 2 else None,
308
+ uuid=state['uuid']
309
+ ))
310
+
311
+ def update_tab(state, criteria):
312
+ state['currentTab'] = criteria
313
+ return state
314
+
315
+ for criteria in CRITERIA:
316
+ for i, button in enumerate(ratingButtons[criteria]):
317
+ button.click(
318
+ # fn=lambda i=i, criteria=criteria: gr.Info(f'{"You chose Left Model for " if i == 0 else "You chose Right Model for " if i == 2 else "You skipped "} {criteria.replace("_", " ")}!'),
319
+ # ).then(
320
+ fn=lambda: tuple(gr.update(interactive=False) for _ in range(len(ratingButtons[criteria]))),
321
+ outputs=ratingButtons[criteria]
322
+ ).then(
323
+ inputs=[sessionState, gr.State(i), gr.State(criteria)],
324
+ fn=process_rating,
325
+ outputs=[leaderboards[criteria]],
326
+ )
327
+
328
+ tabs[criteria].select(
329
+ fn=lambda: [gr.update(value=None) for _ in range(3)],
330
+ outputs=[elo_plot, wr_plot, matches_plot]
331
+ ).then(
332
+ inputs=[gr.State(criteria)],
333
+ fn=plot_ratings,
334
+ outputs=[elo_plot, wr_plot, matches_plot]
335
+ ).then(
336
+ inputs=[sessionState, gr.State(criteria)],
337
+ fn=update_tab,
338
+ outputs=[sessionState]
339
+ )
340
+
341
+
342
+ if MODE == 'testing':
343
+ for criteria in CRITERIA:
344
+ simulate_btn.click(
345
+ inputs=[iterate, beta, gr.State(criteria)],
346
+ fn=simulate,
347
+ outputs=[leaderboards[criteria]],
348
+ ).then(fn=lambda: [gr.update(value=None) for _ in range(3)],
349
+ outputs=[elo_plot, wr_plot, matches_plot]
350
+ ).then(
351
+ inputs=[gr.State(criteria)],
352
+ fn=plot_ratings,
353
+ outputs=[elo_plot, wr_plot, matches_plot]
354
+ )
355
+
356
+ add_model_btn.click(
357
+ fn=lambda: MODELS.append(f'model_{len(MODELS)}'),
358
+ )
359
+
360
+ if __name__ == '__main__':
361
+ gr.set_static_paths(['static'])
362
+ arena.queue(default_concurrency_limit=50).launch(inbrowser=True, allowed_paths=['static/'])
config.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ MODE = os.getenv('ARENA_MODE') # 'testing' or 'production'
4
+
5
+ default_beta = 500
6
+
7
+ MODELS = [
8
+ '4DHumans',
9
+ 'CLIFF',
10
+ 'GVHMR',
11
+ 'HybrIK',
12
+ 'WHAM',
13
+ 'TokenHMR',
14
+ 'STAF',
15
+ 'CameraHMR'
16
+ ]
17
+
18
+ VIDEOS = [
19
+ 'backflip',
20
+ 'ballet_dance',
21
+ 'ballet_jump',
22
+ 'basketball_dunk',
23
+ 'boxing',
24
+ 'breakdance',
25
+ 'bridge',
26
+ 'capoeira',
27
+ 'contorsionist',
28
+ 'dance_feathers',
29
+ 'dance_modern1',
30
+ 'dance_modern2',
31
+ 'dance_modern3',
32
+ 'dance_road',
33
+ 'dance_tiktok',
34
+ 'highkick',
35
+ 'hiphop',
36
+ 'parkour',
37
+ 'pillars',
38
+ 'skateboard',
39
+ 'spinkick',
40
+ 'trampoline',
41
+ 'wall_jump',
42
+ 'yoga',
43
+ 'cartoon_fall',
44
+ 'cutting_tree',
45
+ 'fencing',
46
+ 'ferocity',
47
+ 'ice_skating',
48
+ 'moonwalk',
49
+ 'npc',
50
+ 'running',
51
+ 'sitting',
52
+ 'stretching',
53
+ 'tennis',
54
+ ]
55
+
56
+ CRITERIA = [
57
+ 'Global_Appreciation',
58
+ 'Ground_Contacts',
59
+ 'Fidelity',
60
+ 'Fluidity',
61
+ ]
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ pandas
2
+ plotly
3
+ numpy
4
+ gradio==5.16.0
5
+ huggingface_hub==0.28.1
6
+ aioboto3==14.1.0
7
+ pydantic==2.10.3
static/modelViewer.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function updateViewers(state) {
2
+ let video = state.video;
3
+ let modelLeft = state.modelLeft;
4
+ let modelRight = state.modelRight;
5
+
6
+ let gradioVideo = document.getElementById("gradioVideo");
7
+ let videoComponent = gradioVideo ? gradioVideo.querySelector("video") : null;
8
+
9
+ if (videoComponent && document.getElementById("modelViewerLeft") && document.getElementById("modelViewerRight")) {
10
+
11
+ videoComponent.setAttribute("muted", true);
12
+ document.getElementById("modelViewerLeft").contentWindow.postMessage({ action: "loadModel", modelUrl: `https://gradio-model-viewer.s3.eu-west-1.amazonaws.com/models/${modelLeft}/${video}.glb` }, "*");
13
+ document.getElementById("modelViewerRight").contentWindow.postMessage({ action: "loadModel", modelUrl: `https://gradio-model-viewer.s3.eu-west-1.amazonaws.com/models/${modelRight}/${video}.glb` }, "*");
14
+
15
+ let loadedCount = 0;
16
+
17
+ window.addEventListener("message", (event) => {
18
+ if (event.data.status === "modelLoaded") {
19
+ loadedCount++;
20
+ if (loadedCount === 2) {
21
+ videoComponent.addEventListener("play", syncModelViewers);
22
+ videoComponent.addEventListener("pause", syncModelViewers);
23
+ videoComponent.addEventListener("timeupdate", syncModelViewers);
24
+
25
+ Array.from(document.getElementsByClassName('thumbnail-btn')).forEach(btn => btn.disabled = false);
26
+ }
27
+ }
28
+ else if (event.data.status === "modelLoadError") {
29
+ Array.from(document.getElementsByClassName('thumbnail-btn')).forEach(btn => btn.disabled = false);
30
+ }
31
+ });
32
+
33
+ }
34
+ }
35
+
36
+ function syncModelViewers(event) {
37
+ let videoComponent = event.target;
38
+ let modelViewerLeft = document.getElementById("modelViewerLeft");
39
+ let modelViewerRight = document.getElementById("modelViewerRight");
40
+
41
+ if (!modelViewerLeft || !modelViewerRight) return;
42
+
43
+ switch (event.type) {
44
+ case "play":
45
+ modelViewerLeft.contentWindow.postMessage({ action: "playAnimation" }, "*");
46
+ modelViewerRight.contentWindow.postMessage({ action: "playAnimation" }, "*");
47
+ break;
48
+ case "pause":
49
+ modelViewerLeft.contentWindow.postMessage({ action: "pauseAnimation" }, "*");
50
+ modelViewerRight.contentWindow.postMessage({ action: "pauseAnimation" }, "*");
51
+ break;
52
+ case "timeupdate":
53
+ let currentTime = videoComponent.currentTime;
54
+ modelViewerLeft.contentWindow.postMessage({ action: "setAnimationTime", currentTime }, "*");
55
+ modelViewerRight.contentWindow.postMessage({ action: "setAnimationTime", currentTime }, "*");
56
+ break;
57
+ }
58
+ }
static/plots.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // Fix plot display issues with gradio Tabs
2
+ function resetPlots() {
3
+ document.querySelectorAll(".js-plotly-plot").forEach(plot => {
4
+ Plotly.relayout(plot, { autosize: true });
5
+ });
6
+ };
static/popup.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function togglePopup(event, show) {
2
+ var popupBtn = event.target;
3
+ var popupId = popupBtn.getAttribute('data-popup-id');
4
+ var popup = document.getElementById(popupId);
5
+
6
+ popup.parentElement.parentElement.parentElement.parentElement.parentElement.style.overflow = 'visible';
7
+
8
+ popup.classList.toggle("show");
9
+ popupBtn.parentElement.style.overflow = 'visible';
10
+
11
+ event.stopPropagation();
12
+ }
13
+
14
+ document.addEventListener("mouseover", function(event) {
15
+ if (event.target.classList.contains('popup-btn')) {
16
+ togglePopup(event, true);
17
+ }
18
+ });
19
+
20
+ document.addEventListener("mouseout", function(event) {
21
+ if (event.target.classList.contains('popup-btn')) {
22
+ togglePopup(event, false);
23
+ }
24
+ });
static/style.css ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .viewer-container {
2
+ display: flex;
3
+ gap: 20px;
4
+ justify-content: center;
5
+ }
6
+ model-viewer {
7
+ width: 50%;
8
+ height: 500px;
9
+ background-color: #eeeeee;
10
+ }
11
+ iframe {
12
+ width: 50%;
13
+ height: 500px;
14
+ background-color: #eeeeee;
15
+ border: none;
16
+ border-radius: 5px;
17
+ }
18
+
19
+ .js-plotly-plot {
20
+ width: 100%;
21
+ display: flex;
22
+ justify-content: center;
23
+ }
24
+
25
+ @font-face {
26
+ font-family: 'Glyphicons Halflings';
27
+
28
+ src: url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot');
29
+ src: url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'), url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
30
+ }
31
+ .glyphicon {
32
+ position: relative;
33
+ top: 2px;
34
+ display: inline-block;
35
+ font-family: 'Glyphicons Halflings';
36
+ font-style: normal;
37
+ font-weight: normal;
38
+ line-height: 1;
39
+
40
+ -webkit-font-smoothing: antialiased;
41
+ -moz-osx-font-smoothing: grayscale;
42
+ }
43
+ .glyphicon-question-sign:before {
44
+ content: "\e085";
45
+ }
46
+
47
+ .popup-btn {
48
+ position: relative;
49
+ display: inline-block;
50
+ cursor: pointer;
51
+ -webkit-user-select: none;
52
+ -moz-user-select: none;
53
+ -ms-user-select: none;
54
+ user-select: none;
55
+ overflow: visible !important;
56
+ font-size: 20px;
57
+ border: none;
58
+ text-align: center;
59
+ text-decoration: none;
60
+ padding-left: 10px;
61
+ }
62
+
63
+ .popup-text {
64
+ visibility: hidden;
65
+ width: 300px;
66
+ background-color: #e4e4e7;
67
+ color: #000;
68
+ text-align: center;
69
+ border-radius: 10px;
70
+ padding: 20px 10px;
71
+ position: absolute;
72
+ z-index: 1;
73
+ bottom: 150%;
74
+ left: 50%;
75
+ margin-left: -145px;
76
+ overflow: visible !important;
77
+ font-size: 17px;
78
+ font-family: Arial, sans-serif;
79
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
80
+ transition: visibility 0.3s, opacity 0.3s ease-in-out;
81
+ opacity: 0;
82
+ }
83
+
84
+ .popup-text.dark {
85
+ background-color: #52525b;
86
+ color: #fff;
87
+ }
88
+
89
+ /* Popup arrow */
90
+ .popup-text::after {
91
+ content: "";
92
+ position: absolute;
93
+ top: 100%;
94
+ left: 50%;
95
+ margin-left: -5px;
96
+ border-width: 5px;
97
+ border-style: solid;
98
+ border-color: #e4e4e7 transparent transparent transparent;
99
+ }
100
+
101
+ .popup-text.dark::after {
102
+ border-color: #52525b transparent transparent transparent;
103
+ }
104
+
105
+ .popup-text.show {
106
+ visibility: visible;
107
+ opacity: 1;
108
+ }
109
+
110
+ .thumbnail-btn {
111
+ height:75px;
112
+ width: auto;
113
+ overflow: hidden;
114
+ display: flex;
115
+ }
116
+ .thumbnail-btn:hover {
117
+ cursor: pointer;
118
+ transform: scale(1.05);
119
+ transition: transform 0.5s;
120
+ }
121
+
122
+ .thumbnail {
123
+ width: auto;
124
+ height: 100%;
125
+ object-fit: cover;
126
+ border-radius: 5px;
127
+ }
128
+
129
+ .gallery {
130
+ display: flex;
131
+ flex-wrap: wrap;
132
+ gap: 10px;
133
+ justify-content: flex-start;
134
+ align-items: flex-start;
135
+ }
utils/__pycache__/data_utils.cpython-310.pyc ADDED
Binary file (1.68 kB). View file
 
utils/__pycache__/emoji_utils.cpython-310.pyc ADDED
Binary file (495 Bytes). View file
 
utils/__pycache__/plot_utils.cpython-310.pyc ADDED
Binary file (1.49 kB). View file
 
utils/__pycache__/s3_utils.cpython-310.pyc ADDED
Binary file (4.37 kB). View file
 
utils/__pycache__/utils.cpython-310.pyc ADDED
Binary file (4.69 kB). View file
 
utils/data_utils.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .s3_utils import read_from_s3
2
+ from config import MODELS
3
+ import pandas as pd
4
+
5
+ async def generate_leaderboard(criteria : str) -> pd.DataFrame:
6
+ """
7
+ Generate the leaderboard from saved data.
8
+
9
+ Args:
10
+ criteria (str): The criteria corresponding to the leaderboard.
11
+
12
+ Returns:
13
+ pd.DataFrame: The leaderboard.
14
+ """
15
+ try:
16
+ leaderboard = await read_from_s3(f'leaderboard_{criteria}.csv')
17
+ if leaderboard is None:
18
+ raise Exception
19
+ except:
20
+ leaderboard = pd.DataFrame({
21
+ 'Model': pd.Series(dtype='str'),
22
+ 'Elo': pd.Series(dtype='int'),
23
+ 'Wins': pd.Series(dtype='int'),
24
+ 'Matches': pd.Series(dtype='int'),
25
+ 'Win Rate': pd.Series(dtype='float')
26
+ })
27
+
28
+ for model in MODELS:
29
+ if model not in leaderboard['Model'].values:
30
+ leaderboard = pd.concat([leaderboard, pd.DataFrame([{'Model': model, 'Elo': 1500, 'Wins': 0, 'Matches': 0, 'Win Rate': 0.0}])], ignore_index=True)
31
+ leaderboard = leaderboard.sort_values('Elo', ascending=False).reset_index(drop=True)
32
+ leaderboard['Win Rate'] = leaderboard['Win Rate'].apply(lambda x: round(x, 2))
33
+
34
+ return leaderboard
35
+
36
+ async def generate_data() -> pd.DataFrame:
37
+ """
38
+ Generate the data for the matches.
39
+
40
+ Returns:
41
+ pd.DataFrame: The data for the matches.
42
+ """
43
+ try :
44
+ data = await read_from_s3('data.csv')
45
+ if data is None:
46
+ raise Exception
47
+ except:
48
+ data = pd.DataFrame(columns=['Criteria', 'Model', 'Opponent', 'Won', 'Elo', 'Win Rate', 'Matches', 'Timestamp', 'UUID'])
49
+ return data
utils/emoji_utils.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def add_emoji(model : str) -> str:
2
+ match model:
3
+ case '4DHumans':
4
+ return '\U0001F3A5 4DHumans'
5
+ case 'CLIFF':
6
+ return '\U0001F3D4 CLIFF'
7
+ case 'GVHMR':
8
+ return '\U0001F4CD GVHMR'
9
+ case 'HybrIK':
10
+ return '\U0001F9BE HybrIK'
11
+ case 'WHAM':
12
+ return '\U0001F3AF WHAM'
13
+ case _:
14
+ return model
utils/graphs.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
utils/plot_utils.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import plotly.express as px
2
+ from .data_utils import generate_data
3
+
4
+ async def plot_ratings(criteria : str):
5
+ """
6
+ Plot different ratings of the models for a given criteria on a line graph.
7
+
8
+ Args:
9
+ criteria (str): The criteria to plot the ratings for.
10
+ """
11
+ data = await generate_data()
12
+
13
+ elo_data = data[data['Criteria'] == criteria].reset_index(drop=True)
14
+ elo_data.reset_index(inplace=True)
15
+ elo_data['index'] = elo_data['index'].apply(lambda x: int(x/2))
16
+ elo_fig = px.line(elo_data, x='index', y='Elo', color='Model', title='Elo Ratings Over Total Votes', labels={'index': 'Total Votes', 'Elo': 'Elo Rating'})
17
+
18
+ wr_data = data[data['Criteria'] == criteria].reset_index(drop=True)
19
+ wr_data.reset_index(inplace=True)
20
+ wr_data['index'] = wr_data['index'].apply(lambda x: int(x/2))
21
+ wr_fig = px.line(wr_data, x='index', y='Win Rate', color='Model', title='Win Rates Over Total Votes', labels={'index': 'Total Votes', 'Win Rate': 'Win Rate'})
22
+
23
+ matches_data = data[data['Criteria'] == criteria].reset_index(drop=True)
24
+ matches_data.reset_index(inplace=True)
25
+ matches_data['index'] = matches_data['index'].apply(lambda x: int(x/2))
26
+ matches_fig = px.line(matches_data, x='index', y='Matches', color='Model', title='Matches Played Over Total Votes', labels={'index': 'Total Votes', 'Matches': 'Matches Played'})
27
+
28
+ return elo_fig, wr_fig, matches_fig
utils/s3_utils.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aioboto3
2
+ import pandas as pd
3
+ from io import StringIO
4
+ from typing import Optional, Union
5
+ import os
6
+
7
+ AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
8
+ AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
9
+ S3_BUCKET = os.getenv('S3_BUCKET')
10
+ S3_VIDEO_PATH = 'sample videos'
11
+ S3_MODEL_PATH = 'models'
12
+ S3_DATA_PATH = '3d_animation_arena/results'
13
+
14
+ async def download_from_s3(file_key : str, target_dir: str, bucket : str = S3_BUCKET) -> Optional[str]:
15
+ """
16
+ Downloads a file from an S3 bucket.
17
+
18
+ Args:
19
+ file_key (str): The key of the file in the S3 bucket, including extension.
20
+ target_dir (str): The path to the directory to save the downloaded file.
21
+ bucket (str, optional): The name of the S3 bucket.
22
+
23
+ Returns:
24
+ Optional[str]: The path to the file or None if the download fails.
25
+ """
26
+ session = aioboto3.Session()
27
+ target_path = os.path.join(target_dir, file_key)
28
+
29
+ async with session.client(
30
+ 's3',
31
+ aws_access_key_id=AWS_ACCESS_KEY_ID,
32
+ aws_secret_access_key=AWS_SECRET_ACCESS_KEY
33
+ ) as s3_client:
34
+ try:
35
+ os.makedirs(target_dir, exist_ok=True)
36
+ if os.path.exists(target_path):
37
+ print(f'{file_key} already exists in {target_dir}')
38
+ return target_path
39
+ with open(target_path, 'wb') as f:
40
+ match file_key.split('.')[-1]:
41
+ case 'mp4':
42
+ await s3_client.download_fileobj(bucket, os.path.join(S3_VIDEO_PATH, file_key), f)
43
+ case 'glb'|'obj'|'stl'|'gltf'|'splat'|'ply':
44
+ await s3_client.download_fileobj(bucket, os.path.join(S3_MODEL_PATH, file_key), f)
45
+ case _:
46
+ print(f"Unsupported file type: {file_key}")
47
+ raise ValueError(f"Unsupported file type: {file_key}")
48
+ return target_path
49
+ except Exception as e:
50
+ print(f'Error downloading {file_key} from bucket {bucket}: {e}')
51
+ raise e
52
+
53
+
54
+ async def read_from_s3(file_key : str, bucket : str = S3_BUCKET) -> Optional[Union[pd.DataFrame, str]]:
55
+ """
56
+ Reads a file from an S3 bucket based on its file extension and returns the appropriate data type.
57
+
58
+ Args:
59
+ file_key (str): The key of the file in the S3 bucket.
60
+ bucket (str, optional): The name of the S3 bucket.
61
+
62
+ Returns:
63
+ Optional[Union[pd.DataFrame, str]]:
64
+ - A pandas DataFrame if the file is a CSV.
65
+ - A temporary file path (str) if the file is a GLB.
66
+ - A presigned URL (str) if the file is an MP4.
67
+ - None if the file type is unsupported.
68
+ """
69
+ session = aioboto3.Session()
70
+ async with session.client(
71
+ 's3',
72
+ aws_access_key_id=AWS_ACCESS_KEY_ID,
73
+ aws_secret_access_key=AWS_SECRET_ACCESS_KEY
74
+ ) as s3_client:
75
+ try:
76
+ match file_key.split('.')[-1]:
77
+ case 'csv':
78
+ response = await s3_client.get_object(Bucket=bucket, Key=os.path.join(S3_DATA_PATH, file_key))
79
+ content = await response['Body'].read()
80
+ result = pd.read_csv(StringIO(content.decode("utf-8")))
81
+ return result
82
+ case _:
83
+ print(f"Unsupported file type for reading: {file_key}")
84
+ raise ValueError(f"Unsupported file type for reading: {file_key}")
85
+ except Exception as e:
86
+ print(f'Error reading {file_key} from bucket {bucket}: {e}')
87
+ raise e
88
+
89
+
90
+ async def write_to_s3(file_key : str, dataframe: pd.DataFrame, bucket : str = S3_BUCKET) -> None:
91
+ """
92
+ Writes a pandas DataFrame to an S3 bucket as a CSV file.
93
+
94
+ Args:
95
+ file_key (str): The key (file name) under which the file will be stored in the S3 bucket.
96
+ dataframe (pd.DataFrame): The pandas DataFrame to write to the S3 bucket.
97
+ bucket (str, optional): The name of the S3 bucket.
98
+
99
+ Raises:
100
+ Exception: Reraises any exception encountered during the write process.
101
+ """
102
+ session = aioboto3.Session()
103
+ async with session.client(
104
+ 's3',
105
+ aws_access_key_id=AWS_ACCESS_KEY_ID,
106
+ aws_secret_access_key=AWS_SECRET_ACCESS_KEY
107
+ ) as s3_client:
108
+ try:
109
+ match file_key.split('.')[-1]:
110
+ case 'csv':
111
+ csv_buffer = StringIO()
112
+ dataframe.to_csv(csv_buffer, index=False)
113
+ await s3_client.put_object(
114
+ Bucket=bucket,
115
+ Key=os.path.join(S3_DATA_PATH, file_key),
116
+ Body=csv_buffer.getvalue()
117
+ )
118
+ case _:
119
+ print(f"Unsupported file type for writing: {file_key}")
120
+ raise ValueError(f"Unsupported file type for writing: {file_key}")
121
+ except Exception as e:
122
+ print(f'Error writing {file_key} to bucket {bucket}: {e}')
123
+ raise e
utils/utils.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+ import pandas as pd
3
+ import numpy as np
4
+ import time
5
+ import asyncio
6
+ from utils.s3_utils import write_to_s3
7
+ from utils.data_utils import generate_leaderboard, generate_data
8
+
9
+ submit_lock = asyncio.Lock()
10
+
11
+ def update_ratings(R_win : int, R_lose : int, k : int = 32) -> Tuple[int, int]:
12
+ """
13
+ Update the ratings of two players after a match.
14
+
15
+ Args:
16
+ R_win (int): The rating of the winning player.
17
+ R_lose (int): The rating of the losing player.
18
+ k (int, optional): The k-factor. Defaults to 32.
19
+
20
+ Returns:
21
+ Tuple[int, int]: The updated ratings of the winning and losing players.
22
+ """
23
+ E_win = 1 / (1 + 10 ** ((R_lose - R_win) / 480))
24
+ E_lose = 1 / (1 + 10 ** ((R_win - R_lose) / 480))
25
+ return int(R_win + k * (1 - E_win)), int(R_lose + k * (0 - E_lose))
26
+
27
+ def generate_matchup(leaderboard : pd.DataFrame, beta : int) -> tuple[str, str]:
28
+ """
29
+ Generate a pseudo-random matchup between two models.
30
+
31
+ Args:
32
+ leaderboard (pd.DataFrame): The leaderboard of models
33
+ beta (int): The damping factor for the Elo update.
34
+
35
+ Returns:
36
+ model1 (str): The first model.
37
+ model2 (str): The second model.
38
+ """
39
+ if leaderboard['Matches'].sum() == 0:
40
+ return np.random.choice(leaderboard.index, 2, replace=False)
41
+ weights = [np.exp(-leaderboard.at[model, 'Matches'] / beta) for model in leaderboard.index]
42
+ weights = weights / np.sum(weights) # Normalize weights
43
+ selected = np.random.choice(leaderboard.index, 2, replace=False, p=weights)
44
+ np.random.shuffle(selected)
45
+ model1, model2 = selected
46
+ return model1, model2
47
+
48
+ async def simulate(iter : int, beta : int, criteria : str) -> pd.DataFrame:
49
+ """
50
+ Simulate matches between random models.
51
+
52
+ Args:
53
+ iter (int): The number of matches to simulate.
54
+ beta (int): The damping factor for the Elo update.
55
+ criteria (str): The criteria for the rating.
56
+
57
+ Returns:
58
+ leaderboard (pd.DataFrame): Updated leaderboard after simulation
59
+ """
60
+ data = await generate_data()
61
+
62
+ leaderboard = await generate_leaderboard(criteria)
63
+ leaderboard.set_index('Model', inplace=True)
64
+
65
+ for _ in range(iter):
66
+ # Generate random matchups
67
+ timestamp = time.time()
68
+ model1, model2 = generate_matchup(leaderboard, beta)
69
+ R1, R2 = leaderboard.at[model1, 'Elo'], leaderboard.at[model2, 'Elo']
70
+ R1_new, R2_new = update_ratings(R1, R2)
71
+
72
+ # Update leaderboard
73
+ leaderboard.at[model1, 'Elo'], leaderboard.at[model2, 'Elo'] = R1_new, R2_new
74
+ leaderboard.at[model1, 'Wins'] += 1
75
+ leaderboard.at[model1, 'Matches'] += 1
76
+ leaderboard.at[model2, 'Matches'] += 1
77
+ leaderboard.at[model1, 'Win Rate'] = np.round(leaderboard.at[model1, 'Wins'] / leaderboard.at[model1, 'Matches'], 2)
78
+ leaderboard.at[model2, 'Win Rate'] = np.round(leaderboard.at[model2, 'Wins'] / leaderboard.at[model2, 'Matches'], 2)
79
+
80
+ # Save match data
81
+ data.loc[len(data)] = {
82
+ 'Criteria': criteria,
83
+ 'Model': model1,
84
+ 'Opponent': model2,
85
+ 'Won': True,
86
+ 'Elo': leaderboard.at[model1, 'Elo'],
87
+ 'Win Rate': leaderboard.at[model1, 'Win Rate'],
88
+ 'Matches': leaderboard.at[model1, 'Matches'],
89
+ 'Timestamp': timestamp,
90
+ 'UUID': None
91
+ }
92
+
93
+ data.loc[len(data)] = {
94
+ 'Criteria': criteria,
95
+ 'Model': model2,
96
+ 'Opponent': model1,
97
+ 'Won': False,
98
+ 'Elo': leaderboard.at[model2, 'Elo'],
99
+ 'Win Rate': leaderboard.at[model2, 'Win Rate'],
100
+ 'Matches': leaderboard.at[model2, 'Matches'],
101
+ 'Timestamp': timestamp,
102
+ 'UUID': None
103
+ }
104
+
105
+ leaderboard = leaderboard.sort_values('Elo', ascending=False).reset_index(drop=False)
106
+
107
+ await asyncio.gather(
108
+ write_to_s3(f'leaderboard_{criteria}.csv', leaderboard),
109
+ write_to_s3('data.csv', data)
110
+ )
111
+
112
+ return leaderboard
113
+
114
+
115
+ async def submit_rating(criteria : str, winner : str, loser : str, uuid : str) -> None:
116
+ """
117
+ Submit a rating for a match.
118
+
119
+ Args:
120
+ criteria (str): The criteria for the rating.
121
+ winner (str): The winning model.
122
+ loser (str): The losing model.
123
+ uuid (str): The UUID of the session.
124
+ """
125
+ async with submit_lock:
126
+ data = await generate_data()
127
+
128
+ leaderboard = await generate_leaderboard(criteria)
129
+ leaderboard.set_index('Model', inplace=True)
130
+
131
+ if winner is None or loser is None:
132
+ return leaderboard
133
+
134
+ timestamp = time.time()
135
+ R_win, R_lose = leaderboard.at[winner, 'Elo'], leaderboard.at[loser, 'Elo']
136
+ R_win_new, R_lose_new = update_ratings(R_win, R_lose)
137
+
138
+ # Update leaderboard
139
+ leaderboard.loc[[winner, loser], 'Elo'] = [R_win_new, R_lose_new]
140
+ leaderboard.at[winner, 'Wins'] += 1
141
+ leaderboard.loc[[winner, loser], 'Matches'] += [1, 1]
142
+ leaderboard.loc[[winner, loser], 'Win Rate'] = (
143
+ leaderboard.loc[[winner, loser], 'Wins'] / leaderboard.loc[[winner, loser], 'Matches']
144
+ ).apply(lambda x: round(x, 2))
145
+
146
+ # Save match data
147
+ data.loc[len(data)] = {
148
+ 'Criteria': criteria,
149
+ 'Model': winner,
150
+ 'Opponent': loser,
151
+ 'Won': True,
152
+ 'Elo': leaderboard.at[winner, 'Elo'],
153
+ 'Win Rate': leaderboard.at[winner, 'Win Rate'],
154
+ 'Matches': leaderboard.at[winner, 'Matches'],
155
+ 'Timestamp': timestamp,
156
+ 'UUID': uuid
157
+ }
158
+
159
+ data.loc[len(data)] = {
160
+ 'Criteria': criteria,
161
+ 'Model': loser,
162
+ 'Opponent': winner,
163
+ 'Won': False,
164
+ 'Elo': leaderboard.at[loser, 'Elo'],
165
+ 'Win Rate': leaderboard.at[loser, 'Win Rate'],
166
+ 'Matches': leaderboard.at[loser, 'Matches'],
167
+ 'Timestamp': timestamp,
168
+ 'UUID': uuid
169
+ }
170
+
171
+ leaderboard = leaderboard.sort_values('Elo', ascending=False).reset_index(drop=False)
172
+ await asyncio.gather(
173
+ write_to_s3(f'leaderboard_{criteria}.csv', leaderboard),
174
+ write_to_s3('data.csv', data)
175
+ )
176
+ return leaderboard