Zipeng365's picture
Align leaderboard with paper core protocol
54a2406 verified
Raw
History Blame Contribute Delete
24.9 kB
import json
import os
from functools import lru_cache
from html import escape
from pathlib import Path
import gradio as gr
import pandas as pd
import plotly.graph_objects as go
from huggingface_hub import hf_hub_download
DATASET_REPO = os.getenv("TSDECOMPOSE_DATASET_REPO", "Zipeng365/TSDecompose-Benchmark")
DATA_VERSION = os.getenv("TSDECOMPOSE_DATA_VERSION", "v1.0.0")
LOCAL_DATA_DIR = os.getenv("TSDECOMPOSE_LOCAL_DATA_DIR")
SITE_FILES = {
"paper_global": "data/paper_tables/global_performance_summary.csv",
"paper_radar": "data/paper_figures/selected_radar_charts.png",
"synthetic": f"site_data/{DATA_VERSION}/leaderboard_synthetic_full22_overall.csv",
"synthetic_rank": "data/synthetic_full22_extension/results/summary/ranking_paper_5metric_overall.csv",
"real_proxy": f"site_data/{DATA_VERSION}/leaderboard_real_proxy22_overall.csv",
"semisynth": f"site_data/{DATA_VERSION}/leaderboard_semisynth_transfer_by_method.csv",
"semisynth_rank": "data/semisynth_transfer/results/summary/ranking_paper_5metric_overall.csv",
"track_b": f"site_data/{DATA_VERSION}/leaderboard_post_rebuttal_real_physics_track_b.csv",
"track_c": f"site_data/{DATA_VERSION}/leaderboard_post_rebuttal_real_proxy_track_c.csv",
"methods": f"site_data/{DATA_VERSION}/methods.json",
"suites": f"site_data/{DATA_VERSION}/suites.json",
"metrics": f"site_data/{DATA_VERSION}/evaluation_metrics.json",
}
VIEW_LABELS = {
"paper_global": "Paper Table 2 / Figure 3 Anchors",
"synthetic": "Benchmark 22-Method Synthetic Extension",
"real_proxy": "Benchmark 22-Method Real Proxy",
"semisynth": "Benchmark 22-Method Semi-Synthetic Transfer",
"track_b": "Post-Rebuttal Track B: CO2 / Tides",
"track_c": "Post-Rebuttal Track C: Real Proxy Panel",
}
VIEW_NOTES = {
"paper_global": (
"Camera-ready paper-aligned view. This reproduces Table 2's six-family "
"global performance summary from the 6-scenario, 50-draw core protocol. "
"Table 2 reports two of the five paper-core component-recovery metrics, "
"using tier-balanced stationary aggregation over Tiers 1-2 and Tier 3 for "
"the non-stationary columns; Figure 3 shows the "
"five-axis capability profile. This is not a single universal ranking."
),
"synthetic": (
"Benchmark-only expanded synthetic board: 6 scenarios x 50 draws = 300 generated "
"time-series instances. The expanded roster gives 22 method rows, "
"or 6,600 method-instance evaluations. This is a living extension view, not the "
"paper's primary Table 2/Figure 3 view. It reuses the five component-recovery "
"metrics and uses the released five-metric mean within-setting ranking."
),
"real_proxy": (
"Benchmark-only real-data companion track. These are proxy diagnostics, "
"not exact component-recovery labels and not camera-ready paper rankings. "
"Lower overall mean rank is better."
),
"semisynth": (
"Benchmark-only 22-method semi-synthetic transfer track: known synthetic "
"mechanisms injected into six real monthly backgrounds with three mechanisms, "
"two background scales, and eight windows per setting. This is a living "
"benchmark extension, not a camera-ready paper table; the displayed rank uses "
"the released five-metric mean within-setting ranking."
),
"track_b": (
"Post-rebuttal mechanism-aware checks on CO2 and tides. These use "
"mechanism-informed approximate structure and should be read as companion evidence."
),
"track_c": (
"Post-rebuttal six-dataset proxy/stability panel. These rows measure "
"plausibility and robustness where exact real-world components are unavailable."
),
}
HIGHER_IS_BETTER = {
"stationary_trend_r2": True,
"stationary_seasonal_spectral_corr": True,
"nonstationary_trend_r2": True,
"nonstationary_seasonal_spectral_corr": True,
"metric_T_r2_mean": True,
"metric_S_r2_mean": True,
"metric_S_spectral_corr_mean": True,
"metric_S_maxlag_corr_mean": True,
"coverage_success": True,
"metric_T_r2": True,
"metric_S_r2": True,
"metric_S_spectral_corr": True,
"metric_S_maxlag_corr": True,
"trend_r2": True,
"seasonal_r2": True,
"quad_fit_r2": True,
"seasonal_amplitude_slope_sign_match": True,
"target_frequency_coverage": True,
"m2_s2_separation_success": True,
"band_plausibility": True,
"band_power_ratio": True,
"band_peak_hit": True,
"resampling_trend_corr": True,
"resampling_season_corr": True,
"resampling_season_spectral_corr": True,
"resampling_season_maxlag_corr": True,
"resampling_band_overlap": True,
"resampling_stability": True,
"temporal_spectrum_overlap": True,
"overall_mean_rank": False,
"metric_T_dtw_mean": False,
"metric_T_dtw": False,
"c2_relative_error": False,
"seasonal_amplitude_slope_error": False,
"main_peak_frequency_error": False,
"temporal_trend_smoothness_drift": False,
"temporal_dominant_frequency_drift": False,
"mean_metric_rank": False,
}
DEFAULT_METRIC = {
"paper_global": "stationary_trend_r2",
"synthetic": "overall_mean_rank",
"real_proxy": "overall_mean_rank",
"semisynth": "overall_mean_rank",
"track_b": "seasonal_r2",
"track_c": "band_plausibility",
}
COLUMN_LABELS = {
"stationary_trend_r2": "Stationary Trend R2",
"stationary_seasonal_spectral_corr": "Stationary Seasonal Spectral Corr",
"nonstationary_trend_r2": "Non-stationary Trend R2",
"nonstationary_seasonal_spectral_corr": "Non-stationary Seasonal Spectral Corr",
"metric_T_r2": "Trend R2",
"metric_T_r2_mean": "Trend R2 mean",
"metric_T_dtw": "Trend DTW",
"metric_T_dtw_mean": "Trend DTW mean",
"metric_S_r2": "Seasonal R2",
"metric_S_r2_mean": "Seasonal R2 mean",
"metric_S_spectral_corr": "Seasonal spectral corr",
"metric_S_spectral_corr_mean": "Seasonal spectral corr mean",
"metric_S_maxlag_corr": "Seasonal max-lag corr",
"metric_S_maxlag_corr_mean": "Seasonal max-lag corr mean",
"mean_metric_rank": "Mean metric rank",
"overall_mean_rank": "Overall mean rank",
"overall_std_rank": "Overall rank SD",
"coverage_success": "Coverage",
"row_count": "Rows",
"setting_count": "Settings",
}
TABLE_LABELS = {
"display_name": "Method",
"family": "Family",
"method": "Method ID",
"stationary_trend_r2": "Stat. T R2",
"stationary_seasonal_spectral_corr": "Stat. S spec.",
"nonstationary_trend_r2": "Nonstat. T R2",
"nonstationary_seasonal_spectral_corr": "Nonstat. S spec.",
"metric_T_r2_mean": "T R2",
"metric_T_dtw_mean": "T DTW",
"metric_S_r2_mean": "S R2",
"metric_S_spectral_corr_mean": "S spec.",
"metric_S_maxlag_corr_mean": "S max-lag",
"overall_mean_rank": "Mean rank",
"overall_std_rank": "Rank SD",
}
PAPER_GLOBAL_METRICS = [
"stationary_trend_r2",
"stationary_seasonal_spectral_corr",
"nonstationary_trend_r2",
"nonstationary_seasonal_spectral_corr",
]
FAMILY_COLORS = {
"paper family": "#2563eb",
"classical": "#2563eb",
"extension_proxy": "#d97706",
"neural_block": "#7c3aed",
"unclassified": "#64748b",
}
def _local_path(filename: str) -> Path:
if LOCAL_DATA_DIR:
return Path(LOCAL_DATA_DIR) / Path(filename).name
return Path(__file__).resolve().parent.parent / "TSDecompose-Benchmark" / filename
def _download(filename: str) -> str:
local_candidate = _local_path(filename)
if local_candidate.exists():
return str(local_candidate)
token = os.getenv("HF_TOKEN") or None
return hf_hub_download(
repo_id=DATASET_REPO,
repo_type="dataset",
filename=filename,
token=token,
)
@lru_cache(maxsize=1)
def metric_metadata() -> dict:
with open(_download(SITE_FILES["metrics"]), encoding="utf-8") as handle:
return json.load(handle)
def _metric_label(metric: str) -> str:
return COLUMN_LABELS.get(metric, metric)
def _table_label(column: str) -> str:
return TABLE_LABELS.get(column, COLUMN_LABELS.get(column, column))
def metrics_html() -> str:
meta = metric_metadata()
metrics = meta.get("metrics", [])
cards = []
for item in metrics:
direction = "higher is better" if item.get("direction") == "higher" else "lower is better"
cards.append(
f"""
<div class="metric-card">
<div class="metric-name">{escape(item.get("display_name", ""))}</div>
<div class="metric-direction">{escape(direction)}</div>
<code>{escape(item.get("formula", ""))}</code>
<p>{escape(item.get("description", ""))}</p>
</div>
"""
)
return f"""
<section class="metric-panel">
<div class="metric-title">Paper Evaluation Protocol</div>
<p>
The classic paper benchmark uses <strong>{int(meta.get("paper_core_metric_count", 5))} core component-recovery metrics</strong>.
Each metric is computed per generated draw after output alignment, then averaged by scenario, tier, or regime.
</p>
<p>
<strong>Table 2</strong> displays only <strong>{int(meta.get("paper_table2_display_metric_count", 2))}</strong>
of those metrics: Trend R2 and Seasonal spectral correlation, split over stationary regimes (Tiers 1-2)
and the non-stationary regime (Tier 3). Its stationary columns use a tier-balanced average of Tier 1
and Tier 2 means over valid metric values; non-stationary columns use Tier 3 means. <strong>Figure 3</strong> is the five-metric profile view.
</p>
<div class="metric-grid">{''.join(cards)}</div>
</section>
"""
def _method_table() -> pd.DataFrame:
path = _download(SITE_FILES["methods"])
methods = pd.read_json(path)
return methods[["method", "display_name", "family"]].drop_duplicates("method")
def _paper_family_table() -> pd.DataFrame:
df = pd.read_csv(_download(SITE_FILES["paper_global"]))
df = _numeric(df)
df["display_name"] = df["method_family"]
df["method"] = df["method_family"]
df["family"] = "paper family"
return df
def _numeric(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
for col in out.columns:
if col not in {"method", "display_name", "family", "dataset", "round_id"}:
try:
out[col] = pd.to_numeric(out[col])
except (TypeError, ValueError):
pass
return out
def _attach_methods(df: pd.DataFrame, methods: pd.DataFrame) -> pd.DataFrame:
out = df.merge(methods, on="method", how="left")
out["display_name"] = out["display_name"].fillna(out["method"])
out["family"] = out["family"].fillna("unclassified")
return out
def _add_mean_rank(df: pd.DataFrame, metrics: list[str]) -> pd.DataFrame:
out = df.copy()
rank_cols = []
for metric in metrics:
if metric not in out.columns:
continue
values = pd.to_numeric(out[metric], errors="coerce")
if values.notna().sum() == 0:
continue
ascending = not HIGHER_IS_BETTER.get(metric, True)
col = f"rank_{metric}"
out[col] = values.rank(ascending=ascending, method="min")
rank_cols.append(col)
if rank_cols:
out["mean_metric_rank"] = out[rank_cols].mean(axis=1).round(3)
out["rank"] = out["mean_metric_rank"].rank(ascending=True, method="min").astype(int)
return out
def _attach_released_rank(df: pd.DataFrame, rank_file: str) -> pd.DataFrame:
rank = pd.read_csv(_download(rank_file))
rank = _numeric(rank)
keep = [
col
for col in ["method", "overall_mean_rank", "overall_std_rank", "setting_count", "rank_order"]
if col in rank.columns
]
out = df.drop(columns=[col for col in keep if col != "method" and col in df.columns])
out = out.merge(rank[keep], on="method", how="left")
if "rank_order" in out.columns:
out["rank"] = pd.to_numeric(out["rank_order"], errors="coerce")
elif "overall_mean_rank" in out.columns:
values = pd.to_numeric(out["overall_mean_rank"], errors="coerce")
out["rank"] = values.rank(ascending=True, method="min")
return out
@lru_cache(maxsize=1)
def load_data() -> dict[str, pd.DataFrame]:
methods = _method_table()
paper_global = _paper_family_table()
synthetic = pd.read_csv(_download(SITE_FILES["synthetic"]))
synthetic = _attach_methods(_numeric(synthetic), methods)
synthetic = _attach_released_rank(synthetic, SITE_FILES["synthetic_rank"])
real_proxy = pd.read_csv(_download(SITE_FILES["real_proxy"]))
real_proxy = _attach_methods(_numeric(real_proxy), methods)
real_proxy["rank"] = pd.to_numeric(real_proxy["rank_order"], errors="coerce")
semisynth = pd.read_csv(_download(SITE_FILES["semisynth"]))
semisynth = _attach_methods(_numeric(semisynth), methods)
semisynth = _attach_released_rank(semisynth, SITE_FILES["semisynth_rank"])
track_b = pd.read_csv(_download(SITE_FILES["track_b"]))
track_b = _attach_methods(_numeric(track_b), methods)
track_c = pd.read_csv(_download(SITE_FILES["track_c"]))
track_c = _attach_methods(_numeric(track_c), methods)
return {
"paper_global": paper_global,
"synthetic": synthetic,
"real_proxy": real_proxy,
"semisynth": semisynth,
"track_b": track_b,
"track_c": track_c,
}
def available_metrics(view: str) -> list[str]:
df = load_data()[view]
return [
col
for col in df.columns
if col not in {"method", "display_name", "family", "dataset", "round_id"}
and pd.api.types.is_numeric_dtype(df[col])
]
def metric_choices(view: str) -> list[tuple[str, str]]:
return [(_metric_label(metric), metric) for metric in available_metrics(view)]
def filter_table(view: str, metric: str, family: str, query: str, top_n: int):
data = load_data()
df = data[view].copy()
metrics = available_metrics(view)
if metric not in metrics:
metric = DEFAULT_METRIC[view]
if family != "All":
df = df[df["family"] == family]
if query.strip():
q = query.strip().lower()
text_cols = [col for col in ["method", "display_name", "family", "dataset"] if col in df.columns]
mask = pd.Series(False, index=df.index)
for col in text_cols:
mask = mask | df[col].astype(str).str.lower().str.contains(q, regex=False)
df = df[mask]
ascending = not HIGHER_IS_BETTER.get(metric, True)
if view == "paper_global":
ranked = df.head(int(top_n))
else:
ranked = df.sort_values(metric, ascending=ascending, na_position="last").head(int(top_n))
if view == "paper_global":
show_cols = ["display_name", metric]
show_cols += [col for col in PAPER_GLOBAL_METRICS if col != metric]
remaining_metrics = []
else:
show_cols = []
for col in [
"rank",
"display_name",
"family",
"dataset",
metric,
"method",
"mean_metric_rank",
"overall_mean_rank",
"coverage_success",
"row_count",
"setting_count",
]:
if col in ranked.columns and col not in show_cols:
show_cols.append(col)
remaining_metrics = [
col
for col in ranked.columns
if col not in show_cols
and col not in {"rank_order"}
and pd.api.types.is_numeric_dtype(ranked[col])
]
table = ranked[show_cols + remaining_metrics].copy()
table = table.round(4)
table_html = _table_html(table)
plot_df = ranked.copy()
if "dataset" in plot_df.columns and plot_df["method"].duplicated().any():
plot_df = (
plot_df.groupby(["method", "display_name", "family"], as_index=False)[metric]
.mean(numeric_only=True)
.sort_values(metric, ascending=ascending)
.head(int(top_n))
)
metric_name = _metric_label(metric)
fig = _bar_figure(plot_df, metric, metric_name, view)
summary = (
f"**{VIEW_LABELS[view]}** \n"
f"{VIEW_NOTES[view]} \n\n"
f"Rows shown: `{len(table)}` from `{len(data[view])}`. "
f"Metric: `{metric_name}`. "
f"Metric direction: `{'higher is better' if HIGHER_IS_BETTER.get(metric, True) else 'lower is better'}`. "
f"Dataset source: [`{DATASET_REPO}`](https://huggingface.co/datasets/{DATASET_REPO})."
)
figure = _figure_update(view)
return table_html, fig, summary, figure
def _table_html(table: pd.DataFrame) -> str:
display = table.copy()
if "family" in display.columns:
display["family"] = display["family"].replace(
{"extension_proxy": "extension proxy"}
)
display = display.rename(columns={col: _table_label(col) for col in display.columns})
html = display.to_html(index=False, escape=False, classes="leaderboard-table")
return f"""
<div class="table-title">Leaderboard table</div>
<div class="table-wrap">{html}</div>
"""
def _bar_figure(plot_df: pd.DataFrame, metric: str, metric_name: str, view: str) -> go.Figure:
data = plot_df.copy()
data[metric] = pd.to_numeric(data[metric], errors="coerce")
data = data[data[metric].notna()]
labels = data["display_name"].astype(str).tolist()
values = [float(value) for value in data[metric].tolist()]
families = data.get("family", pd.Series(["unclassified"] * len(data))).astype(str).tolist()
colors = [FAMILY_COLORS.get(family, "#0f766e") for family in families]
hover = [
f"Family: {family}<br>Method: {method}"
for family, method in zip(families, data.get("method", data["display_name"]).astype(str))
]
fig = go.Figure()
fig.add_bar(
x=values,
y=labels,
orientation="h",
marker=dict(color=colors, line=dict(color="#1f2937", width=0.5)),
customdata=hover,
hovertemplate="%{y}<br>" + escape(metric_name) + ": %{x:.4f}<br>%{customdata}<extra></extra>",
)
title = f"{VIEW_LABELS[view]} - {metric_name}"
fig.update_layout(
title=dict(text=title, x=0.01, xanchor="left"),
height=max(360, 34 * max(len(labels), 6)),
margin=dict(l=150, r=24, t=56, b=48),
yaxis=dict(
autorange="reversed",
title="",
type="category",
categoryorder="array",
categoryarray=labels,
tickfont=dict(size=13),
),
xaxis=dict(title=metric_name, zeroline=True, rangemode="tozero"),
showlegend=False,
paper_bgcolor="white",
plot_bgcolor="white",
bargap=0.28,
)
return fig
def _figure_update(view: str):
if view != "paper_global":
return gr.update(value=None, visible=False)
bundled = Path(__file__).resolve().parent / "assets" / "selected_radar_charts.png"
if bundled.exists():
return gr.update(value=str(bundled), visible=True)
return gr.update(value=_download(SITE_FILES["paper_radar"]), visible=True)
def refresh_controls(view: str):
metrics = available_metrics(view)
return gr.update(choices=metric_choices(view), value=DEFAULT_METRIC[view]), gr.update(value="All")
def stats_html() -> str:
data = load_data()
paper_families = data["paper_global"]["method"].nunique()
synthetic_methods = data["synthetic"]["method"].nunique()
return f"""
<div class="stats-grid">
<div class="stat"><span class="stat-value">{paper_families}</span><span class="stat-label">Paper method families</span></div>
<div class="stat"><span class="stat-value">5</span><span class="stat-label">Paper core metrics</span></div>
<div class="stat"><span class="stat-value">6 x 50</span><span class="stat-label">Classic synthetic instances</span></div>
<div class="stat"><span class="stat-value">{synthetic_methods}</span><span class="stat-label">Expansion method rows</span></div>
<div class="stat"><span class="stat-value">2</span><span class="stat-label">Evidence versions</span></div>
</div>
"""
CSS = """
.gradio-container { max-width: 1220px !important; }
.stats-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
margin: 10px 0 16px;
}
.stat {
border: 1px solid #d8dee9;
border-radius: 8px;
padding: 12px 14px;
background: #ffffff;
}
.stat-value {
display: block;
font-size: 24px;
font-weight: 700;
color: #0f3b57;
}
.stat-label {
display: block;
font-size: 13px;
color: #53606f;
margin-top: 2px;
}
.metric-panel {
border: 1px solid #d8dee9;
border-radius: 8px;
padding: 14px;
background: #ffffff;
margin: 0 0 16px;
}
.metric-title {
font-size: 16px;
font-weight: 700;
color: #1f2937;
margin-bottom: 6px;
}
.metric-panel p {
color: #374151;
font-size: 14px;
margin: 6px 0;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px;
margin-top: 12px;
}
.metric-card {
border: 1px solid #e5e9f0;
border-radius: 8px;
padding: 10px;
background: #fbfcfd;
min-width: 0;
}
.metric-name {
font-size: 13px;
font-weight: 700;
color: #1f2937;
}
.metric-direction {
font-size: 12px;
color: #53606f;
margin: 3px 0 6px;
}
.metric-card code {
display: block;
white-space: normal;
overflow-wrap: anywhere;
background: #f3f5f8;
border-radius: 6px;
padding: 6px;
font-size: 11px;
color: #273142;
}
.metric-card p {
font-size: 12px;
margin: 7px 0 0;
}
.table-title {
font-size: 14px;
color: #303946;
margin: 12px 0 8px;
}
.table-wrap {
overflow-x: auto;
border: 1px solid #d8dee9;
border-radius: 8px;
background: #ffffff;
max-height: 520px;
}
.leaderboard-table {
border-collapse: collapse;
width: max-content;
min-width: 100%;
font-size: 13px;
}
.leaderboard-table th,
.leaderboard-table td {
border-bottom: 1px solid #edf0f4;
padding: 8px 10px;
text-align: left;
white-space: nowrap;
}
.leaderboard-table th {
position: sticky;
top: 0;
background: #f7f9fb;
color: #1f2937;
z-index: 1;
}
.leaderboard-table th:nth-child(1),
.leaderboard-table td:nth-child(1) {
min-width: 48px;
}
.leaderboard-table th:nth-child(2),
.leaderboard-table td:nth-child(2) {
min-width: 210px;
}
.leaderboard-table th:nth-child(3),
.leaderboard-table td:nth-child(3) {
min-width: 130px;
}
.leaderboard-table tr:nth-child(even) td {
background: #fbfcfd;
}
@media (max-width: 780px) {
.stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.metric-grid { grid-template-columns: repeat(1, minmax(0, 1fr)); }
}
"""
GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0])
BLOCKS_KWARGS = {"title": "TSDecompose Benchmark Leaderboard"}
if GRADIO_MAJOR < 6:
BLOCKS_KWARGS["css"] = CSS
with gr.Blocks(**BLOCKS_KWARGS) as demo:
gr.Markdown(
"""
# TSDecompose Benchmark Leaderboard
Interactive leaderboard for **Time-Series Decomposition as a Standalone Task: A Mechanism-Identifiable Benchmark**.
The dataset repository is the source of record; this Space is the web presentation layer.
"""
)
gr.HTML(value=stats_html())
gr.HTML(value=metrics_html())
with gr.Row():
view = gr.Dropdown(
choices=[(label, key) for key, label in VIEW_LABELS.items()],
value="paper_global",
label="View",
)
metric = gr.Dropdown(
choices=metric_choices("paper_global"),
value=DEFAULT_METRIC["paper_global"],
label="Metric",
)
with gr.Row():
family = gr.Dropdown(
choices=["All", "paper family", "classical", "extension_proxy"],
value="All",
label="Family",
)
query = gr.Textbox(label="Search", placeholder="method, family, or dataset")
top_n = gr.Slider(5, 40, value=22, step=1, label="Rows")
note = gr.Markdown()
figure_ref = gr.Image(
label="Paper Figure 3 reference",
type="filepath",
interactive=False,
visible=True,
)
plot = gr.Plot()
table = gr.HTML()
inputs = [view, metric, family, query, top_n]
for control in inputs:
control.change(filter_table, inputs=inputs, outputs=[table, plot, note, figure_ref])
view.change(refresh_controls, inputs=view, outputs=[metric, family]).then(
filter_table, inputs=inputs, outputs=[table, plot, note, figure_ref]
)
demo.load(filter_table, inputs=inputs, outputs=[table, plot, note, figure_ref])
if __name__ == "__main__":
if GRADIO_MAJOR >= 6:
demo.launch(css=CSS)
else:
demo.launch()