catalog / app.py
bruAristimunha's picture
Case-insensitive on_hf match (HF lowercases repo slugs)
25ab447
"""EEGDash Dataset Catalog — Hugging Face Space.
Design system (kept in sync with ``style.css``):
* Typography: Inter for UI (14px base, 600 for headings), JetBrains Mono for
code snippets. Hierarchy: hero title > section titles > labels > meta.
* Palette: Okabe-Ito (colorblind-safe). Brand is #0072B2 (EEG-blue). One warm
accent #E69F00 reserved for the ``on 🤗`` flag — never decorative. Neutral
ramp is slate (#f8fafc → #0f172a).
* Encoding: categorical modality gets one Okabe-Ito hue per value. Continuous
(dataset size) is never encoded by color.
* Annotation: the hero, modality strip and detail panel each carry one
sentence of prose so the page reads as an argument, not a data dump.
"""
from __future__ import annotations
import ast
import html as _html
import json
import logging
import os
from functools import lru_cache
from pathlib import Path
import gradio as gr
import pandas as pd
from huggingface_hub import HfApi
from huggingface_hub.utils import HfHubHTTPError
HF_ORG = "EEGDash"
ROOT = Path(__file__).parent
CSV_PATH = ROOT / "dataset_summary.csv"
CSS_PATH = ROOT / "style.css"
ASSETS_DIR = ROOT / "assets"
EEGDASH_URL = "https://eegdash.org"
GITHUB_URL = "https://github.com/eegdash/EEGDash"
PYPI_URL = "https://pypi.org/project/eegdash/"
DISCORD_URL = "https://discord.gg/eegdash"
def _read_svg(name: str) -> str:
"""Read an SVG asset and strip the XML prolog so it inlines cleanly.
Inlining lets us color icons via ``currentColor`` and avoids the file
endpoint for tiny assets that would otherwise cost an extra round-trip.
"""
path = ASSETS_DIR / name
if not path.exists():
return ""
raw = path.read_text(encoding="utf-8")
# Remove XML declaration + comments (Inkscape adds both).
for marker in ("?>", "-->"):
idx = raw.rfind(marker)
if idx != -1 and idx < 300:
raw = raw[idx + len(marker):].lstrip()
return raw
ICON_GITHUB = _read_svg("github.svg")
ICON_PYPI = _read_svg("pypi.svg")
ICON_DISCORD = _read_svg("discord.svg")
SVG_MARK = _read_svg("mark.svg")
SVG_BIDS = _read_svg("bids.svg")
def _plot_iframe(name: str, *, height: int, title: str) -> str:
"""Embed a plotly plot in a sandboxed iframe.
Gradio's ``gr.HTML`` strips ``<script>`` tags for XSS safety, which
would leave plotly fragments inert. An iframe is a clean boundary:
scripts inside the child document run normally and the host page stays
safe from them.
"""
path = ASSETS_DIR / "plots" / f"{name}.html"
if not path.exists():
return ""
src = f"/gradio_api/file=assets/plots/{name}.html"
return (
f'<iframe class="eeg-plot__iframe" src="{src}" '
f'title="{title}" loading="lazy" '
f'sandbox="allow-scripts allow-same-origin allow-popups" '
f'style="width:100%;height:{height}px;border:0;display:block;"></iframe>'
)
# Asset URLs rendered client-side. Gradio 5 serves from the working dir
# through /gradio_api/file= when the path is in ``allowed_paths``.
LOGO_URL = "/gradio_api/file=assets/logo.svg"
FAVICON_URL = "/gradio_api/file=assets/favicon.ico"
RECORDING_ICON = {
"eeg": "/gradio_api/file=assets/recording/eeg.png",
"ieeg": "/gradio_api/file=assets/recording/ieeg.png",
"meg": "/gradio_api/file=assets/recording/meg.png",
}
# Okabe-Ito categorical palette — one hue per modality, reused consistently
# across the modality strip and filter chips so the reader learns the mapping
# once.
MODALITY_HUES: dict[str, str] = {
"Visual": "#0072B2",
"Auditory": "#009E73",
"Motor": "#D55E00",
"Tactile": "#CC79A7",
"Multisensory": "#E69F00",
"Resting State": "#56B4E9",
"Sleep": "#F0E442",
"Anesthesia": "#999999",
"Other": "#555555",
"Unknown": "#cbd5e1",
}
DEFAULT_HUE = "#64748b"
TABLE_COLUMNS = [
"dataset",
"author_year",
"source",
"record_modality",
"Type Subject",
"modality of exp",
"type of exp",
"n_subjects",
"n_records",
"n_tasks",
"nchans",
"sfreq",
"size",
"license",
"on_hf",
]
DISPLAY_HEADERS = {
"dataset": "Dataset",
"author_year": "Author (year)",
"source": "Source",
"record_modality": "Recording",
"Type Subject": "Pathology",
"modality of exp": "Modality",
"type of exp": "Type",
"n_subjects": "Subjects",
"n_records": "Records",
"n_tasks": "Tasks",
"nchans": "Channels",
"sfreq": "Hz",
"size": "Size",
"license": "License",
"on_hf": "🤗",
}
log = logging.getLogger(__name__)
# -------------------- Data loading --------------------
def _parse_mode_from_json_col(cell: object) -> str:
if not isinstance(cell, str) or not cell.strip():
return ""
try:
parsed = json.loads(cell)
except json.JSONDecodeError:
try:
parsed = ast.literal_eval(cell)
except (SyntaxError, ValueError):
return ""
if not parsed:
return ""
top = max(parsed, key=lambda d: d.get("count", 0))
val = top.get("val", "")
if isinstance(val, float) and val.is_integer():
val = int(val)
return str(val)
@lru_cache(maxsize=1)
def _hf_repos() -> set[str]:
try:
api = HfApi()
repos = api.list_datasets(author=HF_ORG, limit=2000)
return {r.id.split("/", 1)[-1] for r in repos}
except (HfHubHTTPError, Exception): # noqa: BLE001
return set()
def _load_catalog() -> pd.DataFrame:
df = pd.read_csv(CSV_PATH)
df["nchans"] = df["nchans_set"].apply(_parse_mode_from_json_col)
df["sfreq"] = df["sampling_freqs"].apply(_parse_mode_from_json_col)
# HF normalizes slugs to lowercase when creating repos; compare that way
# so mixed-case entries (e.g. "EEG2025r1") still flag correctly.
on_hub = {s.lower() for s in _hf_repos()}
df["on_hf"] = df["dataset"].apply(
lambda s: "✓" if str(s).lower() in on_hub else ""
)
for col in ("n_subjects", "n_records", "n_tasks"):
df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0).astype(int)
extra = ["dataset_title", "doi", "duration_hours_total"]
for col in TABLE_COLUMNS + extra:
if col not in df.columns:
df[col] = ""
df = df[TABLE_COLUMNS + extra].fillna("")
return df
# -------------------- Filtering --------------------
def _unique_sorted(series: pd.Series) -> list[str]:
return sorted({str(v).strip() for v in series if str(v).strip()})
def _filter(
df: pd.DataFrame,
query: str,
modalities: list[str],
subject_types: list[str],
sources: list[str],
licenses: list[str],
min_subjects: int,
only_on_hf: bool,
) -> pd.DataFrame:
out = df
if query:
q = query.lower().strip()
hay = (
out["dataset"].str.lower()
+ " "
+ out["author_year"].str.lower()
+ " "
+ out["dataset_title"].astype(str).str.lower()
)
out = out[hay.str.contains(q, regex=False, na=False)]
if modalities:
out = out[out["modality of exp"].isin(modalities)]
if subject_types:
out = out[out["Type Subject"].isin(subject_types)]
if sources:
out = out[out["source"].isin(sources)]
if licenses:
out = out[out["license"].isin(licenses)]
if min_subjects > 0:
out = out[out["n_subjects"] >= min_subjects]
if only_on_hf:
out = out[out["on_hf"] == "✓"]
return out
def _render_table(df: pd.DataFrame) -> pd.DataFrame:
return df[TABLE_COLUMNS].rename(columns=DISPLAY_HEADERS)
# -------------------- Hero (stats + modality strip) --------------------
def _fmt_num(n: float) -> str:
if n >= 1_000_000:
return f"{n / 1_000_000:.1f}M"
if n >= 1_000:
return f"{n / 1_000:.1f}k"
return f"{int(n):,}"
def _hero_html(df: pd.DataFrame, total_all: int) -> str:
"""Hero banner — the one thing a first-time visitor reads.
Four stat cards answer: "how big is this catalog?" at a glance. The
count is filter-aware so the banner tracks what's currently visible.
"""
subjects = int(df["n_subjects"].sum())
records = int(df["n_records"].sum())
hours_series = pd.to_numeric(df["duration_hours_total"], errors="coerce").fillna(0)
hours = float(hours_series.sum())
on_hf = int((df["on_hf"] == "✓").sum())
viewing = len(df)
return f"""
<section class="eeg-hero">
<div class="eeg-hero__left">
<img class="eeg-hero__logo" src="{LOGO_URL}" alt="EEGDash" />
<p class="eeg-hero__lede">
Open catalog of {total_all} EEG / MEG datasets. Search, filter, and
load any of them with a single line of Python — streamed from NEMAR
or mirrored to <a href="https://huggingface.co/{HF_ORG}">🤗 {HF_ORG}</a>.
</p>
<div class="eeg-hero__cta">
<a class="eeg-btn eeg-btn--primary" href="{EEGDASH_URL}" target="_blank" rel="noopener">eegdash.org <span aria-hidden="true">→</span></a>
<a class="eeg-btn eeg-btn--icon" href="{GITHUB_URL}" target="_blank" rel="noopener" aria-label="GitHub">{ICON_GITHUB}<span>GitHub</span></a>
<a class="eeg-btn eeg-btn--icon" href="{PYPI_URL}" target="_blank" rel="noopener" aria-label="PyPI">{ICON_PYPI}<span>PyPI</span></a>
</div>
</div>
<div class="eeg-hero__stats" role="group" aria-label="Catalog totals">
<div class="eeg-stat"><div class="eeg-stat__n">{_fmt_num(viewing)}</div><div class="eeg-stat__l">datasets <span class="eeg-stat__meta">of {total_all}</span></div></div>
<div class="eeg-stat"><div class="eeg-stat__n">{_fmt_num(subjects)}</div><div class="eeg-stat__l">subjects</div></div>
<div class="eeg-stat"><div class="eeg-stat__n">{_fmt_num(records)}</div><div class="eeg-stat__l">recordings</div></div>
<div class="eeg-stat eeg-stat--accent"><div class="eeg-stat__n">{on_hf}</div><div class="eeg-stat__l">on <span aria-label="Hugging Face">🤗</span></div></div>
</div>
</section>
"""
def _modality_strip_html(df: pd.DataFrame) -> str:
"""Horizontal bar of dataset counts by modality — quick shape check.
Effectiveness via length (bars), expressiveness via categorical hue.
One stacked row is enough because we're answering a single question:
which experimental paradigms dominate the catalog?
"""
counts = (
df["modality of exp"]
.replace("", "Unknown")
.value_counts()
.sort_values(ascending=False)
)
if counts.empty:
return ""
total = int(counts.sum())
segments = []
legend = []
for name, n in counts.items():
hue = MODALITY_HUES.get(str(name), DEFAULT_HUE)
pct = (n / total) * 100
# Every modality appears in the legend (user-facing single line),
# but sub-pixel bar segments get a min-width so they stay clickable.
segments.append(
f'<span class="eeg-bar__seg" style="width:{pct:.2f}%;background:{hue};min-width:2px" '
f'title="{_html.escape(str(name))}: {n}"></span>'
)
legend.append(
f'<span class="eeg-legend__item">'
f'<span class="eeg-legend__swatch" style="background:{hue}"></span>'
f"{_html.escape(str(name))} <span class='eeg-legend__n'>{n}</span>"
f"</span>"
)
return f"""
<section class="eeg-modality" aria-label="Datasets by experimental modality">
<div class="eeg-modality__head">
<span class="eeg-modality__title">By modality</span>
<span class="eeg-modality__meta">{total} datasets · {len(counts)} modalities</span>
</div>
<div class="eeg-bar" role="img" aria-label="Stacked breakdown of datasets by modality">{''.join(segments)}</div>
<div class="eeg-legend">{''.join(legend)}</div>
</section>
"""
# -------------------- Detail card (HTML) --------------------
def _e(v: object) -> str:
return _html.escape(str(v)) if v is not None else ""
def _snippet_block(label: str, code: str) -> str:
return (
f'<div class="eeg-snippet"><div class="eeg-snippet__hd">{_e(label)}</div>'
f'<pre class="eeg-snippet__code">{_e(code)}</pre></div>'
)
def _detail_html(df: pd.DataFrame, slug: str) -> str:
if not slug:
return _empty_detail()
match = df[df["dataset"] == slug]
if match.empty:
return _empty_detail()
row = match.iloc[0]
on_hf = row["on_hf"] == "✓"
title = str(row.get("dataset_title", "") or slug)
doi = str(row.get("doi", "") or "").strip()
author = str(row.get("author_year", "") or "").strip()
license_ = str(row.get("license", "") or "—").strip() or "—"
modality = str(row.get("modality of exp", "") or "").strip() or "—"
pathology = str(row.get("Type Subject", "") or "").strip() or "—"
modality_hue = MODALITY_HUES.get(modality, DEFAULT_HUE)
doi_link = (
f'<a class="eeg-tag" href="https://doi.org/{_e(doi)}" target="_blank" rel="noopener">doi:{_e(doi)}</a>'
if doi
else ""
)
hf_link = (
f'<a class="eeg-tag eeg-tag--accent" href="https://huggingface.co/datasets/{HF_ORG}/{_e(slug)}" target="_blank" rel="noopener">🤗 on Hub</a>'
if on_hf
else '<span class="eeg-tag eeg-tag--muted">not mirrored yet</span>'
)
rec_type = str(row.get("record_modality", "") or "").strip().lower()
rec_icon = RECORDING_ICON.get(rec_type)
rec_badge = (
f'<img class="eeg-card__rec" src="{rec_icon}" alt="{_e(rec_type.upper())} recording" title="{_e(rec_type.upper())} recording"/>'
if rec_icon
else ""
)
stats = [
("Subjects", _fmt_num(int(row.get("n_subjects", 0) or 0))),
("Recordings", _fmt_num(int(row.get("n_records", 0) or 0))),
("Tasks", _fmt_num(int(row.get("n_tasks", 0) or 0))),
("Channels", str(row.get("nchans", "") or "—")),
("Sampling", f"{row.get('sfreq', '') or '—'} Hz"),
("Size", str(row.get("size", "") or "—")),
]
stat_cards = "".join(
f'<div class="eeg-kv"><div class="eeg-kv__n">{_e(v)}</div><div class="eeg-kv__l">{_e(k)}</div></div>'
for k, v in stats
)
native_snippet = (
"from eegdash import EEGDashDataset\n\n"
f'ds = EEGDashDataset(dataset="{slug}", cache_dir="./cache")\n'
'print(len(ds), "recordings")'
)
if on_hf:
hub_snippet = (
"from braindecode.datasets import BaseConcatDataset\n\n"
f'ds = BaseConcatDataset.pull_from_hub("{HF_ORG}/{slug}")'
)
hub_block = _snippet_block("From 🤗 Hub (braindecode, Zarr)", hub_snippet)
else:
hub_snippet = (
"from eegdash import EEGDashDataset\n\n"
f'ds = EEGDashDataset(dataset="{slug}", cache_dir="./cache")\n'
f'ds.push_to_hub("{HF_ORG}/{slug}")'
)
hub_block = (
'<div class="eeg-note">This dataset isn’t mirrored on 🤗 yet. '
f'<a href="{GITHUB_URL}/issues">Open an issue</a> to request it '
"or push it yourself:</div>"
+ _snippet_block("Push to the Hub", hub_snippet)
)
return f"""
<article class="eeg-card" aria-labelledby="eeg-card-title">
<header class="eeg-card__hd">
<div class="eeg-card__id">
<span class="eeg-card__slug">{_e(slug)}</span>
<span class="eeg-card__modality" style="--hue:{modality_hue}">{_e(modality)}</span>
{rec_badge}
</div>
<h2 id="eeg-card-title" class="eeg-card__title">{_e(title)}</h2>
<div class="eeg-card__meta">
{f'<span class="eeg-tag">{_e(author)}</span>' if author else ''}
<span class="eeg-tag">{_e(license_)}</span>
<span class="eeg-tag">{_e(pathology)}</span>
{doi_link}
{hf_link}
</div>
</header>
<div class="eeg-card__kvs">{stat_cards}</div>
<section class="eeg-card__body">
<h3 class="eeg-card__h3">Load it</h3>
{_snippet_block("Native EEGDash (streams from S3 / NEMAR)", native_snippet)}
{hub_block}
</section>
</article>
"""
def _empty_detail() -> str:
return f"""
<article class="eeg-card eeg-card--empty">
<div class="eeg-card__ghost">
<div class="eeg-card__ghost-mark">{SVG_MARK}</div>
<div class="eeg-card__ghost-title">Pick a dataset</div>
<p>Click any row in the table to see its metadata, load snippet, and 🤗 mirror status.</p>
</div>
</article>
"""
# -------------------- Event handlers --------------------
CATALOG = _load_catalog()
TOTAL_ALL = len(CATALOG)
MODALITY_CHOICES = _unique_sorted(CATALOG["modality of exp"])
SUBJECT_CHOICES = _unique_sorted(CATALOG["Type Subject"])
SOURCE_CHOICES = _unique_sorted(CATALOG["source"])
LICENSE_CHOICES = _unique_sorted(CATALOG["license"])
def _on_select(evt: gr.SelectData, df) -> str:
if evt is None or evt.index is None:
return _empty_detail()
row_idx = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
if df is None:
return _empty_detail()
if isinstance(df, pd.DataFrame):
if df.empty or row_idx >= len(df):
return _empty_detail()
slug = str(df.iloc[row_idx, 0])
elif isinstance(df, dict) and "data" in df:
rows = df["data"]
if not rows or row_idx >= len(rows):
return _empty_detail()
slug = str(rows[row_idx][0])
else:
try:
slug = str(df[row_idx][0])
except (IndexError, TypeError, KeyError):
return _empty_detail()
return _detail_html(CATALOG, slug)
def _on_filter(
query, modalities, subject_types, sources, licenses, min_subjects, only_on_hf
):
filtered = _filter(
CATALOG,
query,
modalities,
subject_types,
sources,
licenses,
min_subjects,
only_on_hf,
)
return (
_render_table(filtered),
_hero_html(filtered, TOTAL_ALL),
_modality_strip_html(filtered),
)
# -------------------- UI assembly --------------------
CSS = CSS_PATH.read_text(encoding="utf-8") if CSS_PATH.exists() else ""
THEME = gr.themes.Base(
primary_hue=gr.themes.colors.blue,
secondary_hue=gr.themes.colors.slate,
neutral_hue=gr.themes.colors.slate,
font=(gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"),
font_mono=(
gr.themes.GoogleFont("JetBrains Mono"),
"ui-monospace",
"SFMono-Regular",
"monospace",
),
).set(
body_background_fill="#f8fafc",
body_background_fill_dark="#0b1220",
background_fill_primary="#ffffff",
background_fill_primary_dark="#111827",
border_color_primary="#e2e8f0",
border_color_primary_dark="#1f2937",
button_primary_background_fill="#0072B2",
button_primary_background_fill_hover="#005A8F",
button_primary_text_color="#ffffff",
block_radius="14px",
input_radius="10px",
body_text_color="#0f172a",
body_text_color_dark="#e2e8f0",
)
HEAD = f"""
<link rel="icon" type="image/x-icon" href="{FAVICON_URL}" />
<meta name="description" content="Search {TOTAL_ALL} open EEG / MEG datasets — EEGDash catalog." />
<meta property="og:title" content="EEGDash — EEG/MEG dataset catalog" />
<meta property="og:description" content="Open catalog of EEG/MEG datasets, loadable with one line of Python." />
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js" charset="utf-8"></script>
"""
with gr.Blocks(
title="EEGDash — EEG/MEG dataset catalog",
css=CSS,
theme=THEME,
analytics_enabled=False,
head=HEAD,
) as demo:
hero = gr.HTML(_hero_html(CATALOG, TOTAL_ALL), elem_classes=["eeg-hero-wrap"])
modality_strip = gr.HTML(
_modality_strip_html(CATALOG), elem_classes=["eeg-modality-wrap"]
)
with gr.Accordion(
"Catalog views",
open=True,
elem_classes=["eeg-overview"],
):
with gr.Tabs(elem_classes=["eeg-tabs"]):
with gr.Tab("Flow"):
gr.HTML(
'<p class="eeg-overview__lede">The catalog as a navigation '
'map. Every dataset flows from its <em>experimental '
'modality</em> (left) to its <em>clinical population</em> '
'(middle) to its <em>repository</em> (right). Ribbon '
'thickness is the dataset count along that path — follow '
'any color to see where a paradigm of interest lives.</p>'
+ _plot_iframe("dataset_sankey", height=640, title="Catalog flow (sankey)"),
elem_classes=["eeg-plot"],
)
with gr.Tab("Bubbles"):
gr.HTML(
'<p class="eeg-overview__lede">Every dataset as an '
'individual mark. Bubble size is recording count, color is '
'experimental modality, axes span subjects × duration. '
'Hover to find a specific dataset; use the filter below to '
'narrow the field.</p>'
+ _plot_iframe("dataset_bubble", height=780, title="Dataset bubble chart"),
elem_classes=["eeg-plot"],
)
with gr.Tab("Treemap"):
gr.HTML(
'<p class="eeg-overview__lede">Nested rectangles grouped by '
'modality. Area is proportional to recording count — the '
'biggest tiles are the heaviest contributors.</p>'
+ _plot_iframe("dataset_treemap", height=820, title="Dataset treemap"),
elem_classes=["eeg-plot"],
)
with gr.Tab("Growth"):
gr.HTML(
'<p class="eeg-overview__lede">New datasets added to the '
'catalog over time, colored by source. The slope tells you '
'how fast the archive has expanded.</p>'
+ _plot_iframe("dataset_growth", height=520, title="Catalog growth"),
elem_classes=["eeg-plot"],
)
with gr.Tab("Clinical"):
gr.HTML(
'<p class="eeg-overview__lede">Clinical populations '
'represented in the catalog — from healthy controls to '
'neurodegenerative and psychiatric conditions.</p>'
+ _plot_iframe("dataset_clinical", height=520, title="Clinical breakdown"),
elem_classes=["eeg-plot"],
)
with gr.Row(elem_classes=["eeg-toolbar"]):
query = gr.Textbox(
label="Search",
placeholder="Type a dataset id, author, or keyword…",
show_label=False,
elem_classes=["eeg-search"],
scale=4,
)
only_on_hf = gr.Checkbox(
label="Only 🤗-mirrored",
value=False,
elem_classes=["eeg-toggle"],
scale=1,
)
with gr.Accordion("Filters", open=False, elem_classes=["eeg-filters"]):
with gr.Row():
modalities = gr.CheckboxGroup(
label="Modality", choices=MODALITY_CHOICES, value=[]
)
subject_types = gr.CheckboxGroup(
label="Pathology / population", choices=SUBJECT_CHOICES, value=[]
)
with gr.Row():
sources = gr.CheckboxGroup(
label="Source", choices=SOURCE_CHOICES, value=[]
)
licenses = gr.Dropdown(
label="License",
choices=LICENSE_CHOICES,
multiselect=True,
value=[],
)
min_subjects = gr.Slider(
label="Minimum subjects",
minimum=0,
maximum=500,
step=10,
value=0,
)
with gr.Row(elem_classes=["eeg-main"]):
with gr.Column(scale=3, elem_classes=["eeg-main__table"]):
table = gr.Dataframe(
value=_render_table(CATALOG),
interactive=False,
wrap=False,
column_widths=[
"140px", "140px", "90px", "90px", "120px", "110px",
"140px", "85px", "85px", "60px", "80px", "70px",
"85px", "110px", "50px",
],
label=None,
show_label=False,
elem_classes=["eeg-table"],
max_height=640,
)
with gr.Column(scale=2, elem_classes=["eeg-main__detail"]):
detail = gr.HTML(_empty_detail(), elem_classes=["eeg-detail"])
gr.HTML(
f"""
<footer class="eeg-foot">
<span>
EEGDash is open source · BSD-3-Clause · data licenses follow their origin.
<a href="{EEGDASH_URL}">eegdash.org</a> ·
<a href="{GITHUB_URL}">github</a> ·
<a href="https://huggingface.co/{HF_ORG}">🤗 {HF_ORG}</a>
</span>
</footer>
""",
elem_classes=["eeg-foot-wrap"],
)
filter_inputs = [
query, modalities, subject_types, sources, licenses, min_subjects, only_on_hf,
]
for w in filter_inputs:
w.change(_on_filter, filter_inputs, [table, hero, modality_strip])
table.select(_on_select, [table], [detail])
if __name__ == "__main__":
demo.queue().launch(
ssr_mode=False,
allowed_paths=[str(ASSETS_DIR)],
)