diff --git a/cspell/library-words.txt b/cspell/library-words.txt index 9b8fe90..dc8d8fa 100644 --- a/cspell/library-words.txt +++ b/cspell/library-words.txt @@ -8,3 +8,4 @@ xlabel ylabel frameon allclose +linalg diff --git a/cspell/project-words.txt b/cspell/project-words.txt index a89cdc5..43b5325 100644 --- a/cspell/project-words.txt +++ b/cspell/project-words.txt @@ -11,3 +11,4 @@ wasserstein bonf bonferroni aeiou +stylometrically diff --git a/pyproject.toml b/pyproject.toml index a11d076..685ceb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "matplotlib>=3.10.8", "scipy>=1.17.0", "seaborn>=0.13.2", + "sentence-transformers>=5.3.0", ] [build-system] @@ -101,6 +102,8 @@ module = [ "seaborn.*", "matplotlib", "matplotlib.*", + "sentence_transformers", + "sentence_transformers.*" ] ignore_missing_imports = true diff --git a/src/voice/__init__.py b/src/voice/__init__.py index 1abc425..5551485 100644 --- a/src/voice/__init__.py +++ b/src/voice/__init__.py @@ -5,12 +5,18 @@ LLMs for stylistic fidelity. """ +from voice.comparison import ( + make_embedding_comparison, + make_stylometric_comparison, +) from voice.datasets import DatasetSpec, get_dataset -from voice.stylometry import get_metrics, make_comparison +from voice.stylometry import get_groups, get_metrics __all__: list[str] = [ "DatasetSpec", "get_metrics", + "get_groups", "get_dataset", - "make_comparison", + "make_embedding_comparison", + "make_stylometric_comparison", ] diff --git a/src/voice/comparison/__init__.py b/src/voice/comparison/__init__.py new file mode 100644 index 0000000..2294a0a --- /dev/null +++ b/src/voice/comparison/__init__.py @@ -0,0 +1,18 @@ +""" +Comparison package for VOICE. + +This package contains: +- Functions for comparing distributions of stylometric metrics +""" + +from voice.comparison.embedding_comparison import make_embedding_comparison +from voice.comparison.stylometric_comparison import ( + make_stylometric_comparison, + stylometric_distribution, +) + +__all__: list[str] = [ + "make_stylometric_comparison", + "make_embedding_comparison", + "stylometric_distribution", +] diff --git a/src/voice/comparison/_utils.py b/src/voice/comparison/_utils.py new file mode 100644 index 0000000..c893e47 --- /dev/null +++ b/src/voice/comparison/_utils.py @@ -0,0 +1,88 @@ +""" +Utilities for the comparison module. + +This module is private by convention and not part of the public API. +""" + +from __future__ import annotations + +from collections.abc import Callable + +import numpy as np + +from voice.stylometry._defaults import CALIBRATION_DEFAULTS + +# ----------------------------------------------------------------------------- +# Calibrated percentile +# ----------------------------------------------------------------------------- + + +def calibrated_percentile(value: float, dist: np.ndarray) -> float: + """ + Compute the calibrated percentile (empirical CDF) of `value` under `dist`. + + This returns: + p = (1/N) * sum_i 1 * [dist_i <= value] + + :param value: Scalar value to evaluate + :param dist: 1D array representing a reference distribution + :return: Empirical CDF value in [0, 1] + :raises ValueError: If `dist` is empty + """ + dist = np.asarray(dist, dtype=float).ravel() + if dist.size == 0: + raise ValueError("dist must be non-empty.") + return float(np.mean(dist <= value)) + + +# ----------------------------------------------------------------------------- +# Bootstrap null distribution +# ----------------------------------------------------------------------------- + + +def bootstrap_null_distribution( + n: int, + score_fn: Callable[[np.ndarray, np.ndarray], float], + *, + sample_size: int = CALIBRATION_DEFAULTS.sample_size, + num_iterations: int = CALIBRATION_DEFAULTS.num_iterations, + seed: int = CALIBRATION_DEFAULTS.seed, +) -> np.ndarray: + """ + Estimate a bootstrap null distribution via random split resampling. + + For each iteration, draw 2*`sample_size` unique indices from [0, n), + split into two non-overlapping groups of size `sample_size`, and + call `score_fn` on the two index arrays to produce a scalar. + + The `score_fn` need not be symmetric. Each suite is responsible for + closing over its own precomputed data (e.g. a metric value array or + an embedding matrix) when constructing the callable. + + :param n: Total number of examples available (typically training set size) + :param score_fn: Callable(idx_a, idx_b) -> float. Receives two + non-overlapping index arrays of length `sample_size` and returns + a scalar score for that split. + :param sample_size: Size of each resampled subset + :param num_iterations: Number of Monte Carlo resampling iterations + :param seed: RNG seed for reproducibility + :return: 1D array of scores of length `num_iterations` + :raises ValueError: If inputs are invalid + """ + if sample_size <= 0: + raise ValueError("sample_size must be a positive integer.") + if num_iterations <= 0: + raise ValueError("num_iterations must be a positive integer.") + if 2 * sample_size > n: + raise ValueError( + f"sample_size must be <= half the available examples ({n / 2})." + ) + + rng = np.random.default_rng(seed) + out = np.empty(num_iterations, dtype=float) + + for i in range(num_iterations): + idx = rng.choice(n, size=2 * sample_size, replace=False) + out[i] = score_fn(idx[:sample_size], idx[sample_size:]) + + return out diff --git a/src/voice/comparison/embedding_comparison.py b/src/voice/comparison/embedding_comparison.py new file mode 100644 index 0000000..9722a8a --- /dev/null +++ b/src/voice/comparison/embedding_comparison.py @@ -0,0 +1,231 @@ +""" +Functions for comparing style via neural embedding similarity. + +This module contains: + - Embedding of text examples using a pretrained SentenceTransformer + - Self-similarity calibration distribution (train/train resampling) + - Comparison of generated completions against the true corpus + +All calibration is performed relative to the training split by design, +to avoid data leakage from validation/test splits. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass + +import numpy as np +from sentence_transformers import SentenceTransformer + +from voice.comparison._utils import ( + bootstrap_null_distribution, + calibrated_percentile, +) +from voice.datasets import VoiceDataset +from voice.datasets.dataset import Example +from voice.stylometry._defaults import ( + CALIBRATION_DEFAULTS, + COMPARISON_DEFAULTS, +) + +# ----------------------------------------------------------------------------- +# Embedding extraction +# ----------------------------------------------------------------------------- + + +def embedding_matrix( + ds: Sequence[Example], + model: SentenceTransformer, +) -> np.ndarray: + """ + Embed a sequence of examples using a pretrained SentenceTransformer. + + Each example's `answer` field is embedded independently. The returned + matrix has one row per example and one column per embedding dimension, + with rows L2-normalised. + + :param ds: Sequence of examples, each expected to have an `answer` field + :param model: Pretrained SentenceTransformer model + :return: Float32 array of shape (len(ds), embedding_dim) + """ + texts = [e.answer for e in ds] + embeddings = model.encode( + texts, convert_to_numpy=True, normalize_embeddings=True + ) + return embeddings.astype(np.float32) + + +def mean_embedding(embeddings: np.ndarray) -> np.ndarray: + """ + Compute the L2-normalised mean of a set of embeddings. + + :param embeddings: Float array of shape (n, d) + :return: L2-normalised 1D array of shape (d,) + """ + mean = embeddings.mean(axis=0) + norm = np.linalg.norm(mean) + if norm == 0.0: + return mean + return mean / norm + + +def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: + """ + Compute cosine similarity between two L2-normalised vectors. + + Assumes both inputs are already L2-normalised, so this reduces + to a dot product. + + :param a: 1D array + :param b: 1D array of the same shape as `a` + :return: Cosine similarity as a float in [-1, 1] + """ + return float(np.dot(a, b)) + + +# ----------------------------------------------------------------------------- +# Calibration: self-similarity distribution +# ----------------------------------------------------------------------------- + + +def self_similarity_distribution( + ds: VoiceDataset, + model: SentenceTransformer, + *, + sample_size: int = CALIBRATION_DEFAULTS.sample_size, + num_iterations: int = CALIBRATION_DEFAULTS.num_iterations, + seed: int = CALIBRATION_DEFAULTS.seed, +) -> np.ndarray: + """ + Estimate the self-similarity distribution via resampling. + + For each iteration, we draw 2*`sample_size` unique examples from the + training split, split them into two non-overlapping groups of size + `sample_size`, and compute the cosine similarity between their + respective mean embeddings. + + :param ds: A VoiceDataset providing a `train` split + :param model: Pretrained SentenceTransformer model + :param sample_size: Size of each resampled subset + :param num_iterations: Number of Monte Carlo resampling iterations + :param seed: RNG seed for reproducibility + :return: 1D array of cosine similarity values of length `num_iterations` + """ + ds_train = ds.train + n_train = len(ds_train) + embeddings = embedding_matrix(ds_train, model) + + return bootstrap_null_distribution( + n_train, + lambda a, b: cosine_similarity( + mean_embedding(embeddings[a]), mean_embedding(embeddings[b]) + ), + sample_size=sample_size, + num_iterations=num_iterations, + seed=seed, + ) + + +# ----------------------------------------------------------------------------- +# Comparison result container +# ----------------------------------------------------------------------------- + + +@dataclass(frozen=True, slots=True) +class EmbeddingComparisonResult: + """ + Result for an embedding-based style comparison. + + .. attribute :: similarity + + Observed cosine similarity between the mean embedding of the + generated completions and the mean embedding of the true corpus. + + .. attribute :: percentile + + Calibrated percentile under the self-similarity distribution. + + .. attribute :: tail + + Right-tail extremeness (1 - percentile). + + .. attribute :: flag + + Boolean indicating percentile >= alpha, i.e. the observed similarity + is not unusually low relative to within-corpus pairs (the model is + indistinguishable from the reference under H0: distinguishable) + """ + + similarity: float + percentile: float + tail: float + flag: bool + + @property + def score(self) -> float: + """Alignment score in [0, 1]; 1 = perfectly indistinguishable.""" + return self.percentile + + +# ----------------------------------------------------------------------------- +# Comparison creation +# ----------------------------------------------------------------------------- + + +def make_embedding_comparison( + completions: Sequence[Example], + true_ds: VoiceDataset, + model: SentenceTransformer, + *, + alpha: float = COMPARISON_DEFAULTS.alpha, +) -> EmbeddingComparisonResult: + """ + Compare style of `completions` to the true corpus via embedding similarity. + + We compute the cosine similarity between the mean embedding of the + generated completions and the mean embedding of the true corpus on the + same split, then calibrate this against the self-similarity distribution + estimated from the true training split. + + H0: the model is stylistically distinguishable from the reference (its + similarity is unusually low). flag=True rejects this null; the observed + similarity is not in the low tail, concluding the model is + indistinguishable. + + :param completions: Sequence of examples to analyse + :param true_ds: VoiceDataset providing the true corpus + :param model: Pretrained SentenceTransformer model + :param alpha: Significance level for hypothesis testing + :return: EmbeddingComparisonResult + :raises ValueError: If completions is empty + """ + if not completions: + raise ValueError("completions must be non-empty.") + + # Split that observed completions are generated on + observed_split = completions[0].split + ds_observed_split = true_ds[observed_split] + + # Observed similarity + gen_embeddings = embedding_matrix(completions, model) + ref_embeddings = embedding_matrix(ds_observed_split, model) + + similarity = cosine_similarity( + mean_embedding(gen_embeddings), + mean_embedding(ref_embeddings), + ) + + # Self-similarity calibration distribution + self_dist = self_similarity_distribution(true_ds, model) + + p = calibrated_percentile(similarity, self_dist) + tail = 1.0 - p + flag = p >= alpha + + return EmbeddingComparisonResult( + similarity=similarity, + percentile=float(p), + tail=float(tail), + flag=flag, + ) diff --git a/src/voice/stylometry/comparison.py b/src/voice/comparison/stylometric_comparison.py similarity index 61% rename from src/voice/stylometry/comparison.py rename to src/voice/comparison/stylometric_comparison.py index 9a5b55e..b5fd959 100644 --- a/src/voice/stylometry/comparison.py +++ b/src/voice/comparison/stylometric_comparison.py @@ -1,10 +1,10 @@ """ -Utilities for comparing stylometric distributions. +Functions for comparing stylometric distributions. This module contains: - Extraction of stylometric distributions from datasets - Self-Wasserstein calibration distribution (train/train resampling) - - Empirical CDF based calibrated percentile + - Comparison of generated completions against the true corpus All calibration is performed relative to the training split by design, to avoid data leakage from validation/test splits. @@ -18,13 +18,18 @@ import numpy as np from scipy.stats import wasserstein_distance -from voice.stylometry.metrics import get_metrics +from voice.comparison._utils import ( + bootstrap_null_distribution, + calibrated_percentile, +) from voice.datasets import VoiceDataset from voice.datasets.dataset import Example from voice.stylometry._defaults import ( CALIBRATION_DEFAULTS, COMPARISON_DEFAULTS, + MetricGroup, ) +from voice.stylometry.metrics import get_metrics # ----------------------------------------------------------------------------- # Distribution extraction @@ -88,52 +93,15 @@ def self_wasserstein_distribution( ds_train = ds.train n_train = len(ds_train) - - if sample_size <= 0: - raise ValueError("sample_size must be a positive integer.") - if num_iterations <= 0: - raise ValueError("num_iterations must be a positive integer.") - if 2 * sample_size > n_train: - raise ValueError( - f"Sample size must be <= half the training set size " - f"({n_train / 2})." - ) - values = stylometric_distribution(ds_train, metric) - rng = np.random.default_rng(seed) - out = np.empty(num_iterations, dtype=float) - - for i in range(num_iterations): - idx = rng.choice(n_train, size=2 * sample_size, replace=False) - a = values[idx[:sample_size]] - b = values[idx[sample_size:]] - out[i] = wasserstein_distance(a, b) - - return out - - -# ----------------------------------------------------------------------------- -# Calibrated percentile -# ----------------------------------------------------------------------------- - - -def calibrated_percentile(value: float, dist: np.ndarray) -> float: - """ - Compute the calibrated percentile (empirical CDF) of `value` under `dist`. - - This returns: - p = (1/N) * sum_i 1 * [dist_i <= value] - - :param value: Scalar value to evaluate - :param dist: 1D array representing a reference distribution - :return: Empirical CDF value in [0, 1] - :raises ValueError: If `dist` is empty - """ - dist = np.asarray(dist, dtype=float).ravel() - if dist.size == 0: - raise ValueError("dist must be non-empty.") - return float(np.mean(dist <= value)) + return bootstrap_null_distribution( + n_train, + lambda a, b: wasserstein_distance(values[a], values[b]), + sample_size=sample_size, + num_iterations=num_iterations, + seed=seed, + ) # ----------------------------------------------------------------------------- @@ -142,7 +110,7 @@ def calibrated_percentile(value: float, dist: np.ndarray) -> float: @dataclass(frozen=True, slots=True) -class ComparisonEntry: +class StylometricComparisonEntry: """ Result for a single stylometric metric comparison. @@ -150,6 +118,10 @@ class ComparisonEntry: Name of the metric. + .. attribute :: group + + The metric group this metric belongs to. + .. attribute :: wasserstein Observed Wasserstein distance between model and reference. @@ -164,32 +136,73 @@ class ComparisonEntry: .. attribute :: flag - Boolean indicating percentile >= 1 - alpha. - - .. attribute :: flag_bonf - - Boolean indicating percentile >= 1 - alpha / N, - where N is the number of metrics in the comparison. + Boolean indicating percentile <= 1 - alpha (uncorrected, + diagnostic only — decision is made at the group level). """ metric: str + group: MetricGroup wasserstein: float percentile: float tail: float flag: bool - flag_bonf: bool + + +@dataclass(frozen=True, slots=True) +class StylometricGroupEntry: + """ + Aggregated result for a stylometric metric group. + + .. attribute :: group + + The metric group. + + .. attribute :: avg_percentile + + Average calibrated percentile across metrics in this group. + + .. attribute :: avg_tail + + Right-tail extremeness (1 - avg_percentile). + + .. attribute :: flag + + Boolean indicating avg_percentile <= 1 - alpha, i.e. the group's + average Wasserstein distance is not unusually large relative to the + self-Wasserstein null (the model is indistinguishable on this group). + """ + + group: MetricGroup + avg_percentile: float + avg_tail: float + flag: bool + + def __repr__(self) -> str: + """ + Represent group entry as a string. + + :return: A string representation of the group entry + """ + return ( + f"StylometricGroupEntry(" + f"avg_percentile={self.avg_percentile:.4f}, " + f"flag={self.flag})" + ) @dataclass(slots=True) -class ComparisonResults: +class StylometricComparisonResults: """ Container for stylometric comparison results across multiple metrics. The object is initialised with a subset of metric names and a significance level `alpha`. Entries can then be added for each metric. - Bonferroni correction is computed automatically using the total - number of metrics stored in this object. + The null hypothesis for each group is that the model is stylometrically + distinguishable from the reference. A group passes (flag=True) when its + average Wasserstein percentile is low enough to reject this null. + The overall result passes (all_pass=True) only when all groups pass, i.e. + an intersection-union test. .. attribute :: metrics @@ -202,7 +215,9 @@ class ComparisonResults: metrics: tuple[str, ...] alpha: float = COMPARISON_DEFAULTS.alpha - _entries: dict[str, ComparisonEntry] = field(default_factory=dict) + _entries: dict[str, StylometricComparisonEntry] = field( + default_factory=dict + ) def __post_init__(self) -> None: """ @@ -221,11 +236,18 @@ def __post_init__(self) -> None: def __repr__(self) -> str: """ - Represent results as a simple metric -> entry mapping. + Represent results as a group -> entry mapping. :return: A string representation of the results """ - return repr(self.as_dict()) + return ( + "{" + + ", ".join( + f"{group.value!r}: {entry}" + for group, entry in self.group_entries.items() + ) + + "}" + ) @property def num_metrics(self) -> int: @@ -233,20 +255,42 @@ def num_metrics(self) -> int: return len(self.metrics) @property - def bonf_threshold(self) -> float: - """Percentile threshold for Bonferroni-corrected significance.""" - return 1.0 - self.alpha / self.num_metrics + def group_entries(self) -> dict[MetricGroup, StylometricGroupEntry]: + """ + Aggregated results keyed by MetricGroup. + + For each group, the average percentile across member metrics is + computed and used to determine group-level flags. + """ + groups: dict[MetricGroup, list[float]] = {} + for entry in self._entries.values(): + groups.setdefault(entry.group, []).append(entry.percentile) + + result: dict[MetricGroup, StylometricGroupEntry] = {} + for group, percentiles in groups.items(): + avg_p = sum(percentiles) / len(percentiles) + avg_tail = 1.0 - avg_p + result[group] = StylometricGroupEntry( + group=group, + avg_percentile=avg_p, + avg_tail=avg_tail, + flag=avg_p <= (1.0 - self.alpha), + ) + return result + + @property + def score(self) -> float: + """Alignment score in [0, 1]; 1 = perfectly indistinguishable.""" + entries = self.group_entries + if not entries: + return 0.0 + return sum(e.avg_tail for e in entries.values()) / len(entries) @property - def non_significant(self) -> tuple[str, ...]: - """Metric names where both flag and flag_bonf are False.""" - return tuple( - m - for m in self.metrics - if m in self._entries - and not self._entries[m].flag - and not self._entries[m].flag_bonf - ) + def all_pass(self) -> bool: + """True if all groups pass; the model is indistinguishable overall.""" + entries = self.group_entries + return bool(entries) and all(e.flag for e in entries.values()) def add( self, @@ -266,38 +310,48 @@ def add( """ if metric not in self.metrics: raise ValueError( - f"Metric '{metric}' not initialised in this ComparisonResults." + f"Metric '{metric}' not initialised in this " + f"StylometricComparisonResults." ) + group = get_metrics()[metric].group tail = 1.0 - percentile - flag = percentile >= (1.0 - self.alpha) - flag_bonf = percentile >= self.bonf_threshold + flag = percentile <= (1.0 - self.alpha) - entry = ComparisonEntry( + entry = StylometricComparisonEntry( metric=metric, + group=group, wasserstein=float(wasserstein), percentile=float(percentile), tail=float(tail), flag=flag, - flag_bonf=flag_bonf, ) self._entries[metric] = entry - def get(self, metric: str) -> ComparisonEntry: + def get(self, metric: str) -> StylometricComparisonEntry: """ Retrieve the result entry for a metric. :param metric: Metric name - :return: ComparisonEntry for metric + :return: StylometricComparisonEntry for metric """ return self._entries[metric] - def as_dict(self) -> dict[str, ComparisonEntry]: + def get_group(self, group: MetricGroup) -> StylometricGroupEntry: + """ + Retrieve the aggregated result entry for a group. + + :param group: MetricGroup + :return: StylometricGroupEntry for the group + """ + return self.group_entries[group] + + def as_dict(self) -> dict[str, StylometricComparisonEntry]: """ - Return a mapping of metric name to ComparisonEntry. + Return a mapping of metric name to StylometricComparisonEntry. - :return: mapping of metric name to ComparisonEntry object + :return: mapping of metric name to StylometricComparisonEntry object """ return dict(self._entries) @@ -307,12 +361,12 @@ def as_dict(self) -> dict[str, ComparisonEntry]: # ----------------------------------------------------------------------------- -def make_comparison( +def make_stylometric_comparison( completions: Sequence[Example], true_ds: VoiceDataset, *, metrics: Sequence[str] | None = None, -) -> ComparisonResults: +) -> StylometricComparisonResults: """ Compare stylometric distributions of `completions` to the true corpus. @@ -322,8 +376,10 @@ def make_comparison( - Calibrated percentile of the observed distance under the self-Wasserstein distribution estimated from the true training split - The returned ComparisonResults includes per-metric flags at level `alpha` - and Bonferroni-corrected flags across the selected metrics. + The returned ComparisonResults includes per-metric flags (diagnostic) and + group-level flags under H0: the model is distinguishable. A group passes + when its average Wasserstein percentile is <= 1 - alpha. The model passes + overall (all_pass=True) only when all groups pass. :param completions: Sequence of examples to analyse :param true_ds: VoiceDataset providing the true corpus @@ -340,7 +396,7 @@ def make_comparison( registry = get_metrics() metric_list = list(registry.keys()) if metrics is None else list(metrics) - results = ComparisonResults(metrics=tuple(metric_list)) + results = StylometricComparisonResults(metrics=tuple(metric_list)) # Split that observed completions are generated on observed_split = completions[0].split diff --git a/src/voice/stylometry/__init__.py b/src/voice/stylometry/__init__.py index bf904f0..e4da059 100644 --- a/src/voice/stylometry/__init__.py +++ b/src/voice/stylometry/__init__.py @@ -6,10 +6,6 @@ - Utilities for comparing distributions of stylometric metrics """ -from voice.stylometry.comparison import ( - make_comparison, - stylometric_distribution, -) from voice.stylometry.metrics import ( calculate_avg_word_length, calculate_char_3gram_moving_avg_type_token_ratio, @@ -28,6 +24,7 @@ calculate_std_word_length, calculate_tri_legomena_ratio, calculate_type_token_ratio, + get_groups, get_metrics, ) from voice.stylometry.plotting import ( @@ -35,6 +32,7 @@ ) __all__: list[str] = [ + "get_groups", "get_metrics", "calculate_avg_word_length", "calculate_std_word_length", @@ -53,7 +51,5 @@ "calculate_char_3gram_moving_avg_type_token_ratio", "calculate_char_4gram_moving_avg_type_token_ratio", "calculate_char_5gram_moving_avg_type_token_ratio", - "stylometric_distribution", "plot_kde", - "make_comparison", ] diff --git a/src/voice/stylometry/_defaults.py b/src/voice/stylometry/_defaults.py index 8db9735..3d76763 100644 --- a/src/voice/stylometry/_defaults.py +++ b/src/voice/stylometry/_defaults.py @@ -6,6 +6,47 @@ """ from dataclasses import dataclass +from enum import Enum + + +class MetricGroup(str, Enum): + """ + Enumeration of stylometric metric groups. + + Metrics within the same group are expected to be correlated; + the group is the unit of analysis for Bonferroni correction. + + .. attribute :: WORD_LENGTH_DISTRIBUTION + + Moments of the word length distribution (mean, std, skew, kurtosis). + + .. attribute :: LEXICAL_RICHNESS + + Type-token ratio and its moving-average variant. + + .. attribute :: LEGOMENA + + Hapax, dis and tri legomena ratios. + + .. attribute :: FUNCTION_WORDS + + Function word usage ratio. + + .. attribute :: CHAR_NGRAM_DIVERSITY + + Character n-gram TTR and MATTR across n = 3, 4, 5. + + .. attribute :: TEXT_LENGTH + + Raw token count. + """ + + WORD_LENGTH_DISTRIBUTION = "word_length_distribution" + LEXICAL_RICHNESS = "lexical_richness" + LEGOMENA = "legomena" + FUNCTION_WORDS = "function_words" + CHAR_NGRAM_DIVERSITY = "char_ngram_diversity" + TEXT_LENGTH = "text_length" @dataclass(frozen=True) diff --git a/src/voice/stylometry/metrics.py b/src/voice/stylometry/metrics.py index 8d0169a..71c1faa 100644 --- a/src/voice/stylometry/metrics.py +++ b/src/voice/stylometry/metrics.py @@ -29,7 +29,7 @@ from collections.abc import Callable from dataclasses import dataclass -from voice.stylometry._defaults import PARAMETER_DEFAULTS +from voice.stylometry._defaults import PARAMETER_DEFAULTS, MetricGroup from voice.stylometry._lexicons import FUNCTION_WORDS from voice.stylometry._sequence_stats import ( length_central_moment_from_units, @@ -55,12 +55,17 @@ class MetricSpec: Function mapping a string to a float. + .. attribute :: group + + The metric group this metric belongs to. + .. attribute :: description Description of the metric. """ fn: MetricFn + group: MetricGroup description: str | None = None def __repr__(self) -> str: @@ -70,21 +75,25 @@ def __repr__(self) -> str: :return: Readable string representation of the MetricSpec """ fn_name = getattr(self.fn, "__name__", repr(self.fn)) - return f"MetricSpec(fn={fn_name}, description={self.description!r})" + return ( + f"MetricSpec(" + f"fn={fn_name}, " + f"group={self.group.name}, " + f"description={self.description!r})" + ) _REGISTRY: dict[str, MetricSpec] = {} def metric( - name: str, *, description: str | None = None + name: str, *, group: MetricGroup, description: str | None = None ) -> Callable[[MetricFn], MetricFn]: """ Register a metric function in the global registry. - Description can also be optionally added. - :param name: Name of the metric (snake case) + :param group: The metric group this metric belongs to :param description: Description of the metric :return: Decorator that registers the metric function """ @@ -101,7 +110,9 @@ def decorator(fn: MetricFn) -> MetricFn: """ if name in _REGISTRY: raise ValueError(f"Duplicate metric name '{name}'") - _REGISTRY[name] = MetricSpec(fn=fn, description=description) + _REGISTRY[name] = MetricSpec( + fn=fn, group=group, description=description + ) return fn return decorator @@ -116,12 +127,25 @@ def get_metrics() -> dict[str, MetricSpec]: return dict(_REGISTRY) +def get_groups() -> set[MetricGroup]: + """ + Return the set of metric groups present in the registry. + + :return: Set of MetricGroup values for all registered metrics + """ + return {spec.group for spec in _REGISTRY.values()} + + # ----------------------------------------------------------------------------- # Word length moment metrics # ----------------------------------------------------------------------------- -@metric("avg_word_length", description="Average word length") +@metric( + "avg_word_length", + group=MetricGroup.WORD_LENGTH_DISTRIBUTION, + description="Average word length", +) def calculate_avg_word_length(text: str) -> float: """ Calculate the average word length of a string. @@ -137,7 +161,11 @@ def calculate_avg_word_length(text: str) -> float: return sum(len(word) for word in words) / len(words) -@metric("std_word_length", description="Standard deviation of word length") +@metric( + "std_word_length", + group=MetricGroup.WORD_LENGTH_DISTRIBUTION, + description="Standard deviation of word length", +) def calculate_std_word_length(text: str) -> float: """ Calculate the standard deviation of word length in a string. @@ -151,7 +179,11 @@ def calculate_std_word_length(text: str) -> float: return math.sqrt(length_central_moment_from_units(words, order=2)) -@metric("skew_word_length", description="Skewness of word length") +@metric( + "skew_word_length", + group=MetricGroup.WORD_LENGTH_DISTRIBUTION, + description="Skewness of word length", +) def calculate_skew_word_length(text: str) -> float: """ Calculate the skewness of word length in a string. @@ -171,7 +203,11 @@ def calculate_skew_word_length(text: str) -> float: return float(m3 / (m2**1.5)) -@metric("kurtosis_word_length", description="Kurtosis of word length") +@metric( + "kurtosis_word_length", + group=MetricGroup.WORD_LENGTH_DISTRIBUTION, + description="Kurtosis of word length", +) def calculate_kurtosis_word_length(text: str) -> float: """ Calculate the kurtosis of word length in a string. @@ -199,7 +235,11 @@ def calculate_kurtosis_word_length(text: str) -> float: # ----------------------------------------------------------------------------- -@metric("num_words", description="Number of words in text") +@metric( + "num_words", + group=MetricGroup.TEXT_LENGTH, + description="Number of words in text", +) def calculate_num_words(text: str) -> float: """ Calculate the number of words in a string. @@ -212,7 +252,11 @@ def calculate_num_words(text: str) -> float: return float(len(word_tokenize(text))) -@metric("hapax_legomena_ratio", description="Ratio of hapax legomena") +@metric( + "hapax_legomena_ratio", + group=MetricGroup.LEGOMENA, + description="Ratio of hapax legomena", +) def calculate_hapax_legomena_ratio(text: str) -> float: """ Calculate the ratio of hapax legomena in a string. @@ -236,7 +280,11 @@ def calculate_hapax_legomena_ratio(text: str) -> float: return hapax / len(words) -@metric("dis_legomena_ratio", description="Ratio of dis legomena") +@metric( + "dis_legomena_ratio", + group=MetricGroup.LEGOMENA, + description="Ratio of dis legomena", +) def calculate_dis_legomena_ratio(text: str) -> float: """ Calculate the ratio of dis legomena in a string. @@ -260,7 +308,11 @@ def calculate_dis_legomena_ratio(text: str) -> float: return dis / len(words) -@metric("tri_legomena_ratio", description="Ratio of tri legomena") +@metric( + "tri_legomena_ratio", + group=MetricGroup.LEGOMENA, + description="Ratio of tri legomena", +) def calculate_tri_legomena_ratio(text: str) -> float: """ Calculate the ratio of tri legomena in a string. @@ -284,7 +336,11 @@ def calculate_tri_legomena_ratio(text: str) -> float: return tri / len(words) -@metric("function_word_ratio", description="Ratio of function words") +@metric( + "function_word_ratio", + group=MetricGroup.FUNCTION_WORDS, + description="Ratio of function words", +) def calculate_function_word_ratio(text: str) -> float: """ Calculate the ratio of function words in a string. @@ -309,7 +365,11 @@ def calculate_function_word_ratio(text: str) -> float: # ----------------------------------------------------------------------------- -@metric("type_token_ratio", description="Type-token ratio") +@metric( + "type_token_ratio", + group=MetricGroup.LEXICAL_RICHNESS, + description="Type-token ratio", +) def calculate_type_token_ratio(text: str) -> float: """ Calculate the type-token ratio (TTR) of a string. @@ -327,6 +387,7 @@ def calculate_type_token_ratio(text: str) -> float: @metric( "moving_avg_type_token_ratio", + group=MetricGroup.LEXICAL_RICHNESS, description="Moving average type-token ratio", ) def calculate_moving_avg_type_token_ratio(text: str) -> float: @@ -360,6 +421,7 @@ def calculate_moving_avg_type_token_ratio(text: str) -> float: @metric( "char_3gram_type_token_ratio", + group=MetricGroup.CHAR_NGRAM_DIVERSITY, description="Type-token ratio of character 3-grams", ) def calculate_char_3gram_type_token_ratio(text: str) -> float: @@ -379,6 +441,7 @@ def calculate_char_3gram_type_token_ratio(text: str) -> float: @metric( "char_4gram_type_token_ratio", + group=MetricGroup.CHAR_NGRAM_DIVERSITY, description="Type-token ratio of character 4-grams", ) def calculate_char_4gram_type_token_ratio(text: str) -> float: @@ -398,6 +461,7 @@ def calculate_char_4gram_type_token_ratio(text: str) -> float: @metric( "char_5gram_type_token_ratio", + group=MetricGroup.CHAR_NGRAM_DIVERSITY, description="Type-token ratio of character 5-grams", ) def calculate_char_5gram_type_token_ratio(text: str) -> float: @@ -422,6 +486,7 @@ def calculate_char_5gram_type_token_ratio(text: str) -> float: @metric( "char_3gram_moving_avg_type_token_ratio", + group=MetricGroup.CHAR_NGRAM_DIVERSITY, description="Moving average type-token ratio of character 3-grams", ) def calculate_char_3gram_moving_avg_type_token_ratio(text: str) -> float: @@ -450,6 +515,7 @@ def calculate_char_3gram_moving_avg_type_token_ratio(text: str) -> float: @metric( "char_4gram_moving_avg_type_token_ratio", + group=MetricGroup.CHAR_NGRAM_DIVERSITY, description="Moving average type-token ratio of character 4-grams", ) def calculate_char_4gram_moving_avg_type_token_ratio(text: str) -> float: @@ -478,6 +544,7 @@ def calculate_char_4gram_moving_avg_type_token_ratio(text: str) -> float: @metric( "char_5gram_moving_avg_type_token_ratio", + group=MetricGroup.CHAR_NGRAM_DIVERSITY, description="Moving average type-token ratio of character 5-grams", ) def calculate_char_5gram_moving_avg_type_token_ratio(text: str) -> float: diff --git a/tests/voice/comparison/test_embedding_comparison.py b/tests/voice/comparison/test_embedding_comparison.py new file mode 100644 index 0000000..f9595cc --- /dev/null +++ b/tests/voice/comparison/test_embedding_comparison.py @@ -0,0 +1,470 @@ +""" +Tests for voice.comparison.embedding_comparison. + +Scope: +- Embedding extraction (embedding_matrix, mean_embedding, cosine_similarity) +- Self-similarity calibration distribution +- EmbeddingComparisonResult dataclass invariants +- make_embedding_comparison end-to-end +""" + +from __future__ import annotations + +import dataclasses + +import numpy as np +import pytest +from datasets import Dataset + +from voice.comparison.embedding_comparison import ( + EmbeddingComparisonResult, + cosine_similarity, + embedding_matrix, + make_embedding_comparison, + mean_embedding, + self_similarity_distribution, +) +from voice.datasets import VoiceDataset +from voice.datasets._schema import Split +from voice.datasets.dataset import Example, _PinnedDatasetSpec + +# ----------------------------------------------------------------------------- +# Helpers / fixtures +# ----------------------------------------------------------------------------- + + +def _canonical_hf_ds(answers: list[str]) -> Dataset: + n = len(answers) + return Dataset.from_dict( + { + "system": [f"s{i}" for i in range(n)], + "question": [f"q{i}" for i in range(n)], + "answer": list(answers), + } + ) + + +def _pinned( + *, + repo_id: str = "ns/name", + revision: str = "a1b2c3d", + splits: tuple[Split, ...] = (Split.TRAIN,), +) -> _PinnedDatasetSpec: + return _PinnedDatasetSpec( + repo_id=repo_id, revision=revision, splits=splits + ) + + +def _ex( + *, + answer: str, + split: Split = Split.TRAIN, +) -> Example: + return Example( + system="s", + question="q", + answer=answer, + spec=_pinned(), + split=split, + ) + + +@pytest.fixture() +def fake_model(monkeypatch): + """ + A SentenceTransformer stand-in whose encode() returns deterministic + L2-normalised vectors derived from the input text lengths. + + Each text of length L gets a 4-dim vector proportional to + [L, 0, 0, 0], then L2-normalised → [1, 0, 0, 0] for any non-empty + string. Empty strings yield the zero vector. + """ + + class _FakeModel: + def encode( + self, + texts: list[str], + convert_to_numpy: bool = True, + normalize_embeddings: bool = True, + ) -> np.ndarray: + _ = convert_to_numpy + __ = normalize_embeddings + out = np.zeros((len(texts), 4), dtype=np.float32) + for i, t in enumerate(texts): + v = np.array([float(len(t)), 1.0, 0.0, 0.0], dtype=np.float32) + norm = np.linalg.norm(v) + if norm > 0: + v /= norm + out[i] = v + return out + + return _FakeModel() + + +# ----------------------------------------------------------------------------- +# embedding_matrix +# ----------------------------------------------------------------------------- + + +def test_embedding_matrix_shape(fake_model): + examples = [_ex(answer="hello"), _ex(answer="world"), _ex(answer="foo")] + mat = embedding_matrix(examples, fake_model) + assert mat.shape == (3, 4) + assert mat.dtype == np.float32 + + +def test_embedding_matrix_rows_are_unit_vectors(fake_model): + examples = [_ex(answer="abc"), _ex(answer="xy")] + mat = embedding_matrix(examples, fake_model) + norms = np.linalg.norm(mat, axis=1) + np.testing.assert_allclose(norms, 1.0, atol=1e-6) + + +def test_embedding_matrix_uses_answer_field(fake_model, monkeypatch): + seen: list[list[str]] = [] + orig_encode = fake_model.encode + + def recording_encode(texts, **kwargs): + seen.append(list(texts)) + return orig_encode(texts, **kwargs) + + monkeypatch.setattr(fake_model, "encode", recording_encode) + + examples = [_ex(answer="one"), _ex(answer="two")] + embedding_matrix(examples, fake_model) + + assert seen == [["one", "two"]] + + +# ----------------------------------------------------------------------------- +# mean_embedding +# ----------------------------------------------------------------------------- + + +def test_mean_embedding_returns_unit_vector(): + mat = np.array([[1.0, 0.0], [0.0, 1.0]], dtype=np.float32) + result = mean_embedding(mat) + assert result.shape == (2,) + np.testing.assert_allclose(np.linalg.norm(result), 1.0, atol=1e-6) + + +def test_mean_embedding_zero_returns_zero(): + mat = np.zeros((3, 4), dtype=np.float32) + result = mean_embedding(mat) + np.testing.assert_array_equal(result, np.zeros(4)) + + +def test_mean_embedding_single_row_returns_same_vector(): + v = np.array([[0.6, 0.8]], dtype=np.float32) + result = mean_embedding(v) + np.testing.assert_allclose(result, [0.6, 0.8], atol=1e-6) + + +# ----------------------------------------------------------------------------- +# cosine_similarity +# ----------------------------------------------------------------------------- + + +def test_cosine_similarity_identical_vectors(): + v = np.array([1.0, 0.0, 0.0]) + assert cosine_similarity(v, v) == pytest.approx(1.0) + + +def test_cosine_similarity_orthogonal_vectors(): + a = np.array([1.0, 0.0]) + b = np.array([0.0, 1.0]) + assert cosine_similarity(a, b) == pytest.approx(0.0) + + +def test_cosine_similarity_opposite_vectors(): + a = np.array([1.0, 0.0]) + b = np.array([-1.0, 0.0]) + assert cosine_similarity(a, b) == pytest.approx(-1.0) + + +def test_cosine_similarity_returns_float(): + a = np.array([1.0, 0.0]) + result = cosine_similarity(a, a) + assert isinstance(result, float) + + +# ----------------------------------------------------------------------------- +# self_similarity_distribution +# ----------------------------------------------------------------------------- + + +def test_self_similarity_distribution_shape(fake_model): + p = _pinned(splits=(Split.TRAIN,)) + vd = VoiceDataset( + datasets={ + Split.TRAIN: _canonical_hf_ds([f"text{i}" for i in range(20)]) + }, + spec=p, + ) + out = self_similarity_distribution( + vd, fake_model, sample_size=5, num_iterations=10, seed=42 + ) + assert out.shape == (10,) + assert out.dtype == float + + +def test_self_similarity_distribution_is_deterministic_given_seed(fake_model): + p = _pinned(splits=(Split.TRAIN,)) + vd = VoiceDataset( + datasets={Split.TRAIN: _canonical_hf_ds([f"t{i}" for i in range(20)])}, + spec=p, + ) + out1 = self_similarity_distribution( + vd, fake_model, sample_size=5, num_iterations=15, seed=7 + ) + out2 = self_similarity_distribution( + vd, fake_model, sample_size=5, num_iterations=15, seed=7 + ) + assert np.array_equal(out1, out2) + + out3 = self_similarity_distribution( + vd, fake_model, sample_size=5, num_iterations=15, seed=99 + ) + assert not np.array_equal(out1, out3) + + +def test_self_similarity_distribution_uses_training_split_only( + fake_model, monkeypatch +): + encode_calls: list[list[str]] = [] + orig_encode = fake_model.encode + + def recording_encode(texts, **kwargs): + encode_calls.append(list(texts)) + return orig_encode(texts, **kwargs) + + monkeypatch.setattr(fake_model, "encode", recording_encode) + + p = _pinned(splits=(Split.TRAIN, Split.TEST)) + vd = VoiceDataset( + datasets={ + Split.TRAIN: _canonical_hf_ds([f"train{i}" for i in range(20)]), + Split.TEST: _canonical_hf_ds(["SHOULD_NOT_APPEAR"]), + }, + spec=p, + ) + self_similarity_distribution( + vd, fake_model, sample_size=5, num_iterations=5 + ) + + all_texts = [t for call in encode_calls for t in call] + assert "SHOULD_NOT_APPEAR" not in all_texts + + +def test_self_similarity_distribution_values_in_range(fake_model): + p = _pinned(splits=(Split.TRAIN,)) + vd = VoiceDataset( + datasets={ + Split.TRAIN: _canonical_hf_ds([f"word{i}" for i in range(30)]) + }, + spec=p, + ) + out = self_similarity_distribution( + vd, fake_model, sample_size=5, num_iterations=20 + ) + assert np.all(out >= -1.0 - 1e-5) and np.all(out <= 1.0 + 1e-5) + + +# ----------------------------------------------------------------------------- +# EmbeddingComparisonResult +# ----------------------------------------------------------------------------- + + +def test_embedding_comparison_result_is_frozen(): + r = EmbeddingComparisonResult( + similarity=0.9, percentile=0.8, tail=0.2, flag=False + ) + assert type(r).__dataclass_params__.frozen is True + with pytest.raises(dataclasses.FrozenInstanceError): + setattr(r, "similarity", 0.5) # noqa: B010 + + +def test_embedding_comparison_result_tail_is_complement_of_percentile(): + r = EmbeddingComparisonResult( + similarity=0.7, percentile=0.6, tail=0.4, flag=False + ) + assert r.tail == pytest.approx(1.0 - r.percentile) + + +def test_embedding_comparison_result_score_equals_percentile(): + r = EmbeddingComparisonResult( + similarity=0.7, percentile=0.6, tail=0.4, flag=False + ) + assert r.score == pytest.approx(r.percentile) + + +# ----------------------------------------------------------------------------- +# make_embedding_comparison +# ----------------------------------------------------------------------------- + + +def test_make_embedding_comparison_rejects_empty_completions(fake_model): + p = _pinned(splits=(Split.TRAIN,)) + true_ds = VoiceDataset( + datasets={Split.TRAIN: _canonical_hf_ds(["a"])}, spec=p + ) + with pytest.raises(ValueError, match=r"completions must be non-empty"): + make_embedding_comparison([], true_ds, fake_model) + + +def test_make_embedding_comparison_returns_result_type( + fake_model, monkeypatch +): + import voice.comparison.embedding_comparison as ec + + monkeypatch.setattr( + ec, + "self_similarity_distribution", + lambda *_a, **_kw: np.array([0.5, 0.6, 0.7, 0.8], dtype=float), + ) + + p = _pinned(splits=(Split.TRAIN, Split.TEST)) + true_ds = VoiceDataset( + datasets={ + Split.TRAIN: _canonical_hf_ds([f"train{i}" for i in range(20)]), + Split.TEST: _canonical_hf_ds([f"ref{i}" for i in range(5)]), + }, + spec=p, + ) + completions = [_ex(answer="hello", split=Split.TEST)] + + result = make_embedding_comparison(completions, true_ds, fake_model) + + assert isinstance(result, EmbeddingComparisonResult) + assert isinstance(result.similarity, float) + assert isinstance(result.percentile, float) + assert isinstance(result.tail, float) + assert isinstance(result.flag, bool) + + +def test_make_embedding_comparison_percentile_and_tail_are_consistent( + fake_model, monkeypatch +): + import voice.comparison.embedding_comparison as ec + + monkeypatch.setattr( + ec, + "self_similarity_distribution", + lambda *_a, **_kw: np.array([0.0, 0.5, 1.0], dtype=float), + ) + + p = _pinned(splits=(Split.TRAIN, Split.TEST)) + true_ds = VoiceDataset( + datasets={ + Split.TRAIN: _canonical_hf_ds([f"t{i}" for i in range(20)]), + Split.TEST: _canonical_hf_ds(["ref"]), + }, + spec=p, + ) + completions = [_ex(answer="hello", split=Split.TEST)] + + result = make_embedding_comparison(completions, true_ds, fake_model) + + assert result.tail == pytest.approx(1.0 - result.percentile) + assert 0.0 <= result.percentile <= 1.0 + assert 0.0 <= result.tail <= 1.0 + + +def test_make_embedding_comparison_flag_false_when_low_percentile( + fake_model, monkeypatch +): + """ + Flag should be False when similarity is low; model is distinguishable. + """ + import voice.comparison.embedding_comparison as ec + + # Self-dist is always high → observed similarity will be low percentile + monkeypatch.setattr( + ec, + "self_similarity_distribution", + lambda *_a, **_kw: np.ones(100, dtype=float), + ) + + p = _pinned(splits=(Split.TRAIN, Split.TEST)) + true_ds = VoiceDataset( + datasets={ + Split.TRAIN: _canonical_hf_ds([f"t{i}" for i in range(20)]), + Split.TEST: _canonical_hf_ds(["ref"]), + }, + spec=p, + ) + completions = [_ex(answer="x", split=Split.TEST)] + + result = make_embedding_comparison( + completions, true_ds, fake_model, alpha=0.01 + ) + assert result.flag is False + + +def test_make_embedding_comparison_flag_true_when_high_percentile( + fake_model, monkeypatch +): + """ + Flag should be True when similarity is high; model is indistinguishable. + """ + import voice.comparison.embedding_comparison as ec + + # Self-dist is always low → observed similarity will be high percentile + monkeypatch.setattr( + ec, + "self_similarity_distribution", + lambda *_a, **_kw: np.zeros(100, dtype=float), + ) + + p = _pinned(splits=(Split.TRAIN, Split.TEST)) + true_ds = VoiceDataset( + datasets={ + Split.TRAIN: _canonical_hf_ds([f"t{i}" for i in range(20)]), + Split.TEST: _canonical_hf_ds(["ref"]), + }, + spec=p, + ) + completions = [_ex(answer="hello world", split=Split.TEST)] + + result = make_embedding_comparison( + completions, true_ds, fake_model, alpha=0.05 + ) + assert result.flag is True + + +def test_make_embedding_comparison_uses_observed_split( + fake_model, monkeypatch +): + """ + The reference embeddings should come from the same split as completions. + """ + import voice.comparison.embedding_comparison as ec + + monkeypatch.setattr( + ec, + "self_similarity_distribution", + lambda *_a, **_kw: np.array([0.5], dtype=float), + ) + + calls: list[object] = [] + orig_getitem = VoiceDataset.__getitem__ + + def recording_getitem(self, key): + calls.append(key) + return orig_getitem(self, key) + + monkeypatch.setattr(VoiceDataset, "__getitem__", recording_getitem) + + p = _pinned(splits=(Split.TRAIN, Split.TEST)) + true_ds = VoiceDataset( + datasets={ + Split.TRAIN: _canonical_hf_ds([f"t{i}" for i in range(20)]), + Split.TEST: _canonical_hf_ds([f"r{i}" for i in range(5)]), + }, + spec=p, + ) + completions = [_ex(answer="hello", split=Split.TEST)] + + make_embedding_comparison(completions, true_ds, fake_model) + + assert Split.TEST in calls diff --git a/tests/voice/stylometry/test_comparison.py b/tests/voice/comparison/test_stylometric_comparison.py similarity index 73% rename from tests/voice/stylometry/test_comparison.py rename to tests/voice/comparison/test_stylometric_comparison.py index 3fa307a..4e28769 100644 --- a/tests/voice/stylometry/test_comparison.py +++ b/tests/voice/comparison/test_stylometric_comparison.py @@ -5,7 +5,7 @@ - Distribution extraction - Self-Wasserstein calibration distribution - Empirical CDF calibrated percentile -- ComparisonResults / ComparisonEntry ergonomics + invariants +- StylometricComparisonResults / StylometricComparisonEntry - Comparison creation """ @@ -19,17 +19,19 @@ import pytest from datasets import Dataset -from voice.datasets import VoiceDataset -from voice.datasets._schema import Split -from voice.datasets.dataset import Example, _PinnedDatasetSpec -from voice.stylometry.comparison import ( - ComparisonEntry, - ComparisonResults, - calibrated_percentile, - make_comparison, +from voice.comparison._utils import calibrated_percentile +from voice.comparison.stylometric_comparison import ( + StylometricComparisonEntry, + StylometricComparisonResults, + StylometricGroupEntry, + make_stylometric_comparison, self_wasserstein_distribution, stylometric_distribution, ) +from voice.datasets import VoiceDataset +from voice.datasets._schema import Split +from voice.datasets.dataset import Example, _PinnedDatasetSpec +from voice.stylometry._defaults import MetricGroup # ----------------------------------------------------------------------------- # Helpers @@ -75,21 +77,26 @@ def _ex( @dataclass(frozen=True, slots=True) class _Metric: fn: Callable[[str], float] + group: MetricGroup @pytest.fixture() def metric_registry(): return { - "len": _Metric(fn=lambda s: float(len(s))), + "len": _Metric( + fn=lambda s: float(len(s)), + group=MetricGroup.TEXT_LENGTH, + ), "vowels": _Metric( - fn=lambda s: float(sum(c in "aeiou" for c in s.lower())) + fn=lambda s: float(sum(c in "aeiou" for c in s.lower())), + group=MetricGroup.LEXICAL_RICHNESS, ), } @pytest.fixture() def patch_metrics(monkeypatch, metric_registry): - import voice.stylometry.comparison as comparison + import voice.comparison.stylometric_comparison as comparison monkeypatch.setattr( comparison, "get_metrics", lambda: dict(metric_registry) @@ -200,7 +207,7 @@ def test_self_wasserstein_distribution_rejects_too_large_sample_size( spec=p, ) - with pytest.raises(ValueError, match=r"<= half the training set size"): + with pytest.raises(ValueError, match=r"<= half the"): self_wasserstein_distribution( vd, metric="len", sample_size=3, num_iterations=1 ) @@ -240,12 +247,14 @@ def raising_metric(s: str) -> float: raise AssertionError("test split should not be touched") return float(len(s)) - import voice.stylometry.comparison as comparison + import voice.comparison.stylometric_comparison as comparison monkeypatch.setattr( comparison, "get_metrics", - lambda: {"len": _Metric(fn=raising_metric)}, + lambda: { + "len": _Metric(fn=raising_metric, group=MetricGroup.TEXT_LENGTH) + }, ) p = _pinned(splits=(Split.TRAIN, Split.TEST)) @@ -266,12 +275,14 @@ def raising_metric(s: str) -> float: def test_self_wasserstein_distribution_is_zero_for_constant_metric( patch_metrics, monkeypatch ): - import voice.stylometry.comparison as comparison + import voice.comparison.stylometric_comparison as comparison monkeypatch.setattr( comparison, "get_metrics", - lambda: {"const": _Metric(fn=lambda _s: 1.0)}, + lambda: { + "const": _Metric(fn=lambda _s: 1.0, group=MetricGroup.TEXT_LENGTH) + }, ) p = _pinned(splits=(Split.TRAIN,)) @@ -287,79 +298,120 @@ def test_self_wasserstein_distribution_is_zero_for_constant_metric( # ----------------------------------------------------------------------------- -# ComparisonEntry / ComparisonResults +# StylometricComparisonEntry / StylometricComparisonResults # ----------------------------------------------------------------------------- def test_comparison_entry_is_frozen_and_slots(): - e = ComparisonEntry( + e = StylometricComparisonEntry( metric="m", + group=MetricGroup.TEXT_LENGTH, wasserstein=1.0, percentile=0.9, tail=0.1, flag=True, - flag_bonf=False, ) assert type(e).__dataclass_params__.frozen is True with pytest.raises(dataclasses.FrozenInstanceError): setattr(e, "metric", "x") # noqa: B010 +def test_comparison_group_entry_is_frozen_and_slots(): + e = StylometricGroupEntry( + group=MetricGroup.TEXT_LENGTH, + avg_percentile=0.9, + avg_tail=0.1, + flag=True, + ) + assert type(e).__dataclass_params__.frozen is True + with pytest.raises(dataclasses.FrozenInstanceError): + setattr(e, "avg_percentile", 0.0) # noqa: B010 + + def test_comparison_results_rejects_unknown_metrics(patch_metrics): with pytest.raises(ValueError, match=r"Unknown metric"): - ComparisonResults(metrics=("len", "nope")) + StylometricComparisonResults(metrics=("len", "nope")) def test_comparison_results_properties_and_add_logic(patch_metrics): - r = ComparisonResults(metrics=("len", "vowels"), alpha=0.1) + # len → TEXT_LENGTH, vowels → LEXICAL_RICHNESS: 2 metrics, 2 groups + r = StylometricComparisonResults(metrics=("len", "vowels"), alpha=0.1) assert r.num_metrics == 2 - assert r.bonf_threshold == 1.0 - 0.1 / 2 - # Boundary checks + # Per-metric entry checks r.add(metric="len", wasserstein=np.float64(1.25), percentile=0.9) e = r.get("len") assert e.metric == "len" + assert e.group == MetricGroup.TEXT_LENGTH assert isinstance(e.wasserstein, float) assert isinstance(e.percentile, float) assert e.tail == pytest.approx(0.1) - assert e.flag is True # >= 1 - alpha - assert e.flag_bonf is False # >= 1 - alpha/2 + assert e.flag is True # 0.9 <= 1 - 0.1 r.add(metric="vowels", wasserstein=0.5, percentile=0.0) e2 = r.get("vowels") - assert e2.flag is False - assert e2.flag_bonf is False + assert e2.group == MetricGroup.LEXICAL_RICHNESS + assert e2.flag is True # 0.0 <= 0.9 + + # Group-level checks + ge_len = r.get_group(MetricGroup.TEXT_LENGTH) + assert ge_len.avg_percentile == pytest.approx(0.9) + assert ge_len.avg_tail == pytest.approx(0.1) + assert ge_len.flag is True # 0.9 <= 0.9 + + ge_vowels = r.get_group(MetricGroup.LEXICAL_RICHNESS) + assert ge_vowels.avg_percentile == pytest.approx(0.0) + assert ge_vowels.flag is True # 0.0 <= 0.9 - assert r.non_significant == ("vowels",) + assert r.all_pass is True d = r.as_dict() d.pop("len") assert "len" in r.as_dict() +def test_comparison_results_score_is_mean_group_tail(patch_metrics): + r = StylometricComparisonResults(metrics=("len", "vowels"), alpha=0.1) + r.add(metric="len", wasserstein=1.0, percentile=0.8) + r.add(metric="vowels", wasserstein=0.5, percentile=0.4) + # TEXT_LENGTH avg_tail = 0.2, LEXICAL_RICHNESS avg_tail = 0.6 → mean = 0.4 + assert r.score == pytest.approx(0.4) + + +def test_comparison_results_all_pass_false_when_group_fails(patch_metrics): + r = StylometricComparisonResults(metrics=("len", "vowels"), alpha=0.1) + # percentile=1.0 > 1 - alpha=0.9 → flag=False → all_pass=False + r.add(metric="len", wasserstein=5.0, percentile=1.0) + r.add(metric="vowels", wasserstein=0.5, percentile=0.0) + assert r.get_group(MetricGroup.TEXT_LENGTH).flag is False + assert r.all_pass is False + + def test_comparison_results_add_rejects_metric_not_initialised(patch_metrics): - r = ComparisonResults(metrics=("len",)) + r = StylometricComparisonResults(metrics=("len",)) with pytest.raises(ValueError, match=r"not initialised"): r.add(metric="vowels", wasserstein=0.0, percentile=0.0) # ----------------------------------------------------------------------------- -# make_comparison +# make_stylometric_comparison # ----------------------------------------------------------------------------- -def test_make_comparison_rejects_empty_completions(patch_metrics): +def test_make_stylometric_comparison_rejects_empty_completions(patch_metrics): p = _pinned(splits=(Split.TRAIN,)) true_ds = VoiceDataset( datasets={Split.TRAIN: _canonical_hf_ds(["a"])}, spec=p ) with pytest.raises(ValueError, match=r"completions must be non-empty"): - make_comparison([], true_ds) + make_stylometric_comparison([], true_ds) -def test_make_comparison_defaults_to_registry_metrics(patch_metrics): +def test_make_stylometric_comparison_defaults_to_registry_metrics( + patch_metrics, +): p = _pinned(splits=(Split.TRAIN, Split.TEST)) true_ds = VoiceDataset( datasets={ @@ -373,14 +425,14 @@ def test_make_comparison_defaults_to_registry_metrics(patch_metrics): _ex(answer="world", split=Split.TEST), ] - import voice.stylometry.comparison as comparison + import voice.comparison.stylometric_comparison as comparison def fake_self_dist(*_args, **_kwargs): return np.array([0.0, 1.0, 2.0, 3.0], dtype=float) comparison.self_wasserstein_distribution = fake_self_dist # type: ignore[assignment] - out = make_comparison(completions, true_ds) + out = make_stylometric_comparison(completions, true_ds) assert out.metrics == ("len", "vowels") for m in out.metrics: @@ -389,7 +441,9 @@ def fake_self_dist(*_args, **_kwargs): assert 0.0 <= entry.percentile <= 1.0 -def test_make_comparison_respects_subset_metrics(patch_metrics, monkeypatch): +def test_make_stylometric_comparison_respects_subset_metrics( + patch_metrics, monkeypatch +): p = _pinned(splits=(Split.TRAIN, Split.TEST)) true_ds = VoiceDataset( datasets={ @@ -400,7 +454,7 @@ def test_make_comparison_respects_subset_metrics(patch_metrics, monkeypatch): ) completions = [_ex(answer="aaa", split=Split.TEST)] - import voice.stylometry.comparison as comparison + import voice.comparison.stylometric_comparison as comparison monkeypatch.setattr( comparison, @@ -408,14 +462,14 @@ def test_make_comparison_respects_subset_metrics(patch_metrics, monkeypatch): lambda *_args, **_kwargs: np.array([0.0, 1.0], dtype=float), ) - out = make_comparison(completions, true_ds, metrics=["len"]) + out = make_stylometric_comparison(completions, true_ds, metrics=["len"]) assert out.metrics == ("len",) assert "len" in out.as_dict() with pytest.raises(KeyError): _ = out.get("vowels") -def test_make_comparison_uses_observed_split_from_completions( +def test_make_stylometric_comparison_uses_observed_split_from_completions( patch_metrics, monkeypatch ): p = _pinned(splits=(Split.TRAIN, Split.TEST)) @@ -428,7 +482,7 @@ def test_make_comparison_uses_observed_split_from_completions( ) completions = [_ex(answer="hello", split=Split.TEST)] - import voice.stylometry.comparison as comparison + import voice.comparison.stylometric_comparison as comparison monkeypatch.setattr( comparison, @@ -445,6 +499,6 @@ def recording_getitem(self, key): monkeypatch.setattr(VoiceDataset, "__getitem__", recording_getitem) - _ = make_comparison(completions, true_ds, metrics=["len"]) + _ = make_stylometric_comparison(completions, true_ds, metrics=["len"]) assert Split.TEST in calls diff --git a/tests/voice/stylometry/test_metrics.py b/tests/voice/stylometry/test_metrics.py index a678ca2..c4f49ac 100644 --- a/tests/voice/stylometry/test_metrics.py +++ b/tests/voice/stylometry/test_metrics.py @@ -17,6 +17,7 @@ import pytest import voice.stylometry.metrics as metrics +from voice.stylometry._defaults import MetricGroup # ----------------------------------------------------------------------------- # Helpers @@ -66,6 +67,13 @@ def test_get_metrics_returns_copy(): assert m1.keys() == m2.keys() +def test_get_groups_returns_set_of_metric_groups(): + g = metrics.get_groups() + assert isinstance(g, set) + assert all(isinstance(group, MetricGroup) for group in g) + assert g == set(MetricGroup) + + def test_get_metrics_contains_expected_metric_names(): expected = { "avg_word_length", @@ -92,29 +100,84 @@ def test_get_metrics_contains_expected_metric_names(): def test_metric_decorator_rejects_duplicate_names(monkeypatch): # Isolate decorator behaviour by swapping the module registry. monkeypatch.setattr(metrics, "_REGISTRY", {}) - metrics.metric("x")(lambda t: 0.0) + metrics.metric("x", group=MetricGroup.TEXT_LENGTH)(lambda t: 0.0) with pytest.raises(ValueError, match=r"Duplicate metric name 'x'"): - metrics.metric("x")(lambda t: 1.0) + metrics.metric("x", group=MetricGroup.TEXT_LENGTH)(lambda t: 1.0) def test_metric_spec_is_frozen(): - spec = metrics.MetricSpec(fn=lambda t: 0.0, description="x") + spec = metrics.MetricSpec( + fn=lambda t: 0.0, + group=MetricGroup.TEXT_LENGTH, + description="x", + ) assert type(spec).__dataclass_params__.frozen is True with pytest.raises(dataclasses.FrozenInstanceError): setattr(spec, "description", "y") # noqa: B010 -def test_metric_spec_repr_contains_function_name_and_description(): +def test_metric_spec_repr_contains_function_name_group_and_description(): spec = metrics.MetricSpec( - fn=metrics.calculate_avg_word_length, description="Average word length" + fn=metrics.calculate_avg_word_length, + group=MetricGroup.WORD_LENGTH_DISTRIBUTION, + description="Average word length", ) r = repr(spec) assert "MetricSpec(" in r assert "calculate_avg_word_length" in r + assert "WORD_LENGTH_DISTRIBUTION" in r assert "Average word length" in r +# ----------------------------------------------------------------------------- +# MetricGroup +# ----------------------------------------------------------------------------- + + +def test_all_metrics_have_a_valid_group(): + for name, spec in _all_metric_items(): + assert isinstance(spec.group, MetricGroup), ( + f"Metric '{name}' has group {spec.group!r}, expected a MetricGroup" + ) + + +def test_metric_groups_cover_all_enum_values(): + assigned = {spec.group for _, spec in _all_metric_items()} + assert assigned == set(MetricGroup) + + +_EXPECTED_GROUPS: dict[str, MetricGroup] = { + "avg_word_length": MetricGroup.WORD_LENGTH_DISTRIBUTION, + "std_word_length": MetricGroup.WORD_LENGTH_DISTRIBUTION, + "skew_word_length": MetricGroup.WORD_LENGTH_DISTRIBUTION, + "kurtosis_word_length": MetricGroup.WORD_LENGTH_DISTRIBUTION, + "num_words": MetricGroup.TEXT_LENGTH, + "hapax_legomena_ratio": MetricGroup.LEGOMENA, + "dis_legomena_ratio": MetricGroup.LEGOMENA, + "tri_legomena_ratio": MetricGroup.LEGOMENA, + "function_word_ratio": MetricGroup.FUNCTION_WORDS, + "type_token_ratio": MetricGroup.LEXICAL_RICHNESS, + "moving_avg_type_token_ratio": MetricGroup.LEXICAL_RICHNESS, + "char_3gram_type_token_ratio": MetricGroup.CHAR_NGRAM_DIVERSITY, + "char_4gram_type_token_ratio": MetricGroup.CHAR_NGRAM_DIVERSITY, + "char_5gram_type_token_ratio": MetricGroup.CHAR_NGRAM_DIVERSITY, + "char_3gram_moving_avg_type_token_ratio": MetricGroup.CHAR_NGRAM_DIVERSITY, + "char_4gram_moving_avg_type_token_ratio": MetricGroup.CHAR_NGRAM_DIVERSITY, + "char_5gram_moving_avg_type_token_ratio": MetricGroup.CHAR_NGRAM_DIVERSITY, +} + + +@pytest.mark.parametrize( + "metric_name,expected_group", _EXPECTED_GROUPS.items() +) +def test_metric_group_assignments( + metric_name: str, expected_group: MetricGroup +): + spec = metrics.get_metrics()[metric_name] + assert spec.group == expected_group + + # ----------------------------------------------------------------------------- # Global invariants (over all metrics) # ----------------------------------------------------------------------------- diff --git a/uv.lock b/uv.lock index d0b7570..dd1c64b 100644 --- a/uv.lock +++ b/uv.lock @@ -117,6 +117,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "anyio" version = "4.12.1" @@ -572,6 +581,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, ] +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/f9/1b9b60a30fc463c14cdea7a77228131a0ccc89572e8df9cb86c9648271ab/cuda_pathfinder-1.5.2-py3-none-any.whl", hash = "sha256:0c5f160a7756c5b072723cbbd6d861e38917ef956c68150b02f0b6e9271c71fa", size = 49988, upload-time = "2026-04-06T23:01:05.17Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -1116,6 +1194,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + [[package]] name = "json5" version = "0.13.0" @@ -1460,6 +1547,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1598,6 +1697,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mistune" version = "3.2.0" @@ -1607,6 +1715,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "multidict" version = "6.7.1" @@ -1829,6 +1946,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -1927,6 +2053,155 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, ] +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -2563,6 +2838,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, + { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, + { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -2611,6 +2974,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + [[package]] name = "rpds-py" version = "0.30.0" @@ -2718,6 +3094,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + [[package]] name = "scipy" version = "1.17.0" @@ -2802,6 +3244,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] +[[package]] +name = "sentence-transformers" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/26/448453925b6ce0c29d8b54327caa71ee4835511aef02070467402273079c/sentence_transformers-5.3.0.tar.gz", hash = "sha256:414a0a881f53a4df0e6cbace75f823bfcb6b94d674c42a384b498959b7c065e2", size = 403330, upload-time = "2026-03-12T14:53:40.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/9c/2fa7224058cad8df68d84bafee21716f30892cecc7ad1ad73bde61d23754/sentence_transformers-5.3.0-py3-none-any.whl", hash = "sha256:dca6b98db790274a68185d27a65801b58b4caf653a4e556b5f62827509347c7d", size = 512390, upload-time = "2026-03-12T14:53:39.035Z" }, +] + [[package]] name = "setuptools" version = "80.10.2" @@ -2852,6 +3313,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "terminado" version = "0.18.1" @@ -2866,6 +3339,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, ] +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + [[package]] name = "tinycss2" version = "1.4.0" @@ -2878,6 +3360,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, +] + [[package]] name = "tornado" version = "6.5.4" @@ -2918,6 +3469,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "transformers" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/88/ae8320064e32679a5429a2c9ebbc05c2bf32cefb6e076f9b07f6d685a9b4/transformers-5.3.0-py3-none-any.whl", hash = "sha256:50ac8c89c3c7033444fb3f9f53138096b997ebb70d4b5e50a2e810bf12d3d29a", size = 10661827, upload-time = "2026-03-04T17:41:42.722Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + [[package]] name = "typer-slim" version = "0.21.1" @@ -2992,6 +3595,7 @@ dependencies = [ { name = "matplotlib" }, { name = "scipy" }, { name = "seaborn" }, + { name = "sentence-transformers" }, ] [package.dev-dependencies] @@ -3017,6 +3621,7 @@ requires-dist = [ { name = "matplotlib", specifier = ">=3.10.8" }, { name = "scipy", specifier = ">=1.17.0" }, { name = "seaborn", specifier = ">=0.13.2" }, + { name = "sentence-transformers", specifier = ">=5.3.0" }, ] [package.metadata.requires-dev]