davanstrien HF staff commited on
Commit
ad7917a
1 Parent(s): 2edc44c

first commit

Browse files
Files changed (3) hide show
  1. app.py +453 -0
  2. requirements.in +7 -0
  3. requirements.txt +234 -0
app.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TODO
2
+ # Remove duplication in code used to generate markdown
3
+ # periodically update models to check all still valid and public
4
+
5
+ import os
6
+ import re
7
+ import sys
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+ from typing import Dict, List, Set, Union
11
+
12
+ import gradio as gr
13
+ from apscheduler.schedulers.background import BackgroundScheduler
14
+ from apscheduler.triggers.cron import CronTrigger
15
+ from cachetools import TTLCache, cached
16
+ from diskcache import Cache
17
+ from dotenv import load_dotenv
18
+ from huggingface_hub import (
19
+ HfApi,
20
+ comment_discussion,
21
+ create_discussion,
22
+ dataset_info,
23
+ get_repo_discussions,
24
+ )
25
+ from huggingface_hub.utils import HFValidationError, RepositoryNotFoundError
26
+ from sqlitedict import SqliteDict
27
+ from toolz import concat, count, unique
28
+ from tqdm.auto import tqdm
29
+ from tqdm.contrib.concurrent import thread_map
30
+
31
+ local = bool(sys.platform.startswith("darwin"))
32
+ cache_location = "cache/" if local else "/data/cache"
33
+
34
+ save_dir = "test_data" if local else "/data/"
35
+ Path(save_dir).mkdir(parents=True, exist_ok=True)
36
+ cache = Cache(cache_location)
37
+ load_dotenv()
38
+ user_agent = os.getenv("USER_AGENT")
39
+ HF_TOKEN = os.getenv("HF_TOKEN")
40
+ REPO = "librarian-bots/dataset-to-model-monitor" # where issues land
41
+ AUTHOR = "librarian-bot" # who makes the issues
42
+ hf_api = HfApi(user_agent=user_agent)
43
+
44
+ ten_min_cache = TTLCache(maxsize=5_000, ttl=600)
45
+
46
+
47
+ @cached(cache=ten_min_cache)
48
+ def get_datasets_for_user(username: str) -> List[str]:
49
+ datasets = hf_api.list_datasets(author=username)
50
+ datasets = (dataset.id for dataset in datasets)
51
+ return datasets
52
+
53
+
54
+ @cached(cache=ten_min_cache)
55
+ def get_models_for_dataset(dataset_id):
56
+ results = list(iter(hf_api.list_models(filter=f"dataset:{dataset_id}")))
57
+ if results:
58
+ results = list({result.id for result in results})
59
+ return {dataset_id: results}
60
+
61
+
62
+ def generate_dataset_model_map(
63
+ dataset_ids: List[str],
64
+ ) -> dict[str, dict[str, List[str]]]:
65
+ results = thread_map(get_models_for_dataset, dataset_ids)
66
+ results = {key: value for d in results for key, value in d.items()}
67
+ return results
68
+
69
+
70
+ def maybe_update_datasets_to_model_map(dataset_id):
71
+ with SqliteDict(f"{save_dir}/models_to_dataset.sqlite") as dataset_to_model_map_db:
72
+ if dataset_id not in dataset_to_model_map_db:
73
+ dataset_to_model_map_db[dataset_id] = list(
74
+ get_models_for_dataset(dataset_id)[dataset_id]
75
+ )
76
+ dataset_to_model_map_db.commit()
77
+ return len(dataset_to_model_map_db)
78
+ return False
79
+
80
+
81
+ def datasets_tracked_by_user(username):
82
+ with SqliteDict(
83
+ f"{save_dir}/tracked_dataset_to_users.sqlite"
84
+ ) as tracked_dataset_to_users_db:
85
+ return [
86
+ dataset
87
+ for dataset, users in tracked_dataset_to_users_db.items()
88
+ if username in users
89
+ ]
90
+
91
+
92
+ def update_tracked_dataset_to_users(dataset_id: str, username: str):
93
+ with SqliteDict(
94
+ f"{save_dir}/tracked_dataset_to_users.sqlite",
95
+ ) as tracked_dataset_to_users_db:
96
+ if dataset_id in tracked_dataset_to_users_db:
97
+ # check if user already tracking dataset
98
+ if username not in tracked_dataset_to_users_db[dataset_id]:
99
+ users_for_dataset = tracked_dataset_to_users_db[dataset_id]
100
+ users_for_dataset.append(username)
101
+ tracked_dataset_to_users_db[dataset_id] = list(set(users_for_dataset))
102
+ tracked_dataset_to_users_db.commit()
103
+ else:
104
+ tracked_dataset_to_users_db[dataset_id] = [username]
105
+ tracked_dataset_to_users_db.commit()
106
+ return datasets_tracked_by_user(username)
107
+
108
+
109
+ HUB_ORG_OR_USERNAME_GLOB_PATTERN = re.compile(r"^([^/]+)(?=/)")
110
+
111
+
112
+ @lru_cache(maxsize=128)
113
+ def match_org_user_glob_pattern(hub_id):
114
+ if match := re.match(HUB_ORG_OR_USERNAME_GLOB_PATTERN, hub_id):
115
+ return match[1]
116
+ else:
117
+ return None
118
+
119
+
120
+ @cached(cache=TTLCache(maxsize=100, ttl=60))
121
+ def grab_dataset_ids_for_user_or_org(hub_id: str) -> List[str]:
122
+ datasets_for_org = hf_api.list_datasets(author=hub_id)
123
+ datasets_for_org = (
124
+ dataset for dataset in datasets_for_org if dataset.private is False
125
+ )
126
+ return [dataset.id for dataset in datasets_for_org]
127
+
128
+
129
+ @cached(cache=TTLCache(maxsize=100, ttl=60))
130
+ def parse_hub_id_entry(hub_id: str) -> Union[str, List[str]]:
131
+ if match := match_org_user_glob_pattern(hub_id):
132
+ return grab_dataset_ids_for_user_or_org(match), match
133
+ try:
134
+ dataset_info(hub_id)
135
+ return hub_id, match
136
+ except HFValidationError as e:
137
+ raise gr.Error(f"Invalid format for Hugging Face Hub dataset ID. {e}") from e
138
+ except RepositoryNotFoundError as e:
139
+ raise gr.Error("Invalid Hugging Face Hub dataset ID") from e
140
+
141
+
142
+ def remove_user_from_tracking_datasets(dataset_id, profile: gr.OAuthProfile | None):
143
+ if not profile and not local:
144
+ return "You must be logged in to remove a dataset"
145
+ username = profile.preferred_username
146
+ dataset_id, match = parse_hub_id_entry(dataset_id)
147
+ if isinstance(dataset_id, str):
148
+ return _remove_user_from_tracking_datasets(dataset_id, username)
149
+ if isinstance(dataset_id, list):
150
+ [
151
+ _remove_user_from_tracking_datasets(dataset, username)
152
+ for dataset in dataset_id
153
+ ]
154
+ return f"Stopped tracking datasets for username or org: {match}"
155
+
156
+
157
+ def _remove_user_from_tracking_datasets(dataset_id: str, username):
158
+ with SqliteDict(
159
+ f"{save_dir}/tracked_dataset_to_users.sqlite"
160
+ ) as tracked_dataset_to_users_db:
161
+ users = tracked_dataset_to_users_db.get(dataset_id)
162
+ if users is None:
163
+ return "Dataset not being tracked"
164
+ try:
165
+ users.remove(username)
166
+ except ValueError:
167
+ return "No longer tracking dataset"
168
+ tracked_dataset_to_users_db[dataset_id] = users
169
+ if len(users) < 1:
170
+ del tracked_dataset_to_users_db[dataset_id]
171
+ with SqliteDict(
172
+ f"{save_dir}/models_to_dataset.sqlite"
173
+ ) as dataset_to_models_db:
174
+ del dataset_to_models_db[dataset_id]
175
+ dataset_to_models_db.commit()
176
+ tracked_dataset_to_users_db.commit()
177
+ return "Dataset no longer being tracked"
178
+
179
+
180
+ def user_unsubscribe_all(username):
181
+ datasets_tracked = datasets_tracked_by_user(username)
182
+ for dataset_id in datasets_tracked:
183
+ remove_user_from_tracking_datasets(username, dataset_id)
184
+ assert len(datasets_tracked_by_user(username)) == 0
185
+ return f"Unsubscribed from {len(datasets_tracked)} datasets"
186
+
187
+
188
+ def user_update(hub_id, profile: gr.OAuthProfile | None):
189
+ if not profile and not local:
190
+ return "Please login to track a dataset"
191
+ username = profile.preferred_username
192
+ hub_id, match = parse_hub_id_entry(hub_id)
193
+ if isinstance(hub_id, str):
194
+ return _user_update(hub_id, username)
195
+ else:
196
+ return glob_update_tracked_datasets(hub_id, username, match)
197
+
198
+
199
+ def glob_update_tracked_datasets(hub_ids, username, match):
200
+ for id_ in tqdm(hub_ids):
201
+ _user_update(id_, username)
202
+ response = "## Dataset tracking summary \n\n"
203
+ response += (
204
+ f"All datasets under the user or organization: {match} are being tracked \n\n"
205
+ )
206
+ tracked_datasets = datasets_tracked_by_user(username)
207
+ response += (
208
+ "You are currently tracking whether new models have been trained on"
209
+ f" {len(tracked_datasets)} datasets.\n\n"
210
+ )
211
+ if tracked_datasets:
212
+ response += "### Datasets being tracked \n\n"
213
+ response += (
214
+ "You are currently monitoring whether new models have been trained on the"
215
+ " following datasets:\n"
216
+ )
217
+ for dataset in tracked_datasets:
218
+ response += f"- [{dataset}](https://huggingface.co/datasets/{dataset})\n"
219
+ return response
220
+
221
+
222
+ def _user_update(hub_id: str, username: str) -> str:
223
+ """Update the user's tracked datasets and return a response string."""
224
+ response = ""
225
+ if number_datasets_being_tracked := maybe_update_datasets_to_model_map(hub_id):
226
+ response += (
227
+ "New dataset being tracked! Now tracking"
228
+ f" {number_datasets_being_tracked} datasets \n\n"
229
+ )
230
+ if not number_datasets_being_tracked:
231
+ response += f"Dataset {hub_id} is already being tracked. \n\n"
232
+ datasets_tracked_by_user = update_tracked_dataset_to_users(hub_id, username)
233
+ response += (
234
+ "You are currently whether new models have been trained on"
235
+ f" {len(datasets_tracked_by_user)} datasets."
236
+ )
237
+ if datasets_tracked_by_user:
238
+ response += (
239
+ "\nYou are currently monitoring whether new models have been trained on the"
240
+ " following datasets:\n"
241
+ )
242
+ for dataset in datasets_tracked_by_user:
243
+ response += f"- [{dataset}](https://huggingface.co/datasets/{dataset})\n"
244
+ else:
245
+ response += "You are not currently tracking any datasets."
246
+ return response
247
+
248
+
249
+ def check_for_new_models_for_dataset_and_update() -> Dict[str, Set[str]]:
250
+ # if not Path(f"{save_dir}/models_to_dataset.json").is_file():
251
+ with SqliteDict(f"{save_dir}/models_to_dataset.sqlite") as old_results_db:
252
+ dataset_ids = list(old_results_db.keys())
253
+ new_results = generate_dataset_model_map(dataset_ids)
254
+ models_to_notify_about = {
255
+ dataset_id: set(models).difference(set(old_results_db[dataset_id]))
256
+ for dataset_id, models in new_results.items()
257
+ if len(models) > len(old_results_db[dataset_id])
258
+ }
259
+ for dataset_id, models in new_results.items():
260
+ old_results_db[dataset_id] = models
261
+ old_results_db.commit()
262
+ return models_to_notify_about
263
+
264
+
265
+ def get_repo_discussion_by_author_and_type(
266
+ repo, author, token, repo_type="space", include_prs=False
267
+ ):
268
+ discussions = get_repo_discussions(repo, repo_type=repo_type, token=token)
269
+ for discussion in discussions:
270
+ if discussion.author == author:
271
+ if not include_prs and discussion.is_pull_request:
272
+ continue
273
+ yield discussion
274
+
275
+
276
+ def create_discussion_text_body(dataset_id, new_models, users_to_notify):
277
+ usernames = [f"@{username}" for username in users_to_notify]
278
+ usernames_string = ", ".join(usernames)
279
+ dataset_id_markdown_url = (
280
+ f"[{dataset_id}](https://huggingface.co/datasets/{dataset_id})"
281
+ )
282
+ description = (
283
+ f"Hey {usernames_string}! Librarian bot found new models trained on the"
284
+ f" {dataset_id_markdown_url} dataset!\n\n"
285
+ )
286
+ description += f"New model trained on {dataset_id}:\n"
287
+ markdown_items = [
288
+ f"- {hub_id_to_huggingface_hub_url_markdown(model)}" for model in new_models
289
+ ]
290
+ markdown_list = "\n".join(markdown_items)
291
+ description += markdown_list
292
+ return description
293
+
294
+
295
+ def maybe_create_discussion(
296
+ repo: str,
297
+ dataset_id: str,
298
+ new_models: Union[List, str],
299
+ users_to_notify: List[str],
300
+ author: str,
301
+ token: str,
302
+ ):
303
+ title = f"Discussion tracking new models trained on {dataset_id}"
304
+ discussions = get_repo_discussion_by_author_and_type(repo, author, HF_TOKEN)
305
+ if discussions_for_dataset := next(
306
+ (discussion for discussion in discussions if title == discussion.title),
307
+ None,
308
+ ):
309
+ discussion_id = discussions_for_dataset.num
310
+ description = create_discussion_text_body(
311
+ dataset_id, new_models, users_to_notify
312
+ )
313
+ comment_discussion(
314
+ repo, discussion_id, description, token=token, repo_type="space"
315
+ )
316
+ else:
317
+ description = create_discussion_text_body(
318
+ dataset_id, new_models, users_to_notify
319
+ )
320
+ create_discussion(
321
+ repo,
322
+ title,
323
+ token=token,
324
+ description=description,
325
+ repo_type="space",
326
+ )
327
+
328
+
329
+ def hub_id_to_huggingface_hub_url_markdown(hub_id: str) -> str:
330
+ return f"[{hub_id}](https://huggingface.co/{hub_id})"
331
+
332
+
333
+ def notify_about_new_models():
334
+ print("running notifications")
335
+ if models_to_notify_about := check_for_new_models_for_dataset_and_update():
336
+ for dataset_id, new_models in models_to_notify_about.items():
337
+ with SqliteDict(
338
+ f"{save_dir}/tracked_dataset_to_users.sqlite"
339
+ ) as tracked_dataset_to_users_db:
340
+ users_to_notify = tracked_dataset_to_users_db.get(dataset_id)
341
+ maybe_create_discussion(
342
+ REPO, dataset_id, new_models, users_to_notify, AUTHOR, HF_TOKEN
343
+ )
344
+ print("notified about new models")
345
+
346
+
347
+ def number_of_users_tracking_datasets():
348
+ with SqliteDict(
349
+ f"{save_dir}/tracked_dataset_to_users.sqlite"
350
+ ) as tracked_dataset_to_users_db:
351
+ return count(unique(concat(iter(tracked_dataset_to_users_db.values()))))
352
+
353
+
354
+ def number_of_datasets_tracked():
355
+ with SqliteDict(f"{save_dir}/models_to_dataset.sqlite") as datasets_to_models_db:
356
+ return len(datasets_to_models_db)
357
+
358
+
359
+ @cached(cache=ten_min_cache)
360
+ def generate_summary_stats():
361
+ return (
362
+ f"Currently there are {number_of_users_tracking_datasets()} users tracking"
363
+ f" datasets with a total of {number_of_datasets_tracked()} datasets being"
364
+ " tracked"
365
+ )
366
+
367
+
368
+ def _user_stats(username: str):
369
+ if not (tracked_datasets := datasets_tracked_by_user(username)):
370
+ return "You are not currently tracking any datasets"
371
+ response = (
372
+ "You are currently tracking whether new models have been trained on"
373
+ f" {len(tracked_datasets)} datasets.\n\n"
374
+ )
375
+ response += "### Datasets being tracked \n\n"
376
+ response += (
377
+ "You are currently monitoring whether new models have been trained on the"
378
+ " following datasets:\n"
379
+ )
380
+ for dataset in tracked_datasets:
381
+ response += f"- [{dataset}](https://huggingface.co/datasets/{dataset})\n"
382
+ return response
383
+
384
+
385
+ def user_stats(profile: gr.OAuthProfile | None):
386
+ if not profile and not local:
387
+ return "You must be logged in to remove a dataset"
388
+ username = profile.preferred_username
389
+ return _user_stats(username)
390
+
391
+
392
+ markdown_text = """
393
+ The Hugging Face Hub allows users to specify the dataset used to train a model in the model metadata.
394
+ This metadata allows you to find models trained on a particular dataset.
395
+ These links can be very powerful for finding models that might be suitable for a particular task.\n\n
396
+
397
+ This Gradio app allows you to track datasets hosted on the Hugging Face Hub and get a notification when new models are trained on the dataset you are tracking.
398
+ 1. Submit the Hugging Face Hub ID for the dataset you are interested in tracking.
399
+ 2. If a new model is listed as being trained on this dataset Librarian Bot will ping you in a discussion on the Hugging Face Hub to let you know.
400
+ 3. Librarian Bot will check for new models for a particular dataset once a day.
401
+
402
+ **Tip** *You can use a wildcard `*` to track all datasets for a user or organization on the hub. For example `biglam/*` will create alerts for all the datasets under the biglam Hugging Face Organization*
403
+
404
+
405
+ **You need to be logged in to your Hugging Face account to use this app.** If you don't have a Hugging Face Hub account you can get one <a href="https://huggingface.co/join">here</a>.
406
+ """
407
+
408
+ with gr.Blocks() as demo:
409
+ gr.Markdown(
410
+ '<div style="text-align: center;"><h1> &#129302; Librarian Bot Dataset-to-Model'
411
+ ' Monitor &#129302; </h1><i><p style="font-size: 20px;">✨ Get alerts when a new'
412
+ " model is created from a dataset you are interested in! ✨</p></i></div>"
413
+ )
414
+
415
+ with gr.Row():
416
+ gr.Markdown(markdown_text)
417
+ with gr.Row():
418
+ hub_id = gr.Textbox(
419
+ "i.e. biglam/brill_iconclass",
420
+ label="Hugging Face Hub ID for dataset to track",
421
+ )
422
+ with gr.Column():
423
+ track_button = gr.Button("Track new models for dataset")
424
+ with gr.Row():
425
+ remove_specific_datasets = gr.Button("Stop tracking dataset")
426
+ remove_all = gr.Button("⛔️ Unsubscribe from all datasets ⛔️")
427
+ with gr.Row(variant="compact"):
428
+ gr.LoginButton(size="sm")
429
+ gr.LogoutButton(size="sm")
430
+ summary_stats_btn = gr.Button(
431
+ "Summary stats for datasets being tracked by this app", size="sm"
432
+ )
433
+ user_stats_btn = gr.Button("List my tracked datasets", size="sm")
434
+ with gr.Row():
435
+ output = gr.Markdown()
436
+ track_button.click(user_update, [hub_id], output)
437
+ remove_specific_datasets.click(
438
+ remove_user_from_tracking_datasets, [hub_id], output
439
+ )
440
+ summary_stats_btn.click(generate_summary_stats, [], output)
441
+ user_stats_btn.click(user_stats, [], output)
442
+ scheduler = BackgroundScheduler()
443
+
444
+ if local:
445
+ scheduler.add_job(notify_about_new_models, "interval", minutes=30)
446
+ else:
447
+ scheduler.add_job(
448
+ notify_about_new_models,
449
+ CronTrigger.from_crontab("0 */12 * * *"),
450
+ )
451
+ scheduler.start()
452
+ demo.queue(max_size=5)
453
+ demo.launch()
requirements.in ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ apscheduler
2
+ gradio[oauth]==3.40.1
3
+ huggingface_hub
4
+ python-dotenv
5
+ tqdm
6
+ sqlitedict
7
+ cachetools
requirements.txt ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.11
3
+ # by the following command:
4
+ #
5
+ # pip-compile requirements.in
6
+ #
7
+ aiofiles==23.2.1
8
+ # via gradio
9
+ aiohttp==3.8.5
10
+ # via gradio
11
+ aiosignal==1.3.1
12
+ # via aiohttp
13
+ altair==5.0.1
14
+ # via gradio
15
+ anyio==3.7.1
16
+ # via
17
+ # httpcore
18
+ # starlette
19
+ apscheduler==3.10.1
20
+ # via -r requirements.in
21
+ async-timeout==4.0.3
22
+ # via aiohttp
23
+ attrs==23.1.0
24
+ # via
25
+ # aiohttp
26
+ # jsonschema
27
+ # referencing
28
+ authlib==1.2.1
29
+ # via gradio
30
+ cachetools==5.3.1
31
+ # via -r requirements.in
32
+ certifi==2023.7.22
33
+ # via
34
+ # httpcore
35
+ # httpx
36
+ # requests
37
+ cffi==1.15.1
38
+ # via cryptography
39
+ charset-normalizer==3.2.0
40
+ # via
41
+ # aiohttp
42
+ # requests
43
+ click==8.1.6
44
+ # via uvicorn
45
+ contourpy==1.1.0
46
+ # via matplotlib
47
+ cryptography==41.0.3
48
+ # via authlib
49
+ cycler==0.11.0
50
+ # via matplotlib
51
+ fastapi==0.101.0
52
+ # via gradio
53
+ ffmpy==0.3.1
54
+ # via gradio
55
+ filelock==3.12.2
56
+ # via huggingface-hub
57
+ fonttools==4.42.0
58
+ # via matplotlib
59
+ frozenlist==1.4.0
60
+ # via
61
+ # aiohttp
62
+ # aiosignal
63
+ fsspec==2023.6.0
64
+ # via
65
+ # gradio-client
66
+ # huggingface-hub
67
+ gradio[oauth]==3.40.1
68
+ # via -r requirements.in
69
+ gradio-client==0.4.0
70
+ # via gradio
71
+ h11==0.14.0
72
+ # via
73
+ # httpcore
74
+ # uvicorn
75
+ httpcore==0.17.3
76
+ # via httpx
77
+ httpx==0.24.1
78
+ # via
79
+ # gradio
80
+ # gradio-client
81
+ huggingface-hub==0.16.4
82
+ # via
83
+ # -r requirements.in
84
+ # gradio
85
+ # gradio-client
86
+ idna==3.4
87
+ # via
88
+ # anyio
89
+ # httpx
90
+ # requests
91
+ # yarl
92
+ importlib-resources==6.0.1
93
+ # via gradio
94
+ itsdangerous==2.1.2
95
+ # via gradio
96
+ jinja2==3.1.2
97
+ # via
98
+ # altair
99
+ # gradio
100
+ jsonschema==4.19.0
101
+ # via altair
102
+ jsonschema-specifications==2023.7.1
103
+ # via jsonschema
104
+ kiwisolver==1.4.4
105
+ # via matplotlib
106
+ linkify-it-py==2.0.2
107
+ # via markdown-it-py
108
+ markdown-it-py[linkify]==2.2.0
109
+ # via
110
+ # gradio
111
+ # mdit-py-plugins
112
+ markupsafe==2.1.3
113
+ # via
114
+ # gradio
115
+ # jinja2
116
+ matplotlib==3.7.2
117
+ # via gradio
118
+ mdit-py-plugins==0.3.3
119
+ # via gradio
120
+ mdurl==0.1.2
121
+ # via markdown-it-py
122
+ multidict==6.0.4
123
+ # via
124
+ # aiohttp
125
+ # yarl
126
+ numpy==1.25.2
127
+ # via
128
+ # altair
129
+ # contourpy
130
+ # gradio
131
+ # matplotlib
132
+ # pandas
133
+ orjson==3.9.4
134
+ # via gradio
135
+ packaging==23.1
136
+ # via
137
+ # gradio
138
+ # gradio-client
139
+ # huggingface-hub
140
+ # matplotlib
141
+ pandas==2.0.3
142
+ # via
143
+ # altair
144
+ # gradio
145
+ pillow==10.0.0
146
+ # via
147
+ # gradio
148
+ # matplotlib
149
+ pycparser==2.21
150
+ # via cffi
151
+ pydantic==1.10.12
152
+ # via
153
+ # fastapi
154
+ # gradio
155
+ pydub==0.25.1
156
+ # via gradio
157
+ pyparsing==3.0.9
158
+ # via matplotlib
159
+ python-dateutil==2.8.2
160
+ # via
161
+ # matplotlib
162
+ # pandas
163
+ python-dotenv==1.0.0
164
+ # via -r requirements.in
165
+ python-multipart==0.0.6
166
+ # via gradio
167
+ pytz==2023.3
168
+ # via
169
+ # apscheduler
170
+ # pandas
171
+ pyyaml==6.0.1
172
+ # via
173
+ # gradio
174
+ # huggingface-hub
175
+ referencing==0.30.2
176
+ # via
177
+ # jsonschema
178
+ # jsonschema-specifications
179
+ requests==2.31.0
180
+ # via
181
+ # gradio
182
+ # gradio-client
183
+ # huggingface-hub
184
+ rpds-py==0.9.2
185
+ # via
186
+ # jsonschema
187
+ # referencing
188
+ semantic-version==2.10.0
189
+ # via gradio
190
+ six==1.16.0
191
+ # via
192
+ # apscheduler
193
+ # python-dateutil
194
+ sniffio==1.3.0
195
+ # via
196
+ # anyio
197
+ # httpcore
198
+ # httpx
199
+ sqlitedict==2.1.0
200
+ # via -r requirements.in
201
+ starlette==0.27.0
202
+ # via fastapi
203
+ toolz==0.12.0
204
+ # via altair
205
+ tqdm==4.66.1
206
+ # via
207
+ # -r requirements.in
208
+ # huggingface-hub
209
+ typing-extensions==4.7.1
210
+ # via
211
+ # fastapi
212
+ # gradio
213
+ # gradio-client
214
+ # huggingface-hub
215
+ # pydantic
216
+ tzdata==2023.3
217
+ # via pandas
218
+ tzlocal==5.0.1
219
+ # via apscheduler
220
+ uc-micro-py==1.0.2
221
+ # via linkify-it-py
222
+ urllib3==2.0.4
223
+ # via requests
224
+ uvicorn==0.23.2
225
+ # via gradio
226
+ websockets==11.0.3
227
+ # via
228
+ # gradio
229
+ # gradio-client
230
+ yarl==1.9.2
231
+ # via aiohttp
232
+
233
+ # The following packages are considered to be unsafe in a requirements file:
234
+ # setuptools