chore: import upstream snapshot with attribution
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from scripts.tts_comparison_report.reporting.components.audio_report import prepare_audio_pairs
|
||||
from scripts.tts_comparison_report.reporting.components.boxplots import BoxPlotsConfig
|
||||
from scripts.tts_comparison_report.reporting.components.eval_report import prepare_eval_artifacts
|
||||
|
||||
__all__ = [
|
||||
"BoxPlotsConfig",
|
||||
"prepare_audio_pairs",
|
||||
"prepare_eval_artifacts",
|
||||
]
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import random
|
||||
import warnings
|
||||
|
||||
from scripts.tts_comparison_report.reporting.constants import SEED
|
||||
from scripts.tts_comparison_report.reporting.models import AudioPair, BucketData, BucketStructure
|
||||
|
||||
_RNG = random.Random(SEED)
|
||||
|
||||
|
||||
def _collect_audio_pairs(
|
||||
benchmark_name: str,
|
||||
bucket_baseline: BucketData,
|
||||
bucket_candidate: BucketData,
|
||||
bucket_structure: BucketStructure,
|
||||
) -> list[AudioPair]:
|
||||
baseline_paths = bucket_baseline.get_benchmark_audio_paths(benchmark_name)
|
||||
candidate_paths = bucket_candidate.get_benchmark_audio_paths(benchmark_name)
|
||||
baseline_meta = bucket_baseline.get_benchmark_sample_meta(benchmark_name, bucket_structure)
|
||||
candidate_meta = bucket_candidate.get_benchmark_sample_meta(benchmark_name, bucket_structure)
|
||||
pairs = []
|
||||
|
||||
if set(baseline_paths) != set(candidate_paths):
|
||||
raise ValueError(f"Audio sample sets differ for benchmark '{benchmark_name}'.")
|
||||
|
||||
for name in baseline_paths:
|
||||
if name not in candidate_paths or name not in baseline_meta or name not in candidate_meta:
|
||||
raise ValueError(
|
||||
f"Missing matched sample '{name}' in audio paths or metadata for benchmark '{benchmark_name}'."
|
||||
)
|
||||
|
||||
if baseline_meta[name].sample_id != candidate_meta[name].sample_id:
|
||||
raise ValueError(
|
||||
f"Sample id mismatch for '{name}' in benchmark '{benchmark_name}'. "
|
||||
"Probably you use different versions of buckets."
|
||||
)
|
||||
|
||||
pair = AudioPair(
|
||||
context_path=baseline_meta[name].context_path,
|
||||
baseline_path=baseline_paths[name],
|
||||
candidate_path=candidate_paths[name],
|
||||
text=baseline_meta[name].gt_text,
|
||||
)
|
||||
pairs.append(pair)
|
||||
|
||||
pairs.sort(key=lambda p: p.baseline_path.stem)
|
||||
|
||||
return pairs
|
||||
|
||||
|
||||
def prepare_audio_pairs(
|
||||
bucket_baseline: BucketData,
|
||||
bucket_candidate: BucketData,
|
||||
bucket_structure: BucketStructure,
|
||||
used_benchmarks: list[str],
|
||||
samples_per_benchmark: int,
|
||||
) -> dict[str, list[AudioPair]]:
|
||||
"""Prepare audio pairs for the selected benchmarks.
|
||||
|
||||
Args:
|
||||
bucket_baseline: Baseline bucket data.
|
||||
bucket_candidate: Candidate bucket data.
|
||||
used_benchmarks: Benchmark names to include in the audio report.
|
||||
samples_per_benchmark: Maximum number of audio pairs to sample per benchmark.
|
||||
|
||||
Returns:
|
||||
Mapping from benchmark name to sampled baseline/candidate audio pairs.
|
||||
|
||||
Raises:
|
||||
ValueError: If benchmark audio sets or sample metadata are inconsistent.
|
||||
"""
|
||||
pairs = {}
|
||||
|
||||
for benchmark_name in used_benchmarks:
|
||||
benchmark_pairs = _collect_audio_pairs(benchmark_name, bucket_baseline, bucket_candidate, bucket_structure)
|
||||
sampled_pairs = _RNG.sample(benchmark_pairs, k=min(samples_per_benchmark, len(benchmark_pairs)))
|
||||
|
||||
if len(sampled_pairs) < samples_per_benchmark:
|
||||
warnings.warn(
|
||||
f"\nBenchmark '{benchmark_name}' contains only {len(sampled_pairs)} available paired samples, "
|
||||
f"but {samples_per_benchmark} were requested.",
|
||||
stacklevel=2,
|
||||
)
|
||||
pairs[benchmark_name] = sampled_pairs
|
||||
|
||||
return pairs
|
||||
@@ -0,0 +1,221 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from dataclasses import dataclass, field
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.patches import PathPatch
|
||||
from scripts.tts_comparison_report.reporting.metrics import DistributionMetricSpec, DistributionMetricsRegistry
|
||||
from scripts.tts_comparison_report.reporting.models import BucketData, StatTestResult, Winner
|
||||
|
||||
|
||||
@dataclass
|
||||
class BoxPlotsConfig:
|
||||
"""Styling and layout configuration for generated benchmark box plots."""
|
||||
|
||||
font_family: str = "sans-serif"
|
||||
font_list: list[str] = field(default_factory=lambda: ["Arial", "Helvetica", "DejaVu Sans"])
|
||||
|
||||
linewidth: float = 0.4
|
||||
default_model_color: str = "#36454F"
|
||||
winner_model_color: str = "#7393B3"
|
||||
box_alpha: float = 0.35
|
||||
grid_alpha: float = 0.4
|
||||
fontsize: int = 6
|
||||
fontsize_title: int = 8
|
||||
|
||||
widths: float = 0.6
|
||||
mean_marker: str = "o"
|
||||
mean_marker_color: str = "#CD5C5C"
|
||||
mean_marker_size: float = 4.0
|
||||
median_color: str = "black"
|
||||
whisker_color: str = "#666666"
|
||||
cap_color: str = "#666666"
|
||||
outlier_color: str = "#708090"
|
||||
outlier_marker: str = "o"
|
||||
outlier_markersize: float = 3.0
|
||||
outlier_alpha: float = 0.5
|
||||
|
||||
|
||||
def _style_boxplot(
|
||||
bp: dict[str, PathPatch],
|
||||
metric: DistributionMetricSpec,
|
||||
winner_lookup: dict[str, Winner],
|
||||
cfg: BoxPlotsConfig,
|
||||
) -> None:
|
||||
for i, patch in enumerate(bp["boxes"]):
|
||||
winner = winner_lookup[metric.report_name]
|
||||
|
||||
if (i == 0 and winner == Winner.baseline) or (i == 1 and winner == Winner.candidate):
|
||||
color = cfg.winner_model_color
|
||||
else:
|
||||
color = cfg.default_model_color
|
||||
|
||||
patch.set_facecolor(color)
|
||||
patch.set_alpha(cfg.box_alpha)
|
||||
patch.set_edgecolor(color)
|
||||
patch.set_linewidth(cfg.linewidth)
|
||||
|
||||
|
||||
def _add_mean_ci_labels(
|
||||
ax: Axes,
|
||||
baseline: np.ndarray,
|
||||
candidate: np.ndarray,
|
||||
metric: DistributionMetricSpec,
|
||||
cfg: BoxPlotsConfig,
|
||||
) -> None:
|
||||
for x, values in [(1, baseline), (2, candidate)]:
|
||||
mean, median = values.mean(), np.median(values)
|
||||
sem = values.std(ddof=1) / np.sqrt(len(values)) if len(values) > 1 else 0.0
|
||||
ci95 = 1.96 * sem
|
||||
label = f"{mean:.3f} ± {ci95:.3f}"
|
||||
|
||||
if metric.plot_range is not None:
|
||||
range_ = metric.plot_range[1] - metric.plot_range[0]
|
||||
else:
|
||||
range_ = values.max() - values.min()
|
||||
|
||||
x_offset = 0.02
|
||||
y_offset = 0.03 * range_
|
||||
|
||||
if median > mean and mean - y_offset > 0:
|
||||
y_offset = -y_offset
|
||||
|
||||
ax.text(x + x_offset, mean + y_offset, label, ha="left", va="center", fontsize=cfg.fontsize)
|
||||
|
||||
|
||||
def _configure_boxplot_axis(
|
||||
ax: Axes,
|
||||
metric: DistributionMetricSpec,
|
||||
baseline_name: str,
|
||||
candidate_name: str,
|
||||
cfg: BoxPlotsConfig,
|
||||
) -> None:
|
||||
ax.set_title(metric.report_name, fontsize=cfg.fontsize_title)
|
||||
ax.set_xticks([1, 2])
|
||||
ax.set_xticklabels([baseline_name, candidate_name])
|
||||
ax.tick_params(axis="x", labelsize=cfg.fontsize)
|
||||
ax.tick_params(axis="y", labelsize=cfg.fontsize)
|
||||
ax.grid(True, axis="y", linestyle="dotted", alpha=cfg.grid_alpha)
|
||||
|
||||
for spine in ax.spines.values():
|
||||
spine.set_linewidth(cfg.linewidth)
|
||||
|
||||
ax.tick_params(axis="both", width=cfg.linewidth)
|
||||
|
||||
if metric.plot_range is not None:
|
||||
ax.set_ylim(metric.plot_range[0], metric.plot_range[1])
|
||||
|
||||
|
||||
def prepare_boxplots(
|
||||
bucket_baseline: BucketData,
|
||||
bucket_candidate: BucketData,
|
||||
stat_test_results: list[StatTestResult],
|
||||
cfg: BoxPlotsConfig,
|
||||
benchmark_name: Optional[str] = None,
|
||||
) -> BytesIO:
|
||||
"""Create an in-memory box plot figure for summary or benchmark-level metrics.
|
||||
|
||||
Args:
|
||||
bucket_baseline: Baseline bucket data.
|
||||
bucket_candidate: Candidate bucket data.
|
||||
stat_test_results: Statistical test results used to highlight the winning model.
|
||||
cfg: Plot styling and layout configuration.
|
||||
benchmark_name: Benchmark name. If omitted, metric samples are aggregated
|
||||
across all benchmarks.
|
||||
|
||||
Returns:
|
||||
PNG image stored in an in-memory bytes buffer.
|
||||
"""
|
||||
baseline_name = bucket_baseline.name
|
||||
candidate_name = bucket_candidate.name
|
||||
winner_lookup = {res.metric_name: res.winner for res in stat_test_results}
|
||||
num_rows = sum(m.add_to_box_plot for m in DistributionMetricsRegistry)
|
||||
fig_height = max(2.0 * num_rows, 4.5)
|
||||
|
||||
with plt.rc_context({"font.family": cfg.font_family, "font.sans-serif": cfg.font_list}):
|
||||
fig, axs = plt.subplots(num_rows, 1, figsize=(6, fig_height), squeeze=False)
|
||||
axs = axs.flatten()
|
||||
plot_idx = 0
|
||||
|
||||
for metric in DistributionMetricsRegistry:
|
||||
if not metric.add_to_box_plot:
|
||||
continue
|
||||
|
||||
baseline = bucket_baseline.get_metric_samples(
|
||||
metric_name=metric.key,
|
||||
benchmark_name=benchmark_name,
|
||||
)
|
||||
candidate = bucket_candidate.get_metric_samples(
|
||||
metric_name=metric.key,
|
||||
benchmark_name=benchmark_name,
|
||||
)
|
||||
baseline = np.asarray(baseline, dtype=float)
|
||||
candidate = np.asarray(candidate, dtype=float)
|
||||
|
||||
ax = axs[plot_idx]
|
||||
plot_idx += 1
|
||||
|
||||
bp = ax.boxplot(
|
||||
[baseline, candidate],
|
||||
positions=[1, 2],
|
||||
widths=cfg.widths,
|
||||
patch_artist=True,
|
||||
showmeans=True,
|
||||
meanline=False,
|
||||
meanprops={
|
||||
"marker": cfg.mean_marker,
|
||||
"markerfacecolor": cfg.mean_marker_color,
|
||||
"markeredgecolor": cfg.mean_marker_color,
|
||||
"markersize": cfg.mean_marker_size,
|
||||
},
|
||||
medianprops={
|
||||
"color": cfg.median_color,
|
||||
"linewidth": cfg.linewidth,
|
||||
},
|
||||
whiskerprops={
|
||||
"color": cfg.whisker_color,
|
||||
"linewidth": cfg.linewidth,
|
||||
},
|
||||
capprops={
|
||||
"color": cfg.cap_color,
|
||||
"linewidth": cfg.linewidth,
|
||||
},
|
||||
boxprops={
|
||||
"linewidth": cfg.linewidth,
|
||||
},
|
||||
flierprops={
|
||||
"marker": cfg.outlier_marker,
|
||||
"markerfacecolor": cfg.outlier_color,
|
||||
"markeredgecolor": cfg.outlier_color,
|
||||
"markersize": cfg.outlier_markersize,
|
||||
"alpha": cfg.outlier_alpha,
|
||||
},
|
||||
)
|
||||
|
||||
_style_boxplot(bp, metric, winner_lookup, cfg)
|
||||
_add_mean_ci_labels(ax, baseline, candidate, metric, cfg)
|
||||
_configure_boxplot_axis(ax, metric, baseline_name, candidate_name, cfg)
|
||||
|
||||
fig.tight_layout(rect=[0, 0, 1, 0.985])
|
||||
|
||||
buffer = BytesIO()
|
||||
fig.savefig(buffer, format="png", dpi=300, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
buffer.seek(0)
|
||||
|
||||
return buffer
|
||||
@@ -0,0 +1,97 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from scripts.tts_comparison_report.reporting.components.boxplots import BoxPlotsConfig, prepare_boxplots
|
||||
from scripts.tts_comparison_report.reporting.components.metrics_table import (
|
||||
prepare_benchmark_metrics_table_rows,
|
||||
prepare_summary_metrics_table_rows,
|
||||
)
|
||||
from scripts.tts_comparison_report.reporting.components.stat_tests import (
|
||||
prepare_stat_tests_analysis_info,
|
||||
prepare_stat_tests_table_rows,
|
||||
run_stat_tests,
|
||||
)
|
||||
from scripts.tts_comparison_report.reporting.models import BucketData, EvalArtifacts, EvalResult, ModelConfiguration
|
||||
|
||||
|
||||
def prepare_eval_artifacts(
|
||||
bucket_baseline: BucketData,
|
||||
bucket_candidate: BucketData,
|
||||
box_plots_cfg: BoxPlotsConfig,
|
||||
) -> EvalArtifacts:
|
||||
"""Prepare summary and benchmark-level evaluation artifacts for report rendering.
|
||||
|
||||
Args:
|
||||
bucket_baseline: Baseline bucket data.
|
||||
bucket_candidate: Candidate bucket data.
|
||||
box_plots_cfg: Configuration used to generate benchmark and summary box plots.
|
||||
|
||||
Returns:
|
||||
Evaluation artifacts containing configuration metadata, summary results,
|
||||
and per-benchmark results.
|
||||
"""
|
||||
baseline_name = bucket_baseline.name
|
||||
candidate_name = bucket_candidate.name
|
||||
is_self_comparison = bucket_baseline.path == bucket_candidate.path
|
||||
|
||||
metrics_table_row = prepare_summary_metrics_table_rows(bucket_baseline, bucket_candidate)
|
||||
stat_test_results = run_stat_tests(bucket_baseline, bucket_candidate)
|
||||
stat_test_table_row = prepare_stat_tests_table_rows(baseline_name, candidate_name, stat_test_results)
|
||||
stat_tests_analysis_info = prepare_stat_tests_analysis_info(baseline_name, candidate_name, stat_test_results)
|
||||
|
||||
box_plots = prepare_boxplots(
|
||||
bucket_baseline=bucket_baseline,
|
||||
bucket_candidate=bucket_candidate,
|
||||
stat_test_results=stat_test_results,
|
||||
cfg=box_plots_cfg,
|
||||
)
|
||||
|
||||
configuration = ModelConfiguration(
|
||||
baseline=bucket_baseline.configuration_str,
|
||||
candidate=bucket_candidate.configuration_str,
|
||||
)
|
||||
summary = EvalResult(
|
||||
metrics_table_row=metrics_table_row,
|
||||
stat_test_table_row=stat_test_table_row,
|
||||
stat_tests_analysis_info=stat_tests_analysis_info,
|
||||
box_plots=box_plots,
|
||||
)
|
||||
benchmarks = {}
|
||||
|
||||
for benchmark_name in bucket_baseline.benchmarks:
|
||||
metrics_table_row = prepare_benchmark_metrics_table_rows(benchmark_name, bucket_baseline, bucket_candidate)
|
||||
stat_test_results = run_stat_tests(bucket_baseline, bucket_candidate, benchmark_name)
|
||||
stat_test_table_row = prepare_stat_tests_table_rows(baseline_name, candidate_name, stat_test_results)
|
||||
stat_tests_analysis_info = prepare_stat_tests_analysis_info(baseline_name, candidate_name, stat_test_results)
|
||||
|
||||
box_plots = prepare_boxplots(
|
||||
bucket_baseline=bucket_baseline,
|
||||
bucket_candidate=bucket_candidate,
|
||||
stat_test_results=stat_test_results,
|
||||
cfg=box_plots_cfg,
|
||||
benchmark_name=benchmark_name,
|
||||
)
|
||||
|
||||
benchmarks[benchmark_name] = EvalResult(
|
||||
metrics_table_row=metrics_table_row,
|
||||
stat_test_table_row=stat_test_table_row,
|
||||
stat_tests_analysis_info=stat_tests_analysis_info,
|
||||
box_plots=box_plots,
|
||||
)
|
||||
|
||||
return EvalArtifacts(
|
||||
configuration=configuration,
|
||||
summary=summary,
|
||||
benchmarks=benchmarks,
|
||||
is_self_comparison=is_self_comparison,
|
||||
)
|
||||
@@ -0,0 +1,154 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import html
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from scripts.tts_comparison_report.reporting.metrics import MetricSpec, MetricsRegistry
|
||||
from scripts.tts_comparison_report.reporting.models import BucketData
|
||||
|
||||
|
||||
def _metric_comparator(
|
||||
a: float,
|
||||
b: float,
|
||||
lower_is_better: Optional[bool],
|
||||
) -> Optional[bool]:
|
||||
if lower_is_better is None:
|
||||
return None
|
||||
|
||||
if lower_is_better:
|
||||
# If the values are equal, the baseline wins.
|
||||
return a <= b
|
||||
|
||||
return a >= b
|
||||
|
||||
|
||||
def _format_metric_values(
|
||||
a: float,
|
||||
b: float,
|
||||
metric: MetricSpec,
|
||||
) -> tuple[str, str]:
|
||||
a, b = metric.multiplier * a, metric.multiplier * b
|
||||
a_is_better = _metric_comparator(a, b, metric.lower_is_better)
|
||||
a, b = round(a, metric.round_digits), round(b, metric.round_digits)
|
||||
a_str, b_str = f"{a}{metric.units}", f"{b}{metric.units}"
|
||||
|
||||
a_str = html.escape(a_str)
|
||||
b_str = html.escape(b_str)
|
||||
|
||||
if metric.lower_is_better is not None:
|
||||
if a_is_better:
|
||||
a_str = f"<strong>{a_str}</strong>"
|
||||
else:
|
||||
b_str = f"<strong>{b_str}</strong>"
|
||||
|
||||
return a_str, b_str
|
||||
|
||||
|
||||
def prepare_benchmark_metrics_table_rows(
|
||||
benchmark_name: str,
|
||||
bucket_baseline: BucketData,
|
||||
bucket_candidate: BucketData,
|
||||
) -> list[list[str]]:
|
||||
"""Prepare formatted metric rows for one benchmark comparison table.
|
||||
|
||||
Args:
|
||||
benchmark_name: Name of the benchmark to render.
|
||||
bucket_baseline: Baseline bucket data.
|
||||
bucket_candidate: Candidate bucket data.
|
||||
|
||||
Returns:
|
||||
Table rows containing metric names and formatted baseline/candidate values.
|
||||
|
||||
Raises:
|
||||
ValueError: If a required metric is missing for the benchmark.
|
||||
"""
|
||||
rows = []
|
||||
|
||||
for metric in MetricsRegistry:
|
||||
a = bucket_baseline.get_metric_avg_value(
|
||||
metric_name=metric.key,
|
||||
benchmark_name=benchmark_name,
|
||||
)
|
||||
b = bucket_candidate.get_metric_avg_value(
|
||||
metric_name=metric.key,
|
||||
benchmark_name=benchmark_name,
|
||||
)
|
||||
|
||||
if a is None or b is None:
|
||||
if metric.optional:
|
||||
continue
|
||||
raise ValueError(f"Unknown metric '{metric.key}' for benchmark '{benchmark_name}'.")
|
||||
|
||||
a_str, b_str = _format_metric_values(a, b, metric)
|
||||
|
||||
rows.append([html.escape(metric.report_name), a_str, b_str])
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def prepare_summary_metrics_table_rows(
|
||||
bucket_baseline: BucketData,
|
||||
bucket_candidate: BucketData,
|
||||
) -> list[list[str]]:
|
||||
"""Prepare formatted metric rows for the summary comparison table.
|
||||
|
||||
Args:
|
||||
bucket_baseline: Baseline bucket data.
|
||||
bucket_candidate: Candidate bucket data.
|
||||
|
||||
Returns:
|
||||
Table rows containing metric names and formatted macro-averaged
|
||||
baseline/candidate values.
|
||||
|
||||
Raises:
|
||||
ValueError: If a required metric is missing for any benchmark included
|
||||
in the summary.
|
||||
"""
|
||||
rows = []
|
||||
|
||||
for metric in MetricsRegistry:
|
||||
if not metric.include_in_summary:
|
||||
continue
|
||||
|
||||
a_vals, b_vals = [], []
|
||||
skip = False
|
||||
|
||||
for benchmark_name in bucket_baseline.benchmarks:
|
||||
a = bucket_baseline.get_metric_avg_value(
|
||||
metric_name=metric.key,
|
||||
benchmark_name=benchmark_name,
|
||||
)
|
||||
b = bucket_candidate.get_metric_avg_value(
|
||||
metric_name=metric.key,
|
||||
benchmark_name=benchmark_name,
|
||||
)
|
||||
|
||||
if a is None or b is None:
|
||||
if metric.optional:
|
||||
skip = True
|
||||
break
|
||||
raise ValueError(f"Unknown metric '{metric.key}' for benchmark '{benchmark_name}'.")
|
||||
|
||||
a_vals.append(a)
|
||||
b_vals.append(b)
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
avg_a, avg_b = np.mean(a_vals), np.mean(b_vals)
|
||||
a_str, b_str = _format_metric_values(avg_a, avg_b, metric)
|
||||
rows.append([html.escape(metric.report_name), a_str, b_str])
|
||||
|
||||
return rows
|
||||
@@ -0,0 +1,191 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import html
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from scipy.stats import mannwhitneyu
|
||||
from scripts.tts_comparison_report.reporting.constants import P_VAL_ROUND_DIGITS
|
||||
from scripts.tts_comparison_report.reporting.metrics import DistributionMetricsRegistry
|
||||
from scripts.tts_comparison_report.reporting.models import BucketData, StatTestAnalysisInfo, StatTestResult, Winner
|
||||
|
||||
|
||||
_SIGNIFICANCE_LEVEL: float = 0.05
|
||||
|
||||
|
||||
class _Alternative(str, Enum):
|
||||
two_sided = "two-sided"
|
||||
greater = "greater"
|
||||
less = "less"
|
||||
|
||||
|
||||
def _run_single_stat_test(
|
||||
baseline: list[float],
|
||||
candidate: list[float],
|
||||
lower_is_better: bool,
|
||||
) -> tuple[Winner, _Alternative, float]:
|
||||
if not baseline:
|
||||
raise ValueError("Baseline sample is empty.")
|
||||
|
||||
if not candidate:
|
||||
raise ValueError("Candidate sample is empty.")
|
||||
|
||||
if len(baseline) != len(candidate):
|
||||
warnings.warn(
|
||||
"\nBaseline and candidate contain different numbers of samples. "
|
||||
"This may indicate missing filewise metrics or dataset mismatch.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# First test whether distributions differ at all, then determine direction.
|
||||
p_val_two_sided = mannwhitneyu(baseline, candidate, alternative="two-sided", method="auto").pvalue
|
||||
|
||||
if p_val_two_sided >= _SIGNIFICANCE_LEVEL:
|
||||
return Winner.tie, _Alternative.two_sided, round(p_val_two_sided, P_VAL_ROUND_DIGITS)
|
||||
|
||||
p_val = mannwhitneyu(baseline, candidate, alternative="less", method="auto").pvalue
|
||||
|
||||
if p_val < _SIGNIFICANCE_LEVEL:
|
||||
winner = Winner.baseline if lower_is_better else Winner.candidate
|
||||
p_val = round(p_val, P_VAL_ROUND_DIGITS)
|
||||
return winner, _Alternative.less, p_val
|
||||
|
||||
p_val = mannwhitneyu(baseline, candidate, alternative="greater", method="auto").pvalue
|
||||
|
||||
if p_val < _SIGNIFICANCE_LEVEL:
|
||||
winner = Winner.candidate if lower_is_better else Winner.baseline
|
||||
p_val = round(p_val, P_VAL_ROUND_DIGITS)
|
||||
return winner, _Alternative.greater, p_val
|
||||
|
||||
return Winner.tie, _Alternative.two_sided, round(p_val_two_sided, P_VAL_ROUND_DIGITS)
|
||||
|
||||
|
||||
def _map_winner_to_name(
|
||||
winner: Winner,
|
||||
baseline_name: str,
|
||||
candidate_name: str,
|
||||
) -> str:
|
||||
if winner == Winner.baseline:
|
||||
return baseline_name
|
||||
if winner == Winner.candidate:
|
||||
return candidate_name
|
||||
return winner.value
|
||||
|
||||
|
||||
def run_stat_tests(
|
||||
bucket_baseline: BucketData,
|
||||
bucket_candidate: BucketData,
|
||||
benchmark_name: Optional[str] = None,
|
||||
) -> list[StatTestResult]:
|
||||
"""Run statistical tests for all distribution metrics.
|
||||
|
||||
Args:
|
||||
bucket_baseline: Baseline bucket data.
|
||||
bucket_candidate: Candidate bucket data.
|
||||
benchmark_name: Benchmark name. If omitted, metric samples are aggregated
|
||||
across all benchmarks.
|
||||
|
||||
Returns:
|
||||
List of StatTestResult instances for configured distribution metrics.
|
||||
|
||||
Raises:
|
||||
ValueError: If metric samples are missing or benchmark data is invalid.
|
||||
"""
|
||||
results = []
|
||||
|
||||
for metric in DistributionMetricsRegistry:
|
||||
winner, alternative, p_value = _run_single_stat_test(
|
||||
baseline=bucket_baseline.get_metric_samples(metric.key, benchmark_name),
|
||||
candidate=bucket_candidate.get_metric_samples(metric.key, benchmark_name),
|
||||
lower_is_better=metric.lower_is_better,
|
||||
)
|
||||
result = StatTestResult(
|
||||
metric_name=metric.report_name,
|
||||
winner=winner,
|
||||
alternative=alternative.value,
|
||||
p_value=p_value,
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def prepare_stat_tests_table_rows(
|
||||
baseline_name: str,
|
||||
candidate_name: str,
|
||||
stat_test_results: list[StatTestResult],
|
||||
) -> list[list[str]]:
|
||||
"""Prepare formatted rows for a statistical test results table.
|
||||
|
||||
Args:
|
||||
baseline_name: Name of the baseline model used in the reports.
|
||||
candidate_name: Name of the candidate model used in the reports.
|
||||
stat_test_results: Statistical test results to format.
|
||||
|
||||
Returns:
|
||||
Table rows containing metric name, winner, alternative hypothesis,
|
||||
and p-value.
|
||||
"""
|
||||
rows = []
|
||||
|
||||
for res in stat_test_results:
|
||||
winner = _map_winner_to_name(res.winner, baseline_name, candidate_name)
|
||||
rows.append(
|
||||
[
|
||||
html.escape(res.metric_name),
|
||||
html.escape(winner),
|
||||
html.escape(res.alternative),
|
||||
html.escape(str(res.p_value)),
|
||||
]
|
||||
)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def prepare_stat_tests_analysis_info(
|
||||
baseline_name: str,
|
||||
candidate_name: str,
|
||||
stat_test_results: list[StatTestResult],
|
||||
) -> StatTestAnalysisInfo:
|
||||
"""Prepare summary information for the statistical test analysis section.
|
||||
|
||||
Args:
|
||||
baseline_name: Name of the baseline model used in the reports.
|
||||
candidate_name: Name of the candidate model used in the reports.
|
||||
stat_test_results: Statistical test results to summarize.
|
||||
|
||||
Returns:
|
||||
Instance of StatTestAnalysisInfo containing the overall winner and its
|
||||
key advantages. If no statistically significant wins are present, both
|
||||
fields are set to `None`.
|
||||
"""
|
||||
a_wins, b_wins = [], []
|
||||
|
||||
for res in stat_test_results:
|
||||
if res.winner == Winner.baseline:
|
||||
a_wins.append(res.metric_name)
|
||||
elif res.winner == Winner.candidate:
|
||||
b_wins.append(res.metric_name)
|
||||
|
||||
if not a_wins and not b_wins:
|
||||
winner, advantages = None, None
|
||||
else:
|
||||
winner, wins = (baseline_name, a_wins) if len(a_wins) >= len(b_wins) else (candidate_name, b_wins)
|
||||
advantages = ", ".join(wins)
|
||||
|
||||
return StatTestAnalysisInfo(
|
||||
winner=winner,
|
||||
advantages=advantages,
|
||||
)
|
||||
Reference in New Issue
Block a user