From f7efbfd61ee1ce667183305fd77c513e003f7ed0 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Fri, 28 Aug 2026 22:11:48 +0100 Subject: [PATCH 1/7] Name a song's sections automatically, when the user asks for it Adds automatic functional song-structure analysis on top of All-In-One (mir-aidj/all-in-one). After separation, an isolated worker maps StemDeck's six stems into the model's four-stem layout, runs inference, and normalizes the result into the existing editable Sections schema. Every field of the model's output is treated as untrusted. The stage is non-fatal by construction: a failure, a timeout, a stall, or a missing model leaves an otherwise usable separation exactly as it was. Cancellation stays authoritative so a long CPU pass can be stopped. Off by default -------------- Structure extraction costs a CPU inference pass per job and its labels are suggestions rather than ground truth, so nobody pays for it without asking. A "Song structure" toggle sits beside Split stems and writes to the server, not to the browser, so two devices pointed at one StemDeck cannot disagree about whether the next import pays for it. The runner reads the setting once per job, so a change applies to the next import without a restart. STEMDECK_AUTO_SECTIONS=1 turns it on for a deployment. Four defects found by evaluating nine real tracks ------------------------------------------------- Inference was run once per track and cached, so before and after were compared on byte-identical model output. Two tracks produced no sections at all and now work; none regressed. Wish You Were Here beat confidence 60 0 -> 12 sections Drum For Your Life 64 12 -> 14 Defying Gravity 30 0 -> 21 Come As You Are 89 13 -> 13 five others unchanged 1. A span past the analyzed duration discarded the whole song. duration_sec is rounded while the model reads the stems, so its timeline overhangs by a fraction of a second. One track ended with a 10 ms span beyond it, which clamped to zero length and was read as malformed. Malformed input is still rejected; a span with no overlap left to keep is now skipped. 2. start and end are ordinary label classes, not only brackets. One track was labelled start for 34 seconds in its middle and end for 32 seconds. Rejecting the song over a misplaced sentinel threw away every section. Sentinels are now stripped only at the extremes of the timeline, and an interior one becomes the neutral part it always meant. 3. Boundary discovery was gated on beat-grid confidence, so refinement did nothing at all below the trust threshold. That is precisely where upstream spans most need splitting: rubato, live and free-time material is the hardest to track rhythmically. Discovery now always runs, and the grid only aligns candidates that evidence has already accepted. 4. Section analysis could not run on Windows at all. The Hugging Face cache populates itself with symlinks, which an unelevated process cannot create, and the resulting WinError 1314 is an OSError rather than the PermissionError the hub falls back on, so the download crashed instead of copying. Zero of four tracks fetched the model before the fix and six of six after. Both entry points that download it now set HF_HUB_DISABLE_SYMLINKS. Adjacent sections that share a kind are numbered ------------------------------------------------ The model predicts boundaries and labels with separate heads, so two neighbouring Verse spans are a real predicted boundary between verse one and verse two rather than a duplicate. An earlier revision merged them and silently discarded five true boundaries on Come As You Are. The boundary is kept and the labels read Chorus 1 and Chorus 2 instead, which keeps the data and stops the timeline looking broken. A kind used once is never numbered, and a section renamed by hand keeps its own name. Known limits ------------ The Harmonix label set has no refrain or pre-chorus class, so some sections cannot be named correctly by construction, and label accuracy falls off in the back half of some songs. Sections are presented as experimental, draggable suggestions, which is what they are. Feeding the model its own four-stem separation instead of StemDeck's remixed six-stem audio raised boundary recall from seven of nine to nine of nine on the reference track. That is measured on one song and would cost a second separation pass per job, so it is left as a follow-up rather than taken here. Note: this adds all-in-one-infer to the dependency set, so uv.lock changes and existing desktop installs will be sent to the full download rather than an in-app update. --- app/api/jobs.py | 52 +- app/core/config.py | 26 + app/core/models.py | 4 +- app/core/registry.py | 1 + app/core/settings.py | 24 + app/main.py | 8 + app/pipeline/runner.py | 24 +- app/pipeline/section_refine.py | 329 ++++++++++++ app/pipeline/section_worker.py | 128 +++++ app/pipeline/sections.py | 391 ++++++++++++++ app/pipeline/warmup.py | 31 +- desktop/src-tauri/src/main.rs | 5 +- docs/models.md | 14 + pyproject.toml | 6 + static/css/daw.css | 17 + static/index.html | 13 +- static/js/catalog.js | 3 +- static/js/i18n.js | 117 +++++ static/js/job.js | 11 +- static/js/main.js | 41 ++ static/js/sections.js | 124 ++++- tests/conftest.py | 4 + tests/js/sections.test.mjs | 145 ++++++ tests/test_jobs_api.py | 84 ++- tests/test_network_gate.py | 35 ++ tests/test_pipeline_runner.py | 183 +++++++ tests/test_pipeline_sections.py | 480 ++++++++++++++++++ tests/test_pipeline_warmup.py | 64 +++ tests/test_registry_persistence.py | 24 + uv.lock | 790 ++++++++++++++++++++++++----- 30 files changed, 3010 insertions(+), 168 deletions(-) create mode 100644 app/pipeline/section_refine.py create mode 100644 app/pipeline/section_worker.py create mode 100644 app/pipeline/sections.py create mode 100644 tests/js/sections.test.mjs create mode 100644 tests/test_pipeline_sections.py create mode 100644 tests/test_pipeline_warmup.py diff --git a/app/api/jobs.py b/app/api/jobs.py index bc68918f..76fd6978 100644 --- a/app/api/jobs.py +++ b/app/api/jobs.py @@ -4,11 +4,14 @@ import json import logging import math +import os import re import shutil import subprocess +import threading import uuid from pathlib import Path +from typing import Literal from fastapi import APIRouter, HTTPException, Request from fastapi.responses import JSONResponse, Response @@ -396,6 +399,20 @@ async def start_vocal_split(job_id: str) -> Response: _SECTION_ID_RE = re.compile(r"^[a-zA-Z0-9_\-]{1,64}$") _COLOR_RE = re.compile(r"^#[0-9a-fA-F]{3,8}$") +_SECTIONS_WRITE_LOCK = threading.Lock() + + +def _write_json_atomic(path: Path, data: dict) -> None: + """Durably replace a JSON file without exposing a partial write.""" + temp = path.parent / f".{path.name}.{uuid.uuid4().hex}.tmp" + try: + with temp.open("w", encoding="utf-8", newline="\n") as handle: + handle.write(json.dumps(data, indent=2) + "\n") + handle.flush() + os.fsync(handle.fileno()) + os.replace(temp, path) + finally: + temp.unlink(missing_ok=True) class SectionItem(BaseModel): @@ -404,6 +421,10 @@ class SectionItem(BaseModel): start: float end: float color: str + kind: ( + Literal["intro", "outro", "break", "bridge", "inst", "solo", "verse", "chorus", "part"] + | None + ) = None @field_validator("id") @classmethod @@ -445,30 +466,29 @@ def update_sections(job_id: str, body: SectionsBody) -> dict: if job is None: raise HTTPException(status_code=404, detail="job not found") - validated = [s.model_dump() for s in body.sections] - job.sections = validated + validated = [s.model_dump(exclude_none=True) for s in body.sections] job_dir = (JOBS_DIR / job_id).resolve() if not job_dir.is_relative_to(JOBS_DIR.resolve()): raise HTTPException(status_code=404, detail="job not found") meta_path = job_dir / "metadata.json" - meta: dict = {} - if meta_path.is_file(): + with _SECTIONS_WRITE_LOCK: + meta: dict = {} try: - meta = json.loads(meta_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - pass - meta["sections"] = validated - try: - meta_path.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8") - except OSError as exc: - logger.exception("failed to write sections for %s: %s", job_id, exc) - raise HTTPException(status_code=500, detail="failed to save sections") from exc - - registry_persist(JOBS_DIR) + if meta_path.is_file(): + meta = json.loads(meta_path.read_text(encoding="utf-8")) + meta["sections"] = validated + meta["sections_source"] = "manual" + _write_json_atomic(meta_path, meta) + except (OSError, json.JSONDecodeError) as exc: + logger.exception("failed to write sections for %s: %s", job_id, exc) + raise HTTPException(status_code=500, detail="failed to save sections") from exc + + _set(job, sections=validated, sections_source="manual") + registry_persist(JOBS_DIR) - return {"job_id": job_id, "sections": validated} + return {"job_id": job_id, "sections": validated, "sections_source": "manual"} # Upper bound on an edited grid. A 20-minute track at 300 BPM is ~6000 beats; diff --git a/app/core/config.py b/app/core/config.py index feb00d5b..11e2635f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -245,6 +245,32 @@ def js_solver_available() -> bool: TIMEOUT_FFMPEG = _env_int("STEMDECK_TIMEOUT_FFMPEG", 300) TIMEOUT_ANALYZE = _env_int("STEMDECK_TIMEOUT_ANALYZE", 120) TIMEOUT_DEMUCS_STALL = _env_int("STEMDECK_TIMEOUT_DEMUCS_STALL", 1800) +# Automatic functional-section analysis. Inference stays on CPU because the +# persistent Demucs worker deliberately keeps its model resident on the chosen +# accelerator between jobs; loading a second model beside it would make VRAM +# use depend on GPU size and the preceding job. The ensemble name remains +# configurable for deployments evaluating a different compatible checkpoint. +SECTION_MODEL = os.environ.get("STEMDECK_SECTION_MODEL", "harmonix-all").strip() or "harmonix-all" +TIMEOUT_SECTIONS = max(60, _env_int("STEMDECK_TIMEOUT_SECTIONS", 30 * 60)) +TIMEOUT_SECTIONS_STALL = max(30, _env_int("STEMDECK_TIMEOUT_SECTIONS_STALL", 120)) +# Conservative evidence gates for the section refiner. The first real-song +# diagnostic found that the upstream decoder emitted 13 Come As You Are spans +# but our equal-label merge hid six boundaries. It also found a suppressed +# 0.059 activation inside the intro with only 0.18 embedding novelty. Requiring +# 0.05 activation and 0.35 novelty preserves strong independent evidence +# without turning each instrumental entrance into a new functional section. +SECTION_REFINEMENT_GRID_MIN_CONFIDENCE = 70 +SECTION_REFINEMENT_BEAT_SNAP_SECONDS = 0.12 +SECTION_REFINEMENT_MIN_ACTIVATION = 0.05 +SECTION_REFINEMENT_MIN_NOVELTY = 0.35 +SECTION_REFINEMENT_NOVELTY_WINDOW_SECONDS = 8.0 +SECTION_REFINEMENT_MIN_SEGMENT_SECONDS = 6.0 +# The test track's disputed 49.85-65.89 span had a 0.045 Verse/Chorus margin, +# while the surrounding accepted semantic spans were at least 0.10. A weak +# tie becomes neutral Part rather than a confidently wrong functional label. +SECTION_REFINEMENT_MIN_LABEL_MARGIN = 0.08 +SECTION_REFINEMENT_RECURRENCE_SIMILARITY = 0.85 +SECTION_REFINEMENT_RECURRENCE_LABEL_MARGIN = 0.25 # On-demand lead/backing vocal split (#275). UVR-MDX-NET Karaoke 2 is an # officially-distributed UVR-project model (MIT + credit-to-UVR per the # audio-separator README) -- the default. STEMDECK_KARAOKE_MODEL lets a diff --git a/app/core/models.py b/app/core/models.py index 77fefde8..593634cb 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -48,7 +48,8 @@ class Job: dynamic_range: float | None = None # peak_db - integrated LUFS (dB) tempo_stability: int | None = None # 0-100, beat interval consistency stem_presence: dict[str, int] | None = None # per-stem RMS 0-100 - sections: list[dict] | None = None # [{id, name, start, end, color}] + sections: list[dict] | None = None # [{id, name, kind?, start, end, color}] + sections_source: Literal["automatic", "manual"] | None = None tags: list[str] | None = None # YouTube tags + categories, lowercased, max 8 stems: list[dict[str, str]] = field(default_factory=list) # Subset of stems the user chose at submit. The pipeline produces all @@ -130,6 +131,7 @@ def to_state(self) -> dict[str, Any]: "tempo_stability": self.tempo_stability, "stem_presence": self.stem_presence, "sections": self.sections, + "sections_source": self.sections_source, "tags": self.tags, "stems": self.stems, "selected_stems": self.selected_stems, diff --git a/app/core/registry.py b/app/core/registry.py index 82f6f75d..dc70c39b 100644 --- a/app/core/registry.py +++ b/app/core/registry.py @@ -292,6 +292,7 @@ def _recover_done_job(job_dir: Path) -> Job | None: tempo_stability=meta.get("tempo_stability"), stem_presence=meta.get("stem_presence"), sections=meta.get("sections"), + sections_source=meta.get("sections_source"), tags=meta.get("tags"), vocal_split=vocal_split, ) diff --git a/app/core/settings.py b/app/core/settings.py index 6772b395..c50da598 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -212,6 +212,30 @@ def set_auto_delete_jobs(value: bool) -> bool: return bool(value) +def _default_auto_sections() -> bool: + """Automatic song-structure detection, off until the user asks for it. + + The stage costs a CPU inference pass per job and produces suggestions + rather than ground truth, so nobody should pay for it without having + chosen to. STEMDECK_AUTO_SECTIONS=1 turns it on for a deployment that + wants it from first boot. + """ + return os.environ.get("STEMDECK_AUTO_SECTIONS", "").strip() == "1" + + +def get_auto_sections() -> bool: + with _LOCK: + v = _ensure().get("auto_sections") + return v if isinstance(v, bool) else _default_auto_sections() + + +def set_auto_sections(value: bool) -> bool: + with _LOCK: + _ensure()["auto_sections"] = bool(value) + _save() + return bool(value) + + def _default_auto_delete_days() -> int: """Honour a STEMDECK_JOB_TTL_SECONDS somebody already tuned. diff --git a/app/main.py b/app/main.py index 2565197a..f51ff989 100644 --- a/app/main.py +++ b/app/main.py @@ -47,6 +47,7 @@ get_allow_network, get_auto_delete_days, get_auto_delete_jobs, + get_auto_sections, get_cookies_file, get_demucs_device, get_demucs_device_choice, @@ -60,6 +61,7 @@ set_allow_network, set_auto_delete_days, set_auto_delete_jobs, + set_auto_sections, set_cookies_file, set_demucs_device, set_export_sample_rate, @@ -324,6 +326,10 @@ def _settings_payload() -> dict[str, object]: # rather than appearing empty on first click. "auto_delete_jobs": get_auto_delete_jobs(), "auto_delete_days": get_auto_delete_days(), + # Automatic song-structure detection. Costs a CPU inference pass per + # job and produces suggestions rather than ground truth, so the user + # decides whether to pay for it. + "auto_sections": get_auto_sections(), "auto_delete_days_min": AUTO_DELETE_DAYS_MIN, "auto_delete_days_max": AUTO_DELETE_DAYS_MAX, "max_duration_sec": get_max_duration_sec(), @@ -374,6 +380,8 @@ async def update_settings(request: Request) -> dict[str, object]: set_allow_network(bool(body["allow_network"])) if "auto_delete_jobs" in body: set_auto_delete_jobs(bool(body["auto_delete_jobs"])) + if "auto_sections" in body: + set_auto_sections(bool(body["auto_sections"])) for key, setter in ( ("auto_delete_days", set_auto_delete_days), ("max_duration_sec", set_max_duration_sec), diff --git a/app/pipeline/runner.py b/app/pipeline/runner.py index ccd63d02..002f1e3e 100644 --- a/app/pipeline/runner.py +++ b/app/pipeline/runner.py @@ -14,6 +14,7 @@ from app.core.models import Job, JobCancelled, _set from app.core.redact import redact from app.core.registry import persist as persist_registry +from app.core.settings import get_auto_sections from app.pipeline.analyze import analyze from app.pipeline.beatgrid import compute_beat_grid from app.pipeline.collect import ( @@ -25,6 +26,7 @@ ) from app.pipeline.download import download from app.pipeline.errors import classify_failure +from app.pipeline.sections import detect_sections from app.pipeline.separate import separate logger = logging.getLogger("stemdeck.pipeline") @@ -213,7 +215,25 @@ def _run_common(job: Job, source: Path, job_dir: Path) -> None: compute_beat_grid(stems_dir) except Exception: logger.exception("beat grid stage failed for job %s", job.id) - _lap(job, "beatgrid", mark) + mark = _lap(job, "beatgrid", mark) + + # Automatic sections are suggestions and never make an otherwise usable + # separation fail. Cancellation remains authoritative so a user can still + # stop a long CPU inference pass immediately. The setting is read here, per + # job, rather than captured at import, so turning the toggle off applies to + # the next job without a restart. + _check_cancel(job) + if get_auto_sections() and job.sections is None and job.duration_sec and job.duration_sec > 0: + _set(job, stage="Analyzing song structure...") + try: + sections = detect_sections(job, stems_dir, job.duration_sec) + if sections: + _set(job, sections=sections, sections_source="automatic") + except JobCancelled: + raise + except Exception: + logger.exception("section analysis stage failed for job %s", job.id) + _lap(job, "sections", mark) def _run_blocking(job: Job, url: str, job_dir: Path) -> None: @@ -246,6 +266,8 @@ def _write_metadata(job: Job, job_dir: Path) -> None: "dynamic_range": job.dynamic_range, "tempo_stability": job.tempo_stability, "stem_presence": job.stem_presence, + "sections": job.sections, + "sections_source": job.sections_source, "tags": job.tags, "has_video": job.has_video, "video_status": job.video_status, diff --git a/app/pipeline/section_refine.py b/app/pipeline/section_refine.py new file mode 100644 index 00000000..66258b4f --- /dev/null +++ b/app/pipeline/section_refine.py @@ -0,0 +1,329 @@ +"""Conservative evidence refinement for automatic functional sections. + +All-In-One predicts boundaries independently from functional labels. This +module preserves those boundaries even when neighboring labels are equal, +adds only suppressed peaks supported by a real embedding change, aligns close +predictions to a trustworthy beat grid, and replaces ambiguous labels with a +neutral ``part`` label. + +The functions are deliberately independent from All-In-One classes so the +numeric behavior can be covered with small deterministic arrays. +""" + +from __future__ import annotations + +import bisect +import math +from collections.abc import Sequence +from numbers import Real + +import numpy as np + +from app.core.config import ( + SECTION_REFINEMENT_BEAT_SNAP_SECONDS, + SECTION_REFINEMENT_GRID_MIN_CONFIDENCE, + SECTION_REFINEMENT_MIN_ACTIVATION, + SECTION_REFINEMENT_MIN_LABEL_MARGIN, + SECTION_REFINEMENT_MIN_NOVELTY, + SECTION_REFINEMENT_MIN_SEGMENT_SECONDS, + SECTION_REFINEMENT_NOVELTY_WINDOW_SECONDS, + SECTION_REFINEMENT_RECURRENCE_LABEL_MARGIN, + SECTION_REFINEMENT_RECURRENCE_SIMILARITY, +) + +_SENTINELS = frozenset(("start", "end")) +_NEUTRAL_LABEL = "part" +_BOUNDARY_TOLERANCE_SECONDS = 0.25 + + +def _number(value: object) -> float | None: + if isinstance(value, bool) or not isinstance(value, Real): + return None + result = float(value) + return result if math.isfinite(result) else None + + +def _fallback(raw_segments: object) -> list[dict[str, object]]: + if not isinstance(raw_segments, list): + return [] + return [dict(segment) for segment in raw_segments if isinstance(segment, dict)] + + +def _parse_segments(raw_segments: object) -> list[dict[str, object]] | None: + if not isinstance(raw_segments, list) or not raw_segments: + return None + parsed: list[dict[str, object]] = [] + for item in raw_segments: + if not isinstance(item, dict) or not isinstance(item.get("label"), str): + return None + start = _number(item.get("start")) + end = _number(item.get("end")) + if start is None or end is None or end <= start: + return None + parsed.append({"start": start, "end": end, "label": item["label"].strip().lower()}) + parsed.sort(key=lambda segment: (float(segment["start"]), float(segment["end"]))) + if any( + abs(float(right["start"]) - float(left["end"])) > _BOUNDARY_TOLERANCE_SECONDS + for left, right in zip(parsed, parsed[1:], strict=False) + ): + return None + return parsed + + +def _evidence_arrays( + activations: object, + embeddings: object, + label_names: Sequence[str], +) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None: + if not isinstance(activations, dict): + return None + try: + boundary = np.asarray(activations.get("segment"), dtype=float) + label_probabilities = np.asarray(activations.get("label"), dtype=float) + embedding_values = np.asarray(embeddings, dtype=float) + except (TypeError, ValueError): + return None + if boundary.ndim != 1 or boundary.size < 2: + return None + if label_probabilities.shape != (len(label_names), boundary.size): + return None + if embedding_values.ndim == 4: + embedding_values = embedding_values.mean(axis=-1) + if ( + embedding_values.ndim != 3 + or embedding_values.shape[1] != boundary.size + or embedding_values.shape[0] < 1 + or embedding_values.shape[2] < 1 + ): + return None + if not ( + np.isfinite(boundary).all() + and np.isfinite(label_probabilities).all() + and np.isfinite(embedding_values).all() + ): + return None + frame_features = np.transpose(embedding_values, (1, 0, 2)).reshape(boundary.size, -1) + center = np.median(frame_features, axis=0, keepdims=True) + centered = frame_features - center + scale = np.median(np.abs(centered), axis=0, keepdims=True) + return boundary, label_probabilities, centered / np.maximum(scale, 1e-6) + + +def _trusted_beats(beat_grid: object) -> list[float]: + if not isinstance(beat_grid, dict): + return [] + confidence = _number(beat_grid.get("confidence")) + raw_beats = beat_grid.get("beats") + if confidence is None or confidence < SECTION_REFINEMENT_GRID_MIN_CONFIDENCE: + return [] + if not isinstance(raw_beats, list): + return [] + beats: list[float] = [] + for value in raw_beats: + beat = _number(value) + if beat is None or (beats and beat <= beats[-1]): + return [] + beats.append(beat) + return beats + + +def _nearest_beat(value: float, beats: list[float]) -> float | None: + if not beats: + return None + index = bisect.bisect_left(beats, value) + choices = beats[max(0, index - 1) : min(len(beats), index + 1)] + if not choices: + return None + nearest = min(choices, key=lambda beat: abs(beat - value)) + return nearest if abs(nearest - value) <= SECTION_REFINEMENT_BEAT_SNAP_SECONDS else None + + +def _embedding_novelty(features: np.ndarray, frame: int, fps: float) -> float: + window = max(1, round(SECTION_REFINEMENT_NOVELTY_WINDOW_SECONDS * fps)) + if frame - window < 0 or frame + window > len(features): + return 0.0 + left = features[frame - window : frame].mean(axis=0) + right = features[frame : frame + window].mean(axis=0) + left_norm = float(np.linalg.norm(left)) + right_norm = float(np.linalg.norm(right)) + if left_norm <= 1e-9 or right_norm <= 1e-9: + return 0.0 + similarity = float(np.dot(left, right) / (left_norm * right_norm)) + return 1.0 - max(-1.0, min(1.0, similarity)) + + +def _local_peak_indices(boundary: np.ndarray) -> list[int]: + if len(boundary) < 3: + return [] + peaks = np.flatnonzero( + (boundary[1:-1] > boundary[:-2]) + & (boundary[1:-1] >= boundary[2:]) + & (boundary[1:-1] >= SECTION_REFINEMENT_MIN_ACTIVATION) + ) + return [int(index + 1) for index in peaks] + + +def _span_index(spans: list[dict[str, object]], midpoint: float) -> int: + for index, span in enumerate(spans): + if float(span["start"]) <= midpoint < float(span["end"]): + return index + return len(spans) - 1 + + +def _is_bracket(spans: list[dict[str, object]], index: int) -> bool: + """Is this span a non-musical marker rather than a section? + + ``start`` and ``end`` are ordinary classes in the label set, and the model + does assign them mid-song: one real track predicted a 34-second ``start`` + at 74 s and a 32-second ``end`` at 245 s. Only a sentinel at an extreme of + the timeline is actually bracketing it. Anywhere else it is just a class + the semantic head chose, over real music that still deserves a boundary + search and a real label. + """ + return str(spans[index]["label"]) in _SENTINELS and index in (0, len(spans) - 1) + + +def _original_label(spans: list[dict[str, object]], midpoint: float) -> str: + return str(spans[_span_index(spans, midpoint)]["label"]) + + +def _mean_label( + probabilities: np.ndarray, + label_names: Sequence[str], + start: float, + end: float, + fps: float, +) -> tuple[str, float]: + lo = max(0, min(probabilities.shape[1] - 1, round(start * fps))) + hi = max(lo + 1, min(probabilities.shape[1], round(end * fps))) + mean = probabilities[:, lo:hi].mean(axis=1) + eligible = [index for index, name in enumerate(label_names) if name not in _SENTINELS] + if not eligible: + return _NEUTRAL_LABEL, 0.0 + ranked = sorted(eligible, key=lambda index: float(mean[index]), reverse=True) + best = ranked[0] + second = ranked[1] if len(ranked) > 1 else best + margin = float(mean[best] - mean[second]) if second != best else float(mean[best]) + label = str(label_names[best]).strip().lower() + return (label if margin >= SECTION_REFINEMENT_MIN_LABEL_MARGIN else _NEUTRAL_LABEL), margin + + +def _segment_vector(features: np.ndarray, start: float, end: float, fps: float) -> np.ndarray: + lo = max(0, min(len(features) - 1, round(start * fps))) + hi = max(lo + 1, min(len(features), round(end * fps))) + vector = features[lo:hi].mean(axis=0) + norm = float(np.linalg.norm(vector)) + return vector / norm if norm > 1e-9 else np.zeros_like(vector) + + +def _regularize_neutral_labels( + records: list[dict[str, object]], + features: np.ndarray, + fps: float, +) -> None: + vectors = [ + _segment_vector(features, float(record["start"]), float(record["end"]), fps) + for record in records + ] + for index, record in enumerate(records): + if record["label"] != _NEUTRAL_LABEL: + continue + matches: list[tuple[float, int]] = [] + duration = float(record["end"]) - float(record["start"]) + for other_index, other in enumerate(records): + if abs(other_index - index) <= 1 or other["label"] in _SENTINELS | {_NEUTRAL_LABEL}: + continue + if float(other["margin"]) < SECTION_REFINEMENT_RECURRENCE_LABEL_MARGIN: + continue + other_duration = float(other["end"]) - float(other["start"]) + if other_duration < duration / 2 or other_duration > duration * 2: + continue + similarity = float(np.dot(vectors[index], vectors[other_index])) + if similarity >= SECTION_REFINEMENT_RECURRENCE_SIMILARITY: + matches.append((similarity, other_index)) + matches.sort(reverse=True) + if not matches: + continue + if len(matches) > 1 and matches[0][0] - matches[1][0] < 0.05: + continue + record["label"] = records[matches[0][1]]["label"] + + +def refine_segments( + raw_segments: object, + activations: object, + embeddings: object, + activation_fps: object, + beat_grid: object, + label_names: Sequence[str], +) -> list[dict[str, object]]: + """Return compact refined model segments, or the untouched upstream list.""" + fallback = _fallback(raw_segments) + spans = _parse_segments(raw_segments) + fps = _number(activation_fps) + evidence = _evidence_arrays(activations, embeddings, label_names) + if spans is None or fps is None or fps <= 0 or evidence is None: + return fallback + boundary_activation, label_probabilities, features = evidence + duration = len(boundary_activation) / fps + beats = _trusted_beats(beat_grid) + + boundaries = [float(spans[0]["start"])] + boundaries.extend( + (float(left["end"]) + float(right["start"])) / 2 + for left, right in zip(spans, spans[1:], strict=False) + ) + boundaries.append(float(spans[-1]["end"])) + + if beats: + boundaries = [ + value if index in (0, len(boundaries) - 1) else (_nearest_beat(value, beats) or value) + for index, value in enumerate(boundaries) + ] + + # A beat grid aligns accepted candidates; it never decides whether one is + # accepted. Gating discovery on a trustworthy grid silently disabled + # refinement for rubato, live, and free-time material, which is exactly the + # material whose upstream spans most need splitting. + for frame in sorted( + _local_peak_indices(boundary_activation), + key=lambda index: float(boundary_activation[index]), + reverse=True, + ): + candidate = frame / fps + snapped = _nearest_beat(candidate, beats) if beats else None + position = candidate if snapped is None else snapped + if _is_bracket(spans, _span_index(spans, candidate)): + continue + if ( + min(abs(position - boundary) for boundary in boundaries) + < SECTION_REFINEMENT_MIN_SEGMENT_SECONDS + ): + continue + if ( + position < SECTION_REFINEMENT_MIN_SEGMENT_SECONDS + or duration - position < SECTION_REFINEMENT_MIN_SEGMENT_SECONDS + ): + continue + if _embedding_novelty(features, frame, fps) < SECTION_REFINEMENT_MIN_NOVELTY: + continue + boundaries.append(position) + + boundaries = sorted(set(boundaries)) + if any(right - left <= 0 for left, right in zip(boundaries, boundaries[1:], strict=False)): + return fallback + + records: list[dict[str, object]] = [] + for start, end in zip(boundaries, boundaries[1:], strict=False): + midpoint = (start + end) / 2 + if _is_bracket(spans, _span_index(spans, midpoint)): + label, margin = _original_label(spans, midpoint), 1.0 + else: + label, margin = _mean_label(label_probabilities, label_names, start, end, fps) + records.append({"start": start, "end": end, "label": label, "margin": margin}) + + _regularize_neutral_labels(records, features, fps) + return [ + {"start": float(record["start"]), "end": float(record["end"]), "label": record["label"]} + for record in records + ] diff --git a/app/pipeline/section_worker.py b/app/pipeline/section_worker.py new file mode 100644 index 00000000..0d336317 --- /dev/null +++ b/app/pipeline/section_worker.py @@ -0,0 +1,128 @@ +"""Isolated All-In-One inference worker for automatic song sections.""" + +from __future__ import annotations + +import argparse +import contextlib +import json +import os +import sys +import threading +from pathlib import Path + +from app.pipeline.section_refine import refine_segments + +_HEARTBEAT_SECONDS = 10 + +# Hugging Face populates its cache with symlinks. Creating one on Windows needs +# either elevation or Developer Mode, and the resulting WinError 1314 is an +# OSError rather than the PermissionError the hub falls back on, so the +# download crashes instead of copying. StemDeck runs unelevated by design, so +# the checkpoints are copied unconditionally: they total about 10 MB, and a +# deterministic cache is worth more than the saved space. +os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS", "1") + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--stems-dir", type=Path, required=True) + parser.add_argument("--identifier", required=True) + parser.add_argument("--model", required=True) + parser.add_argument("--beat-grid", type=Path) + return parser + + +def _heartbeat(stop: threading.Event) -> None: + while not stop.wait(_HEARTBEAT_SECONDS): + print("SECTION_HEARTBEAT", file=sys.stderr, flush=True) + + +def _result_segments(result: object) -> list[dict[str, object]]: + if isinstance(result, list): + if len(result) != 1: + raise RuntimeError("section model returned an unexpected result count") + result = result[0] + segments = getattr(result, "segments", None) + if not isinstance(segments, list): + raise RuntimeError("section model returned no segments") + return [ + { + "start": float(segment.start), + "end": float(segment.end), + "label": str(segment.label), + } + for segment in segments + ] + + +def _load_beat_grid(path: Path | None) -> object | None: + if path is None or not path.is_file(): + return None + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + print("section refinement ignored an unreadable beat grid", file=sys.stderr, flush=True) + return None + + +def main(argv: list[str] | None = None) -> int: + args = _parser().parse_args(argv) + for path in (args.stems_dir / f"{name}.wav" for name in ("bass", "drums", "other", "vocals")): + if not path.is_file(): + raise FileNotFoundError("required section-analysis stem is missing") + + stop = threading.Event() + heartbeat = threading.Thread(target=_heartbeat, args=(stop,), daemon=True) + heartbeat.start() + try: + # Third-party diagnostics must stay off stdout. The parent accepts + # exactly one compact JSON line there so malformed output cannot be + # mistaken for section data. + with contextlib.redirect_stdout(sys.stderr): + import torch + from allin1_infer.config import HARMONIX_LABELS + from allin1_infer.helpers import run_inference + from allin1_infer.models import load_pretrained_model + from allin1_infer.spectrogram import extract_spectrograms + + spec_paths = extract_spectrograms( + [args.stems_dir], + args.stems_dir / "spec", + multiprocess=False, + ) + model = load_pretrained_model(model_name=args.model, device="cpu") + with torch.no_grad(): + result = run_inference( + path=Path(f"{args.identifier}.wav"), + spec_path=spec_paths[0], + model=model, + device="cpu", + include_activations=True, + include_embeddings=True, + ) + raw_segments = _result_segments(result) + try: + segments = refine_segments( + raw_segments, + getattr(result, "activations", None), + getattr(result, "embeddings", None), + getattr(result, "activation_fps", None), + _load_beat_grid(args.beat_grid), + HARMONIX_LABELS, + ) + except Exception as exc: + print( + f"section refinement fell back after {type(exc).__name__}", + file=sys.stderr, + flush=True, + ) + segments = raw_segments + print(json.dumps({"segments": segments}, separators=(",", ":")), flush=True) + return 0 + finally: + stop.set() + heartbeat.join(timeout=2) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/app/pipeline/sections.py b/app/pipeline/sections.py new file mode 100644 index 00000000..8d7d1672 --- /dev/null +++ b/app/pipeline/sections.py @@ -0,0 +1,391 @@ +"""Automatic functional song-section analysis. + +The semantic model runs after Demucs and consumes four stems. StemDeck uses +the six-stem model, so guitar, piano, and other are summed into a temporary +float WAV before an isolated worker performs inference. Every external result +is treated as untrusted data and normalized into the existing editable +Sections schema. +""" + +from __future__ import annotations + +import json +import logging +import math +import os +import shutil +import subprocess +import sys +import tempfile +import threading +import time +from collections import deque +from numbers import Real +from pathlib import Path + +from app.core.config import ( + SECTION_MODEL, + TIMEOUT_SECTIONS, + TIMEOUT_SECTIONS_STALL, + ffmpeg_executable, +) +from app.core.models import Job, JobCancelled +from app.core.registry import set_proc + +logger = logging.getLogger("stemdeck.sections") + +_KINDS = frozenset(("intro", "outro", "break", "bridge", "inst", "solo", "verse", "chorus", "part")) +_SENTINELS = frozenset(("start", "end")) +# What an interior sentinel becomes: the model named a real span with a bracket +# class, which says only that it could not name it musically. +_NEUTRAL_KIND = "part" +_NAMES = { + "intro": "Intro", + "outro": "Outro", + "break": "Break", + "bridge": "Bridge", + "inst": "Instrumental", + "solo": "Solo", + "verse": "Verse", + "chorus": "Chorus", + "part": "Part", +} +_COLORS = { + "intro": "#4a7fff", + "verse": "#00c8a0", + "chorus": "#9a4aff", + "bridge": "#ff8a20", + "break": "#2ab8e8", + "inst": "#e8c840", + "solo": "#ff4a90", + "outro": "#00d4d4", + "part": "#8391a5", +} +_MIN_SECTION_SECONDS = 0.5 +_BOUNDARY_TOLERANCE_SECONDS = 0.25 +_WORK_PREFIX = ".sections-work-" +_HEARTBEAT_PREFIX = "SECTION_HEARTBEAT" + + +def _number(value: object) -> float | None: + if isinstance(value, bool) or not isinstance(value, Real): + return None + result = float(value) + return result if math.isfinite(result) else None + + +def _raw_segments(raw: object) -> list[object] | None: + if isinstance(raw, dict): + raw = raw.get("segments") + return raw if isinstance(raw, list) else None + + +def _merge_short(segments: list[dict[str, object]]) -> list[dict[str, object]]: + result = list(segments) + while True: + short_index = next( + ( + i + for i, segment in enumerate(result) + if float(segment["end"]) - float(segment["start"]) < _MIN_SECTION_SECONDS + ), + None, + ) + if short_index is None: + return result + if len(result) <= 2: + return [] + + i = short_index + if 0 < i < len(result) - 1 and result[i - 1]["kind"] == result[i + 1]["kind"]: + result[i - 1]["end"] = result[i + 1]["end"] + del result[i : i + 2] + elif i > 0: + result[i - 1]["end"] = result[i]["end"] + del result[i] + else: + result[i + 1]["start"] = result[i]["start"] + del result[i] + + +def normalize_sections(raw_segments: object, duration: float) -> list[dict]: + """Convert untrusted model output into editable, gap-free section records.""" + duration_value = _number(duration) + raw = _raw_segments(raw_segments) + if duration_value is None or duration_value < 2 * _MIN_SECTION_SECONDS or not raw: + return [] + + parsed: list[dict[str, object]] = [] + for item in raw: + if not isinstance(item, dict): + return [] + label = item.get("label", item.get("kind")) + if not isinstance(label, str): + return [] + kind = label.strip().lower() + if kind not in _KINDS | _SENTINELS: + return [] + start = _number(item.get("start")) + end = _number(item.get("end")) + if start is None or end is None or end <= start: + return [] + start = max(0.0, min(duration_value, start)) + end = max(0.0, min(duration_value, end)) + if end <= start: + # Malformed input was already rejected above, so a span can only + # collapse here by lying entirely outside the analyzed duration. + # The model reads the stems while duration_sec is rounded, so its + # timeline routinely overhangs by a fraction of a second and a real + # track ended with a 10 ms span past it. That is one span with no + # overlap to keep, not a reason to discard the whole song. + continue + parsed.append({"start": start, "end": end, "kind": kind}) + + parsed.sort(key=lambda segment: (float(segment["start"]), float(segment["end"]))) + # The sentinels bracket the model's timeline and carry no musical meaning, + # so they are stripped from either end however many there are and whichever + # name they carry. The model does emit a degenerate one at the wrong end -- + # a real 484 s track produced a 10 ms "start" span *after* its final + # section -- and treating that as scrambled output threw away every section + # for the whole song. Only a sentinel sitting between two real sections + # means the timeline itself cannot be trusted. + lo, hi = 0, len(parsed) + while lo < hi and parsed[lo]["kind"] in _SENTINELS: + lo += 1 + while hi > lo and parsed[hi - 1]["kind"] in _SENTINELS: + hi -= 1 + for segment in parsed[lo:hi]: + if segment["kind"] in _SENTINELS: + segment["kind"] = _NEUTRAL_KIND + + # Normalize tiny floating-point disagreements at adjacent boundaries, but + # reject model output containing a real overlap or unlabeled internal gap. + for left, right in zip(parsed, parsed[1:], strict=False): + delta = float(right["start"]) - float(left["end"]) + if abs(delta) > _BOUNDARY_TOLERANCE_SECONDS: + return [] + boundary = (float(left["end"]) + float(right["start"])) / 2 + left["end"] = boundary + right["start"] = boundary + + meaningful = [segment for segment in parsed[lo:hi] if segment["kind"] in _KINDS] + if len(meaningful) < 2: + return [] + meaningful[0]["start"] = 0.0 + meaningful[-1]["end"] = duration_value + # Boundaries and semantic labels are separate model tasks. Adjacent spans + # with the same label still represent independently predicted structural + # boundaries and must remain editable instead of being collapsed. + meaningful = _merge_short(meaningful) + if len(meaningful) < 2: + return [] + + sections: list[dict] = [] + for index, segment in enumerate(meaningful, start=1): + kind = str(segment["kind"]) + start = round(float(segment["start"]), 3) + end = round(float(segment["end"]), 3) + if end - start < _MIN_SECTION_SECONDS: + return [] + sections.append( + { + "id": f"auto-{index:03d}", + "name": _NAMES[kind], + "kind": kind, + "start": start, + "end": end, + "color": _COLORS[kind], + } + ) + return sections + + +def _terminate(proc: subprocess.Popen) -> None: + if proc.poll() is not None: + return + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=5) + + +def _run_registered_process(job: Job, cmd: list[str]) -> tuple[int, list[str], list[str]]: + """Run a child with cancellation, total timeout, and output-stall detection.""" + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8:replace" + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + bufsize=1, + env=env, + ) + if proc.stdout is None or proc.stderr is None: + _terminate(proc) + raise RuntimeError("section-analysis process has no output pipes") + + stdout: deque[str] = deque(maxlen=20) + stderr: deque[str] = deque(maxlen=80) + last_output = [time.monotonic()] + output_lock = threading.Lock() + + def read_lines(stream, sink: deque[str]) -> None: + for line in stream: + with output_lock: + sink.append(line.rstrip()) + last_output[0] = time.monotonic() + + readers = [ + threading.Thread(target=read_lines, args=(proc.stdout, stdout), daemon=True), + threading.Thread(target=read_lines, args=(proc.stderr, stderr), daemon=True), + ] + for reader in readers: + reader.start() + + started = time.monotonic() + set_proc(job.id, proc) + try: + while proc.poll() is None: + if job.cancel_requested: + _terminate(proc) + raise JobCancelled() + now = time.monotonic() + if now - started > TIMEOUT_SECTIONS: + logger.warning("section analysis timed out for job %s", job.id) + _terminate(proc) + break + with output_lock: + silent_for = now - last_output[0] + if silent_for > TIMEOUT_SECTIONS_STALL: + logger.warning( + "section analysis stalled for %ss for job %s", + TIMEOUT_SECTIONS_STALL, + job.id, + ) + _terminate(proc) + break + time.sleep(0.1) + finally: + set_proc(job.id, None) + for reader in readers: + reader.join(timeout=2) + + if job.cancel_requested: + raise JobCancelled() + return proc.returncode or 0, list(stdout), list(stderr) + + +def _mix_other_stems(job: Job, stems_dir: Path, work_dir: Path) -> Path | None: + inputs = [stems_dir / f"{name}.wav" for name in ("other", "guitar", "piano")] + if not all(path.is_file() for path in inputs): + return None + output = work_dir / "other.wav" + cmd = [ffmpeg_executable(), "-y", "-nostdin", "-loglevel", "error"] + for path in inputs: + cmd += ["-i", str(path)] + cmd += [ + "-filter_complex", + "[0:a][1:a][2:a]amix=inputs=3:normalize=0:duration=longest", + "-c:a", + "pcm_f32le", + str(output), + ] + returncode, _stdout, stderr = _run_registered_process(job, cmd) + if returncode != 0 or not output.is_file() or output.stat().st_size == 0: + detail = " | ".join(stderr[-3:]) or "no diagnostic output" + logger.warning("could not prepare section stems for job %s: %s", job.id, detail) + output.unlink(missing_ok=True) + return None + return output + + +def _run_worker(job: Job, work_dir: Path) -> object | None: + cmd = [ + sys.executable, + "-m", + "app.pipeline.section_worker", + "--stems-dir", + str(work_dir), + "--identifier", + job.id, + "--model", + SECTION_MODEL, + ] + beat_grid = work_dir / "beats.json" + if beat_grid.is_file(): + cmd += ["--beat-grid", str(beat_grid)] + returncode, stdout, stderr = _run_registered_process(job, cmd) + diagnostics = [line for line in stderr if not line.startswith(_HEARTBEAT_PREFIX)] + if returncode != 0: + logger.warning( + "section model failed for job %s: %s", + job.id, + " | ".join(diagnostics[-5:]) or f"exit {returncode}", + ) + return None + if len(stdout) != 1: + logger.warning("section model returned unexpected output for job %s", job.id) + return None + try: + return json.loads(stdout[0]) + except json.JSONDecodeError: + logger.warning("section model returned invalid JSON for job %s", job.id) + return None + + +def _link_or_copy(source: Path, target: Path) -> None: + try: + os.link(source, target) + return + except OSError: + pass + try: + target.symlink_to(source.resolve()) + return + except OSError: + pass + shutil.copy2(source, target) + + +def _safe_rmtree(path: Path, parent: Path) -> None: + resolved = path.resolve() + if resolved.parent == parent.resolve() and resolved.name.startswith(_WORK_PREFIX): + shutil.rmtree(resolved, ignore_errors=True) + else: # pragma: no cover - construction is internal; guard prevents future widening + logger.error("refusing to remove invalid section workspace %s", resolved) + + +def detect_sections(job: Job, stems_dir: Path, duration: float) -> list[dict] | None: + """Return automatic section suggestions, or None when analysis is unavailable.""" + if job.cancel_requested: + raise JobCancelled() + required = [stems_dir / f"{name}.wav" for name in ("bass", "drums", "vocals")] + if not all(path.is_file() for path in required): + logger.info("section analysis skipped for job %s: required stems are missing", job.id) + return None + + work_dir = Path(tempfile.mkdtemp(prefix=_WORK_PREFIX, dir=stems_dir)).resolve() + try: + for name in ("bass", "drums", "vocals"): + _link_or_copy(stems_dir / f"{name}.wav", work_dir / f"{name}.wav") + beat_grid = stems_dir / "beats.json" + if beat_grid.is_file(): + _link_or_copy(beat_grid, work_dir / "beats.json") + other_path = _mix_other_stems(job, stems_dir, work_dir) + if other_path is None: + return None + raw = _run_worker(job, work_dir) + if raw is None: + return None + normalized = normalize_sections(raw, duration) + if not normalized: + logger.info("section model produced no valid structure for job %s", job.id) + return None + return normalized + finally: + _safe_rmtree(work_dir, stems_dir) diff --git a/app/pipeline/warmup.py b/app/pipeline/warmup.py index 571108ba..21385072 100644 --- a/app/pipeline/warmup.py +++ b/app/pipeline/warmup.py @@ -1,9 +1,9 @@ """Eager model pre-download for the desktop first-boot setup wizard (#275). -Run as `python -m app.pipeline.warmup`. Downloads/caches the three ML -checkpoints StemDeck uses -- Demucs (htdemucs_6s), beat-this, and the -on-demand lead/backing vocal-split karaoke model -- so a user's first real -job doesn't pay for any of them mid-pipeline. Invoked by the Tauri +Run as `python -m app.pipeline.warmup`. Downloads/caches the four ML +checkpoint families StemDeck uses: Demucs (htdemucs_6s), beat-this, +All-In-One song sections, and the on-demand lead/backing vocal-split karaoke +model. This keeps a user's first real job from paying for them mid-pipeline. Invoked by the Tauri `warmup_models` command (desktop/src-tauri/src/main.rs) as one of the setup steps; Docker has no equivalent step and keeps the pre-existing lazy-download-on-first-use behavior (see docs/models.md). @@ -18,9 +18,16 @@ from __future__ import annotations +import os import sys -from app.core.config import BEAT_MODEL_CHECKPOINT, DEMUCS_MODEL, MODELS_DIR, VOCAL_SPLIT_MODEL +from app.core.config import ( + BEAT_MODEL_CHECKPOINT, + DEMUCS_MODEL, + MODELS_DIR, + SECTION_MODEL, + VOCAL_SPLIT_MODEL, +) def _warm_demucs() -> None: @@ -45,9 +52,23 @@ def _warm_vocal_split() -> None: separator.load_model(model_filename=VOCAL_SPLIT_MODEL) +def _warm_sections() -> None: + # Matches app/pipeline/section_worker.py: unelevated Windows cannot create + # the symlinks the Hugging Face cache wants, and the WinError 1314 that + # results escapes the hub's own PermissionError fallback. Both entry points + # that download this model must opt out, or setup fails where a real job + # would have succeeded (and vice versa). + os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS", "1") + + from allin1_infer.models import load_pretrained_model + + load_pretrained_model(model_name=SECTION_MODEL, device="cpu") + + _STEPS = ( ("demucs", _warm_demucs), ("beat_this", _warm_beat_this), + ("sections", _warm_sections), ("vocal_split", _warm_vocal_split), ) diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index b6f918f9..df2841fb 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -1381,11 +1381,12 @@ fn ensure_external_assets() -> Result { struct ModelWarmupStatus { demucs_ready: bool, beat_this_ready: bool, + sections_ready: bool, vocal_split_ready: bool, } /// Eagerly downloads/caches the ML models StemDeck uses (Demucs, beat-this, -/// and the on-demand lead/backing vocal-split karaoke model, #275) via +/// automatic song sections, and the on-demand lead/backing vocal-split karaoke model, #275) via /// `app/pipeline/warmup.py`, so a user's first real job doesn't pay for any /// of them mid-pipeline. Best-effort per model: a single model failing to /// download (e.g. no network) does not fail this command — the setup wizard @@ -1431,12 +1432,14 @@ fn warmup_models(state: tauri::State) -> Result status.demucs_ready = true, "WARMUP_OK beat_this" => status.beat_this_ready = true, + "WARMUP_OK sections" => status.sections_ready = true, "WARMUP_OK vocal_split" => status.vocal_split_ready = true, _ if line.starts_with("WARMUP_FAILED") => { append_to_setup_log(&data_dir, &format!("model warmup: {line}")); diff --git a/docs/models.md b/docs/models.md index 1d1915a3..317e6d27 100644 --- a/docs/models.md +++ b/docs/models.md @@ -9,6 +9,20 @@ license file are documented here. MIT, published by the `demucs` PyPI package (Meta/Facebook Research). No audit needed -- an unambiguous upstream license. +## All-In-One (automatic song sections) + +- **Runtime**: `all-in-one-infer` 3.x, the cross-platform inference fork of + the All-In-One music-structure model. +- **Checkpoint**: `harmonix-all`, downloaded from the upstream Hugging Face + repository during desktop warmup or on first use elsewhere. +- **License**: MIT for both the original All-In-One project and the + `all-in-one-infer` runtime. +- **Upstream**: https://github.com/mir-aidj/all-in-one and + https://github.com/openmirlab/all-in-one-infer + +StemDeck runs this model on CPU after separation and passes its existing stems. +The checkpoint is not bundled in StemDeck installers. + ## UVR-MDX-NET Karaoke 2 (on-demand lead/backing vocal split, #275) - **File**: `UVR_MDXNET_KARA_2.onnx` diff --git a/pyproject.toml b/pyproject.toml index 30b8ee1a..ad60d5e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,12 @@ dependencies = [ # librosa upgrade silently break the vocal-split feature (#407). Pin # <1 to match what audio-separator expects and what uv.lock resolves. "librosa>=0.10,<1", + # Functional song-section boundaries and semantic labels (intro, verse, + # chorus, bridge, etc.). This maintained inference-only package preserves + # the upstream All-In-One model while replacing NATTEN's compiled extension + # with a cross-platform pure-PyTorch implementation. StemDeck supplies its + # already-separated stems, so the package's Demucs path is never invoked. + "all-in-one-infer>=3.1,<4", # Beat/downbeat tracker for the click track. MIT for both code and the # published weights. librosa's tracker resolves fast music to half tempo # (180 BPM punk -> 90) because of its 120 BPM lognormal prior, which is not diff --git a/static/css/daw.css b/static/css/daw.css index 5ade5561..743177c9 100644 --- a/static/css/daw.css +++ b/static/css/daw.css @@ -234,6 +234,10 @@ input, textarea { font-family: inherit; } opacity: 0.85; } .stem-choice[aria-pressed="false"] .stem-dot { opacity: 0.4; } +/* Automatic song-structure detection. Not a stem and not the primary action, + so it takes the section timeline's blue rather than the amber the Split + button owns -- two amber chips side by side read as two competing CTAs. */ +.structure-toggle { --color: #4a7fff; margin-left: 10px; } /* "All" toggle */ .stem-choice-all { @@ -1453,6 +1457,19 @@ input, textarea { font-family: inherit; } .daw-label-title { font-size: 12px; font-weight: 600; } .daw-label-sub { font-size: 9px; } .daw-sections-header { justify-content: space-between; } +.daw-sections-title-group { display: inline-flex; align-items: center; gap: 7px; min-width: 0; } +.sections-suggested-badge { + padding: 2px 5px; + border: 1px solid rgba(74,127,255,0.4); + border-radius: 4px; + color: #7fa2ff; + font-size: 8px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + white-space: nowrap; +} +.sections-suggested-badge.hidden { display: none; } .daw-sections-area { flex: 1; position: relative; diff --git a/static/index.html b/static/index.html index c89b30bb..a155f3c2 100644 --- a/static/index.html +++ b/static/index.html @@ -102,6 +102,14 @@ + + +
`; @@ -106,6 +117,29 @@ function _makeSectionEl(section) { return el; } +export function sectionDisplayName(section, all) { + const kind = String(section?.kind || "").toLowerCase(); + if (!SECTION_KINDS.has(kind)) return String(section?.name || ""); + const name = t(`sections.kind.${kind}`); + // The model predicts boundaries and labels with separate heads, so two + // neighbouring spans can share a kind and still be a real structural change + // (chorus one and chorus two). Merging them was tried and silently discarded + // five true boundaries on the reference track, so they are numbered instead: + // the boundary survives and "Chorus Chorus" stops reading as a bug. + if (!Array.isArray(all)) return name; + const ordered = [...all].sort((a2, b2) => a2.start - b2.start); + const peers = ordered.filter((s) => String(s?.kind || "").toLowerCase() === kind); + if (peers.length < 2) return name; + const position = peers.findIndex((s) => s.id === section.id); + if (position < 0) return name; + return t("sections.kindNumbered", { kind: name, n: position + 1 }); +} + +function _updateSuggestedBadge() { + const badge = document.getElementById("sectionsSuggested"); + if (badge) badge.classList.toggle("hidden", _sectionsSource !== "automatic" || !_sections.length); +} + function _esc(str) { return String(str) .replace(/&/g, "&") @@ -120,12 +154,16 @@ function _wireDrag(el, section) { let active = false; let startX = 0; let origStart = 0; + let origEnd = 0; + let changed = false; el.addEventListener("pointerdown", (e) => { if (e.target.closest(".section-handle,.section-del")) return; active = true; startX = e.clientX; origStart = section.start; + origEnd = section.end; + changed = false; el.setPointerCapture(e.pointerId); el.classList.add("sec-dragging"); e.preventDefault(); @@ -137,8 +175,11 @@ function _wireDrag(el, section) { if (!cw) return; const dt = ((e.clientX - startX) / cw) * _duration; const w = section.end - section.start; - section.start = _clampMove(section.id, origStart + dt, w); - section.end = section.start + w; + const nextStart = _clampMove(section.id, origStart + dt, w); + const nextEnd = nextStart + w; + changed ||= _timesChanged(origStart, origEnd, nextStart, nextEnd); + section.start = nextStart; + section.end = nextEnd; el.style.left = `${(section.start / _duration) * 100}%`; }); @@ -146,10 +187,15 @@ function _wireDrag(el, section) { if (!active) return; active = false; el.classList.remove("sec-dragging"); - _scheduleSave(); + if (changed) _scheduleSave(); }); el.addEventListener("pointercancel", () => { + if (active) { + section.start = origStart; + section.end = origEnd; + _render(); + } active = false; el.classList.remove("sec-dragging"); }); @@ -162,11 +208,17 @@ function _wireResize(handle, el, section) { let active = false; let startX = 0; let origTime = 0; + let origStart = 0; + let origEnd = 0; + let changed = false; handle.addEventListener("pointerdown", (e) => { active = true; startX = e.clientX; origTime = edge === "left" ? section.start : section.end; + origStart = section.start; + origEnd = section.end; + changed = false; handle.setPointerCapture(e.pointerId); el.classList.add("sec-resizing"); e.preventDefault(); @@ -192,6 +244,7 @@ function _wireResize(handle, el, section) { const ps = (section.start / _duration) * 100; const pw = ((section.end - section.start) / _duration) * 100; + changed ||= _timesChanged(origStart, origEnd, section.start, section.end); el.style.left = `${ps}%`; el.style.width = `${pw}%`; }); @@ -200,10 +253,15 @@ function _wireResize(handle, el, section) { if (!active) return; active = false; el.classList.remove("sec-resizing"); - _scheduleSave(); + if (changed) _scheduleSave(); }); handle.addEventListener("pointercancel", () => { + if (active) { + section.start = origStart; + section.end = origEnd; + _render(); + } active = false; el.classList.remove("sec-resizing"); }); @@ -296,7 +354,8 @@ function _openRename(id, labelEl) { const input = document.createElement("input"); input.className = "section-rename-input"; input.type = "text"; - input.value = section.name; + const originalName = sectionDisplayName(section, _sections); + input.value = originalName; input.style.setProperty("--sc", section.color); labelEl.replaceWith(input); input.focus(); @@ -304,14 +363,23 @@ function _openRename(id, labelEl) { const commit = () => { const n = input.value.trim(); - if (n) section.name = n; + if (n && n !== originalName) { + section.name = n; + delete section.kind; + _render(); + _scheduleSave(); + return; + } _render(); - _scheduleSave(); }; input.addEventListener("blur", commit, { once: true }); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); input.blur(); } - if (e.key === "Escape") { input.value = section.name; input.removeEventListener("blur", commit); input.blur(); } + if (e.key === "Escape") { + e.preventDefault(); + input.removeEventListener("blur", commit); + _render(); + } }); } @@ -346,13 +414,29 @@ function _hideSaveIndicator() { function _scheduleSave() { clearTimeout(_saveTimer); _showSaving(); - _saveTimer = setTimeout(_save, 600); + _saveTimer = setTimeout(() => { + _saveTimer = null; + _queueSaveSnapshot(); + }, 600); +} + +export function flushSectionsSave() { + if (_saveTimer !== null) { + clearTimeout(_saveTimer); + _saveTimer = null; + } + return _queueSaveSnapshot(); } -async function _save() { - if (!_trackId) return; +function _queueSaveSnapshot() { + if (!_trackId) return _saveChain; const id = _trackId; const body = JSON.stringify({ sections: _sections }); + _saveChain = _saveChain.then(() => _sendSave(id, body)); + return _saveChain; +} + +async function _sendSave(id, body) { try { const res = await fetch(`/api/jobs/${id}/sections`, { method: "PATCH", @@ -365,13 +449,21 @@ async function _save() { if (id === _trackId) _hideSaveIndicator(); return; } - if (id === _trackId) _showSaved(); + if (id === _trackId) { + _sectionsSource = "manual"; + _updateSuggestedBadge(); + if (body === JSON.stringify({ sections: _sections }) && _saveTimer === null) _showSaved(); + } } catch (e) { console.warn("[sections] save failed:", e); if (id === _trackId) _hideSaveIndicator(); } } +function _timesChanged(beforeStart, beforeEnd, afterStart, afterEnd) { + return Math.abs(beforeStart - afterStart) > 1e-6 || Math.abs(beforeEnd - afterEnd) > 1e-6; +} + // ─── Utilities ──────────────────────────────────────────── function _nextColor() { diff --git a/tests/conftest.py b/tests/conftest.py index 219fb609..d6fa7048 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -61,6 +61,10 @@ def _isolate_network_settings(tmp_path, monkeypatch): # (whose client host isn't loopback). Default the suite ON; gate tests set # it explicitly. Tests checking the real default clear this env var. monkeypatch.setenv("STEMDECK_ALLOW_NETWORK", "1") + # Song-structure extraction is env-seeded too. A developer who exported + # STEMDECK_AUTO_SECTIONS=0 in their shell must not change what the pipeline + # tests assert; the tests that care set it themselves. + monkeypatch.delenv("STEMDECK_AUTO_SECTIONS", raising=False) _settings._state = None # force a fresh load from the isolated path yield _settings._state = None diff --git a/tests/js/sections.test.mjs b/tests/js/sections.test.mjs new file mode 100644 index 00000000..cd0ff03c --- /dev/null +++ b/tests/js/sections.test.mjs @@ -0,0 +1,145 @@ +import { LANGUAGES, TRANSLATIONS } from "../../static/js/i18n.js"; +import { + destroySections, + flushSectionsSave, + initSections, + sectionDisplayName, +} from "../../static/js/sections.js"; + +let pass = 0; +let fail = 0; +const check = (name, condition) => { + if (condition) { + pass++; + console.log(`PASS ${name}`); + } else { + fail++; + console.log(`FAIL ${name}`); + } +}; + +const sectionKeys = [ + "sections.suggested", + "sections.kind.intro", + "sections.kind.outro", + "sections.kind.break", + "sections.kind.bridge", + "sections.kind.inst", + "sections.kind.solo", + "sections.kind.verse", + "sections.kind.chorus", + "sections.kind.part", + "sections.kindNumbered", +]; + +for (const { code } of LANGUAGES) { + const table = code === "pt-PT" ? TRANSLATIONS.pt : TRANSLATIONS[code]; + check(`${code} has every automatic-section label`, sectionKeys.every((key) => table[key])); +} +check( + "English automatic-section badge explains that adjustment is experimental", + TRANSLATIONS.en["sections.suggested"] === "Experimental - drag to adjust.", +); + +check( + "canonical kinds use translated display labels", + sectionDisplayName({ kind: "chorus", name: "model-label" }) === "Chorus", +); +check( + "custom names remain unchanged", + sectionDisplayName({ name: "Pre-Chorus" }) === "Pre-Chorus", +); + +// The model can label two adjacent spans with one kind and still be marking a +// real structural change, so the boundary is kept and the labels are numbered. +const repeated = [ + { id: "a", kind: "chorus", start: 0, end: 10 }, + { id: "b", kind: "verse", start: 10, end: 20 }, + { id: "c", kind: "chorus", start: 20, end: 30 }, +]; +check( + "a repeated kind is numbered in running order", + sectionDisplayName(repeated[0], repeated) === "Chorus 1" && + sectionDisplayName(repeated[2], repeated) === "Chorus 2", +); +check( + "a kind used once is never numbered", + sectionDisplayName(repeated[1], repeated) === "Verse", +); +check( + "numbering follows time, not list order", + sectionDisplayName(repeated[2], [repeated[2], repeated[1], repeated[0]]) === "Chorus 2", +); +check( + "a renamed section keeps its own name even beside repeated kinds", + sectionDisplayName({ id: "d", name: "Pre-Chorus", start: 5, end: 6 }, repeated) === "Pre-Chorus", +); +check( + "every language orders the numbered label around its own kind word", + LANGUAGES.every(({ code }) => { + const table = code === "pt-PT" ? TRANSLATIONS.pt : TRANSLATIONS[code]; + const value = table["sections.kindNumbered"]; + return value.includes("{kind}") && value.includes("{n}"); + }), +); + +const badge = { + hidden: true, + classList: { + toggle(_name, force) { + badge.hidden = force; + }, + }, +}; +globalThis.document = { + getElementById(id) { + return id === "sectionsSuggested" ? badge : null; + }, +}; + +initSections( + "abcdefabcdef", + [{ id: "auto-001", kind: "verse", name: "Verse", start: 0, end: 10, color: "#fff" }], + 10, + "automatic", +); +check("automatic sections show the experimental adjustment badge", badge.hidden === false); +destroySections(); +check("destroying sections hides the experimental adjustment badge", badge.hidden === true); + +const requests = []; +const complete = []; +globalThis.fetch = (_url, options) => new Promise((resolve) => { + requests.push(JSON.parse(options.body)); + complete.push(resolve); +}); + +initSections( + "abcdefabcdef", + [{ id: "auto-001", kind: "intro", name: "Intro", start: 0, end: 10, color: "#fff" }], + 20, + "automatic", +); +const firstSave = flushSectionsSave(); +await Promise.resolve(); +initSections( + "abcdefabcdef", + [{ id: "auto-002", kind: "verse", name: "Verse", start: 10, end: 20, color: "#fff" }], + 20, + "automatic", +); +const secondSave = flushSectionsSave(); +await Promise.resolve(); +check("a second section save waits for the first", requests.length === 1); +complete[0]({ ok: true }); +await firstSave; +await Promise.resolve(); +check("the newest snapshot starts after the first completes", requests.length === 2); +check("the first queued snapshot is preserved", requests[0].sections[0].id === "auto-001"); +check("the second queued snapshot is preserved", requests[1].sections[0].id === "auto-002"); +complete[1]({ ok: true }); +await secondSave; +destroySections(); + +console.log(`\n${pass} passed, ${fail} failed`); +process.exit(fail ? 1 : 0); diff --git a/tests/test_jobs_api.py b/tests/test_jobs_api.py index 7ad59e24..16a45679 100644 --- a/tests/test_jobs_api.py +++ b/tests/test_jobs_api.py @@ -293,7 +293,16 @@ def done_job(client, tmp_path, monkeypatch): def test_sections_happy_path(client, done_job, tmp_path): payload = { - "sections": [{"id": "sec1", "name": "Verse", "start": 0.0, "end": 30.0, "color": "#ff0000"}] + "sections": [ + { + "id": "sec1", + "name": "Verse", + "kind": "verse", + "start": 0.0, + "end": 30.0, + "color": "#ff0000", + } + ] } r = client.patch(f"/api/jobs/{done_job.id}/sections", json=payload) assert r.status_code == 200 @@ -301,11 +310,34 @@ def test_sections_happy_path(client, done_job, tmp_path): assert body["job_id"] == done_job.id assert len(body["sections"]) == 1 assert body["sections"][0]["name"] == "Verse" + assert body["sections_source"] == "manual" + assert done_job.sections_source == "manual" # Verify written to disk meta_path = tmp_path / done_job.id / "metadata.json" assert meta_path.is_file() meta = json.loads(meta_path.read_text()) assert meta["sections"][0]["id"] == "sec1" + assert meta["sections_source"] == "manual" + + +def test_sections_accepts_neutral_part_kind(client, done_job): + payload = { + "sections": [ + { + "id": "auto-005", + "name": "Part", + "kind": "part", + "start": 49.874, + "end": 65.901, + "color": "#8391a5", + } + ] + } + + response = client.patch(f"/api/jobs/{done_job.id}/sections", json=payload) + + assert response.status_code == 200 + assert response.json()["sections"][0]["kind"] == "part" def test_sections_unknown_job_returns_404(client): @@ -338,6 +370,56 @@ def test_sections_invalid_id_returns_422(client, done_job): assert r.status_code == 422 +def test_sections_invalid_kind_returns_422(client, done_job): + payload = { + "sections": [ + { + "id": "sec1", + "name": "Pre-chorus", + "kind": "prechorus", + "start": 0.0, + "end": 5.0, + "color": "#fff", + } + ] + } + r = client.patch(f"/api/jobs/{done_job.id}/sections", json=payload) + assert r.status_code == 422 + + +def test_sections_write_failure_does_not_mutate_live_job(client, done_job, monkeypatch): + import app.api.jobs as jobs_mod + + original = [{"id": "old", "name": "Old", "start": 0.0, "end": 5.0, "color": "#fff"}] + done_job.sections = original + done_job.sections_source = "automatic" + + def fail_write(*_args, **_kwargs): + raise OSError("disk full") + + monkeypatch.setattr(jobs_mod, "_write_json_atomic", fail_write) + response = client.patch( + f"/api/jobs/{done_job.id}/sections", + json={ + "sections": [{"id": "new", "name": "New", "start": 0.0, "end": 5.0, "color": "#000"}] + }, + ) + + assert response.status_code == 500 + assert done_job.sections == original + assert done_job.sections_source == "automatic" + + +def test_atomic_section_metadata_write_leaves_no_temporary_file(tmp_path): + from app.api.jobs import _write_json_atomic + + path = tmp_path / "metadata.json" + _write_json_atomic(path, {"sections_source": "manual"}) + + assert json.loads(path.read_text(encoding="utf-8"))["sections_source"] == "manual" + assert not list(tmp_path.glob(".metadata.json.*.tmp")) + + # ─── SSE job_id validation ──────────────────────────────────────────────────── diff --git a/tests/test_network_gate.py b/tests/test_network_gate.py index 7984d42a..75061ef0 100644 --- a/tests/test_network_gate.py +++ b/tests/test_network_gate.py @@ -232,3 +232,38 @@ def test_post_toggles_off_then_blocks(): # Now off → a non-loopback client is blocked from everything. with TestClient(app) as c: assert c.get("/api/settings").status_code == 403 + + +# ── auto_sections (experimental song-structure extraction) ── + + +def test_auto_sections_defaults_off(_isolated_settings): + """Experimental, and it costs an inference pass. Nobody pays by default.""" + assert settings_mod.get_auto_sections() is False + + +def test_auto_sections_env_can_turn_it_on(monkeypatch, _isolated_settings): + """A deployment that wants it from first boot opts in explicitly.""" + monkeypatch.setenv("STEMDECK_AUTO_SECTIONS", "1") + assert settings_mod.get_auto_sections() is True + # Anything else still means off, so a malformed value cannot silently + # enable a cost the user never asked for. + monkeypatch.setenv("STEMDECK_AUTO_SECTIONS", "yes please") + assert settings_mod.get_auto_sections() is False + + +def test_auto_sections_api_round_trip(_isolated_settings): + with TestClient(app) as c: + assert c.get("/api/settings").json()["auto_sections"] is False + r = c.post("/api/settings", json={"auto_sections": True}) + assert r.status_code == 200 + assert r.json()["auto_sections"] is True + assert c.get("/api/settings").json()["auto_sections"] is True + assert settings_mod.get_auto_sections() is True + + +def test_auto_sections_saved_choice_beats_the_env_default(monkeypatch, _isolated_settings): + """An explicit choice must survive an env var that says otherwise.""" + settings_mod.set_auto_sections(True) + monkeypatch.setenv("STEMDECK_AUTO_SECTIONS", "") + assert settings_mod.get_auto_sections() is True diff --git a/tests/test_pipeline_runner.py b/tests/test_pipeline_runner.py index d7fe32e7..525709ed 100644 --- a/tests/test_pipeline_runner.py +++ b/tests/test_pipeline_runner.py @@ -10,6 +10,8 @@ from app.pipeline.runner import ( _extract_video_track, _presence_from_rms, + _run_common, + _write_metadata, run_local_pipeline, run_pipeline, ) @@ -427,3 +429,184 @@ def test_presence_from_rms_empty_input(): def test_presence_from_rms_all_silent(): assert _presence_from_rms({"vocals": 0.0, "drums": 0.0}) == {"vocals": 0, "drums": 0} + + +def _common_stage_patches(job_dir: Path, sections): + section_patch = ( + patch("app.pipeline.runner.detect_sections", side_effect=sections) + if isinstance(sections, BaseException) + else patch("app.pipeline.runner.detect_sections", return_value=sections) + ) + return ( + patch("app.pipeline.runner.analyze"), + patch("app.pipeline.runner.separate", return_value=job_dir / "model"), + patch("app.pipeline.runner.collect", return_value=["bass", "drums", "vocals"]), + patch("app.pipeline.runner.cleanup_source"), + patch("app.pipeline.runner.make_original_track", return_value=None), + patch("app.pipeline.runner.make_selected_mix", return_value=None), + patch("app.pipeline.runner.compute_stem_peaks", return_value={}), + patch("app.pipeline.runner.compute_beat_grid"), + section_patch, + ) + + +def test_common_pipeline_stores_automatic_section_suggestions(tmp_path: Path): + job = Job(id="abcdefabc111", duration_sec=60.0) + job_dir = tmp_path / job.id + stems_dir = job_dir / "stems" + stems_dir.mkdir(parents=True) + suggested = [ + { + "id": "auto-001", + "name": "Verse", + "kind": "verse", + "start": 0.0, + "end": 60.0, + "color": "#00c8a0", + } + ] + + patches = _common_stage_patches(job_dir, suggested) + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + patches[5], + patches[6], + patches[7], + patches[8] as detect, + patch("app.pipeline.runner.get_auto_sections", return_value=True), + ): + _run_common(job, job_dir / "source.wav", job_dir) + + assert job.sections == suggested + assert job.sections_source == "automatic" + detect.assert_called_once_with(job, stems_dir, 60.0) + assert "sections" in (job.stage_timings or {}) + + +def test_common_pipeline_skips_sections_when_the_user_turned_them_off(tmp_path: Path): + """The toggle must stop the inference pass, not just hide its result. + + The setting is read per job rather than captured at import, so switching it + off applies to the next import without restarting the server. + """ + job = Job(id="abcdefabc116", duration_sec=60.0) + job_dir = tmp_path / job.id + (job_dir / "stems").mkdir(parents=True) + + patches = _common_stage_patches(job_dir, [{"id": "auto-001", "kind": "verse"}]) + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + patches[5], + patches[6], + patches[7], + patches[8] as detect, + patch("app.pipeline.runner.get_auto_sections", return_value=False), + ): + _run_common(job, job_dir / "source.wav", job_dir) + + detect.assert_not_called() + assert job.sections is None + assert job.sections_source is None + + +def test_common_pipeline_keeps_section_failure_nonfatal(tmp_path: Path, caplog): + job = Job(id="abcdefabc112", duration_sec=60.0) + job_dir = tmp_path / job.id + (job_dir / "stems").mkdir(parents=True) + patches = _common_stage_patches(job_dir, RuntimeError("model unavailable")) + + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + patches[5], + patches[6], + patches[7], + patches[8], + patch("app.pipeline.runner.get_auto_sections", return_value=True), + caplog.at_level("ERROR", logger="stemdeck.pipeline"), + ): + _run_common(job, job_dir / "source.wav", job_dir) + + assert job.sections is None + assert "section analysis stage failed" in caplog.text + + +def test_common_pipeline_preserves_section_cancellation(tmp_path: Path): + job = Job(id="abcdefabc113", duration_sec=60.0) + job_dir = tmp_path / job.id + (job_dir / "stems").mkdir(parents=True) + patches = _common_stage_patches(job_dir, JobCancelled()) + + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + patches[5], + patches[6], + patches[7], + patches[8], + patch("app.pipeline.runner.get_auto_sections", return_value=True), + pytest.raises(JobCancelled), + ): + _run_common(job, job_dir / "source.wav", job_dir) + + +def test_common_pipeline_never_reanalyzes_existing_manual_sections(tmp_path: Path): + manual = [{"id": "custom", "name": "Pre-Chorus"}] + job = Job( + id="abcdefabc119", + duration_sec=60.0, + sections=manual, + sections_source="manual", + ) + job_dir = tmp_path / job.id + (job_dir / "stems").mkdir(parents=True) + patches = _common_stage_patches(job_dir, [{"id": "auto-001"}]) + + with ( + patches[0], + patches[1], + patches[2], + patches[3], + patches[4], + patches[5], + patches[6], + patches[7], + patches[8] as detect, + ): + _run_common(job, job_dir / "source.wav", job_dir) + + detect.assert_not_called() + assert job.sections == manual + assert job.sections_source == "manual" + + +def test_metadata_includes_sections_and_source(tmp_path: Path): + job = Job( + id="abcdefabc114", + sections=[{"id": "auto-001"}], + sections_source="automatic", + ) + job_dir = tmp_path / job.id + job_dir.mkdir() + + _write_metadata(job, job_dir) + + import json as _json + + meta = _json.loads((job_dir / "metadata.json").read_text(encoding="utf-8")) + assert meta["sections"] == [{"id": "auto-001"}] + assert meta["sections_source"] == "automatic" diff --git a/tests/test_pipeline_sections.py b/tests/test_pipeline_sections.py new file mode 100644 index 00000000..aaae4925 --- /dev/null +++ b/tests/test_pipeline_sections.py @@ -0,0 +1,480 @@ +from __future__ import annotations + +import math +import sys +import threading +import time +from pathlib import Path + +import numpy as np +import pytest + +from app.core.models import Job, JobCancelled +from app.pipeline.section_refine import refine_segments +from app.pipeline.sections import normalize_sections + +MODEL_LABELS = ( + "start", + "end", + "intro", + "outro", + "break", + "bridge", + "inst", + "solo", + "verse", + "chorus", +) + + +def _refinement_evidence(seconds: int, fps: int = 2): + frames = seconds * fps + activations = { + "segment": np.zeros(frames, dtype=float), + "label": np.zeros((len(MODEL_LABELS), frames), dtype=float), + } + embeddings = np.zeros((4, frames, 2), dtype=float) + return activations, embeddings, fps + + +def _set_label(activations, label: str, start: int, end: int, fps: int, value: float = 0.9): + activations["label"][MODEL_LABELS.index(label), start * fps : end * fps] = value + + +def test_normalize_sections_produces_gap_free_deterministic_records(): + raw = { + "segments": [ + {"start": 0.0, "end": 0.4, "label": "start"}, + {"start": 0.4, "end": 12.0, "label": "intro"}, + {"start": 12.0, "end": 36.0, "label": "verse"}, + {"start": 36.0, "end": 60.0, "label": "chorus"}, + {"start": 60.0, "end": 84.0, "label": "verse"}, + {"start": 84.0, "end": 99.6, "label": "chorus"}, + {"start": 99.6, "end": 100.0, "label": "end"}, + ] + } + + sections = normalize_sections(raw, 100.0) + + assert [s["id"] for s in sections] == [ + "auto-001", + "auto-002", + "auto-003", + "auto-004", + "auto-005", + ] + assert [s["kind"] for s in sections] == ["intro", "verse", "chorus", "verse", "chorus"] + assert sections[0]["start"] == 0.0 + assert sections[-1]["end"] == 100.0 + assert all( + left["end"] == right["start"] for left, right in zip(sections, sections[1:], strict=False) + ) + assert sections[1]["color"] == sections[3]["color"] + assert sections[2]["color"] == sections[4]["color"] + + +def test_normalize_sections_preserves_model_boundaries_and_merges_only_short_fragments(): + raw = [ + {"start": 0.0, "end": 8.0, "label": "intro"}, + {"start": 8.0, "end": 20.0, "label": "verse"}, + {"start": 20.0, "end": 20.3, "label": "break"}, + {"start": 20.3, "end": 32.0, "label": "verse"}, + {"start": 32.0, "end": 48.0, "label": "chorus"}, + {"start": 48.0, "end": 64.0, "label": "chorus"}, + ] + + sections = normalize_sections(raw, 64.0) + + assert [(s["kind"], s["start"], s["end"]) for s in sections] == [ + ("intro", 0.0, 8.0), + ("verse", 8.0, 32.0), + ("chorus", 32.0, 48.0), + ("chorus", 48.0, 64.0), + ] + + +def test_normalize_sections_accepts_a_neutral_low_confidence_part(): + raw = [ + {"start": 0.0, "end": 12.0, "label": "verse"}, + {"start": 12.0, "end": 24.0, "label": "part"}, + ] + + sections = normalize_sections(raw, 24.0) + + assert [section["kind"] for section in sections] == ["verse", "part"] + assert sections[1]["name"] == "Part" + + +def test_normalize_sections_keeps_a_song_whose_model_output_ends_past_the_duration(): + """duration_sec is rounded; the model reads the stems and overhangs it. + + Observed on a real 484-second track, which emitted a 10 ms "start" span + after its final section. Clamping collapsed that span to zero length and + the whole song lost every section. + """ + raw = { + "segments": [ + {"start": 0.0, "end": 0.53, "label": "start"}, + {"start": 0.53, "end": 30.45, "label": "verse"}, + {"start": 30.45, "end": 74.04, "label": "chorus"}, + {"start": 74.04, "end": 484.41, "label": "verse"}, + {"start": 484.41, "end": 484.42, "label": "start"}, + ] + } + + sections = normalize_sections(raw, 484) + + assert [section["kind"] for section in sections] == ["verse", "chorus", "verse"] + assert sections[0]["start"] == 0.0 + assert sections[-1]["end"] == 484 + + +def test_normalize_sections_neutralizes_a_sentinel_predicted_mid_song(): + """``start`` and ``end`` are ordinary classes, not only bracket markers. + + A real track was labelled ``start`` for 34 seconds in its middle. That is + the model failing to name a real span, which is what ``part`` is for. + """ + raw = { + "segments": [ + {"start": 0.0, "end": 12.0, "label": "verse"}, + {"start": 12.0, "end": 46.0, "label": "start"}, + {"start": 46.0, "end": 60.0, "label": "chorus"}, + ] + } + + sections = normalize_sections(raw, 60.0) + + assert [section["kind"] for section in sections] == ["verse", "part", "chorus"] + assert sections[1]["name"] == "Part" + + +def test_refinement_preserves_adjacent_equal_label_boundaries(): + raw = [ + {"start": 0.0, "end": 12.0, "label": "verse"}, + {"start": 12.0, "end": 24.0, "label": "verse"}, + {"start": 24.0, "end": 36.0, "label": "chorus"}, + ] + activations, embeddings, fps = _refinement_evidence(36) + _set_label(activations, "verse", 0, 24, fps) + _set_label(activations, "chorus", 24, 36, fps) + + refined = refine_segments(raw, activations, embeddings, fps, None, MODEL_LABELS) + + assert [(item["start"], item["end"], item["label"]) for item in refined] == [ + (0.0, 12.0, "verse"), + (12.0, 24.0, "verse"), + (24.0, 36.0, "chorus"), + ] + + +def test_refinement_adds_only_an_activation_peak_with_embedding_novelty(): + raw = [ + {"start": 0.0, "end": 24.0, "label": "intro"}, + {"start": 24.0, "end": 48.0, "label": "verse"}, + ] + activations, embeddings, fps = _refinement_evidence(48) + _set_label(activations, "intro", 0, 24, fps) + _set_label(activations, "verse", 24, 48, fps) + activations["segment"][12 * fps] = 0.8 + embeddings[:, : 12 * fps] = -1.0 + embeddings[:, 12 * fps : 24 * fps] = 1.0 + embeddings[:, 24 * fps : 36 * fps] = -1.0 + embeddings[:, 36 * fps :] = 1.0 + grid = {"confidence": 90, "beats": [index / fps for index in range(48 * fps)]} + + refined = refine_segments(raw, activations, embeddings, fps, grid, MODEL_LABELS) + + assert [item["start"] for item in refined] == [0.0, 12.0, 24.0] + assert [item["label"] for item in refined] == ["intro", "intro", "verse"] + + +def test_refinement_finds_a_boundary_without_a_trustworthy_beat_grid(): + """A beat grid aligns candidates; it must never gate whether they are found. + + Rubato, live, and free-time material is where the upstream spans most need + splitting, and it is exactly the material whose grid confidence is lowest. + """ + raw = [ + {"start": 0.0, "end": 24.0, "label": "intro"}, + {"start": 24.0, "end": 48.0, "label": "verse"}, + ] + + def refined_for(grid): + activations, embeddings, fps = _refinement_evidence(48) + _set_label(activations, "intro", 0, 24, fps) + _set_label(activations, "verse", 24, 48, fps) + activations["segment"][12 * fps] = 0.8 + embeddings[:, : 12 * fps] = -1.0 + embeddings[:, 12 * fps : 24 * fps] = 1.0 + embeddings[:, 24 * fps : 36 * fps] = -1.0 + embeddings[:, 36 * fps :] = 1.0 + return refine_segments(raw, activations, embeddings, fps, grid, MODEL_LABELS) + + beats = [index / 2 for index in range(96)] + for grid in (None, {"confidence": 20, "beats": beats}, {"confidence": 90, "beats": beats}): + result = refined_for(grid) + assert [item["start"] for item in result] == [0.0, 12.0, 24.0], grid + assert [item["label"] for item in result] == ["intro", "intro", "verse"], grid + + +def test_refinement_does_not_turn_a_beat_alone_into_a_boundary(): + raw = [ + {"start": 0.0, "end": 24.0, "label": "intro"}, + {"start": 24.0, "end": 48.0, "label": "verse"}, + ] + activations, embeddings, fps = _refinement_evidence(48) + _set_label(activations, "intro", 0, 24, fps) + _set_label(activations, "verse", 24, 48, fps) + embeddings[:, : 12 * fps] = -1.0 + embeddings[:, 12 * fps :] = 1.0 + grid = {"confidence": 90, "beats": [index / fps for index in range(48 * fps)]} + + refined = refine_segments(raw, activations, embeddings, fps, grid, MODEL_LABELS) + + assert [item["start"] for item in refined] == [0.0, 24.0] + + +def test_refinement_snaps_only_to_a_trustworthy_nearby_beat(): + raw = [ + {"start": 0.0, "end": 12.05, "label": "verse"}, + {"start": 12.05, "end": 24.0, "label": "chorus"}, + ] + activations, embeddings, fps = _refinement_evidence(24, fps=20) + _set_label(activations, "verse", 0, 12, fps) + _set_label(activations, "chorus", 12, 24, fps) + + trusted = refine_segments( + raw, + activations, + embeddings, + fps, + {"confidence": 90, "beats": [float(index) for index in range(25)]}, + MODEL_LABELS, + ) + untrusted = refine_segments( + raw, + activations, + embeddings, + fps, + {"confidence": 20, "beats": [float(index) for index in range(25)]}, + MODEL_LABELS, + ) + + assert trusted[0]["end"] == trusted[1]["start"] == 12.0 + assert untrusted[0]["end"] == untrusted[1]["start"] == 12.05 + + +def test_refinement_uses_part_for_an_ambiguous_semantic_label(): + raw = [ + {"start": 0.0, "end": 12.0, "label": "verse"}, + {"start": 12.0, "end": 24.0, "label": "chorus"}, + ] + activations, embeddings, fps = _refinement_evidence(24) + _set_label(activations, "verse", 0, 12, fps, 0.52) + _set_label(activations, "chorus", 0, 12, fps, 0.48) + _set_label(activations, "chorus", 12, 24, fps) + + refined = refine_segments(raw, activations, embeddings, fps, None, MODEL_LABELS) + + assert [item["label"] for item in refined] == ["part", "chorus"] + + +def test_refinement_regularizes_a_neutral_repeated_region(): + raw = [ + {"start": 0.0, "end": 8.0, "label": "chorus"}, + {"start": 8.0, "end": 16.0, "label": "verse"}, + {"start": 16.0, "end": 24.0, "label": "verse"}, + {"start": 24.0, "end": 32.0, "label": "outro"}, + ] + activations, embeddings, fps = _refinement_evidence(32) + _set_label(activations, "chorus", 0, 8, fps) + _set_label(activations, "verse", 8, 16, fps) + _set_label(activations, "verse", 16, 24, fps, 0.52) + _set_label(activations, "chorus", 16, 24, fps, 0.48) + _set_label(activations, "outro", 24, 32, fps) + embeddings[:, 0 : 8 * fps, 0] = 2.0 + embeddings[:, 8 * fps : 16 * fps, 1] = 2.0 + embeddings[:, 16 * fps : 24 * fps, 0] = 2.0 + embeddings[:, 24 * fps :, :] = -2.0 + + refined = refine_segments(raw, activations, embeddings, fps, None, MODEL_LABELS) + + assert refined[2]["label"] == "chorus" + + +def test_refinement_falls_back_when_evidence_is_malformed(): + raw = [ + {"start": 0.0, "end": 12.0, "label": "verse"}, + {"start": 12.0, "end": 24.0, "label": "chorus"}, + ] + + assert refine_segments(raw, {}, None, 100.0, None, MODEL_LABELS) == raw + + +@pytest.mark.parametrize( + "raw", + [ + [{"start": 0.0, "end": 10.0, "label": "verse"}], + [ + {"start": 0.0, "end": 10.0, "label": "verse"}, + {"start": 9.0, "end": 20.0, "label": "chorus"}, + ], + [ + {"start": 0.0, "end": 10.0, "label": "verse"}, + {"start": 12.0, "end": 20.0, "label": "unknown"}, + {"start": 20.0, "end": 30.0, "label": "chorus"}, + ], + [ + {"start": 0.0, "end": math.nan, "label": "verse"}, + {"start": 10.0, "end": 20.0, "label": "chorus"}, + ], + [ + {"start": 10.0, "end": 0.0, "label": "verse"}, + {"start": 10.0, "end": 20.0, "label": "chorus"}, + ], + ], +) +def test_normalize_sections_rejects_untrustworthy_output(raw): + assert normalize_sections(raw, 30.0) == [] + + +def test_normalize_sections_accepts_small_rounding_gaps_at_a_shared_boundary(): + raw = [ + {"start": 0.0, "end": 12.0, "label": "intro"}, + {"start": 12.08, "end": 30.0, "label": "verse"}, + {"start": 30.0, "end": 45.0, "label": "chorus"}, + ] + + sections = normalize_sections(raw, 45.0) + + assert sections[0]["end"] == sections[1]["start"] == 12.04 + + +def test_detect_sections_skips_when_required_stems_are_missing(tmp_path: Path, monkeypatch): + from app.pipeline import sections as module + + stems_dir = tmp_path / "stems" + stems_dir.mkdir() + (stems_dir / "vocals.wav").write_bytes(b"RIFF") + called = False + + def unexpected(*_args, **_kwargs): + nonlocal called + called = True + return {} + + monkeypatch.setattr(module, "_run_worker", unexpected) + + assert module.detect_sections(Job(id="abcdefabcdef"), stems_dir, 60.0) is None + assert called is False + + +def test_detect_sections_cleans_temporary_other_mix(tmp_path: Path, monkeypatch): + from app.pipeline import sections as module + + stems_dir = tmp_path / "stems" + stems_dir.mkdir() + for name in ("vocals", "drums", "bass", "guitar", "piano", "other"): + (stems_dir / f"{name}.wav").write_bytes(b"RIFF") + (stems_dir / "beats.json").write_text('{"confidence":90,"beats":[0,1]}', encoding="utf-8") + workspace = None + + def fake_mix(_job, _stems_dir, work_dir): + nonlocal workspace + workspace = work_dir + temp_other = work_dir / "other.wav" + temp_other.write_bytes(b"RIFF-float") + return temp_other + + monkeypatch.setattr(module, "_mix_other_stems", fake_mix) + + def fake_worker(_job, work_dir): + assert (work_dir / "beats.json").read_text(encoding="utf-8") == ( + '{"confidence":90,"beats":[0,1]}' + ) + return { + "segments": [ + {"start": 0.0, "end": 20.0, "label": "verse"}, + {"start": 20.0, "end": 40.0, "label": "chorus"}, + ] + } + + monkeypatch.setattr(module, "_run_worker", fake_worker) + + sections = module.detect_sections(Job(id="abcdefabcdef"), stems_dir, 40.0) + + assert sections is not None + assert [section["kind"] for section in sections] == ["verse", "chorus"] + assert workspace is not None + assert not workspace.exists() + + +def test_detect_sections_preserves_cancellation(tmp_path: Path): + from app.pipeline import sections as module + + job = Job(id="abcdefabcdef", cancel_requested=True) + + with pytest.raises(JobCancelled): + module.detect_sections(job, tmp_path, 60.0) + + +def test_registered_process_honors_inflight_cancellation(): + from app.pipeline import sections as module + + job = Job(id="abcdefabc116") + + def cancel_soon(): + time.sleep(0.2) + job.cancel_requested = True + + thread = threading.Thread(target=cancel_soon) + thread.start() + try: + with pytest.raises(JobCancelled): + module._run_registered_process( + job, + [sys.executable, "-c", "import time; time.sleep(30)"], + ) + finally: + thread.join(timeout=2) + + +def test_registered_process_enforces_total_timeout(monkeypatch): + from app.pipeline import sections as module + + monkeypatch.setattr(module, "TIMEOUT_SECTIONS", 0.2) + monkeypatch.setattr(module, "TIMEOUT_SECTIONS_STALL", 30) + started = time.monotonic() + + returncode, _stdout, _stderr = module._run_registered_process( + Job(id="abcdefabc117"), + [sys.executable, "-c", "import time; time.sleep(30)"], + ) + + assert returncode != 0 + assert time.monotonic() - started < 5 + + +def test_mix_other_stems_uses_all_sources_and_float_output(tmp_path: Path, monkeypatch): + from app.pipeline import sections as module + + stems_dir = tmp_path / "stems" + work_dir = stems_dir / ".sections-work-test" + work_dir.mkdir(parents=True) + for name in ("other", "guitar", "piano"): + (stems_dir / f"{name}.wav").write_bytes(b"RIFF") + captured = [] + + def fake_process(_job, cmd): + captured.extend(cmd) + (work_dir / "other.wav").write_bytes(b"RIFFDATA") + return 0, [], [] + + monkeypatch.setattr(module, "_run_registered_process", fake_process) + output = module._mix_other_stems(Job(id="abcdefabc118"), stems_dir, work_dir) + + assert output == work_dir / "other.wav" + assert any("amix=inputs=3:normalize=0:duration=longest" in arg for arg in captured) + assert "pcm_f32le" in captured + assert all(str(stems_dir / f"{name}.wav") in captured for name in ("other", "guitar", "piano")) diff --git a/tests/test_pipeline_warmup.py b/tests/test_pipeline_warmup.py new file mode 100644 index 00000000..b5855150 --- /dev/null +++ b/tests/test_pipeline_warmup.py @@ -0,0 +1,64 @@ +import os +from unittest.mock import patch + +from app.pipeline import warmup + + +def test_section_warmup_loads_cpu_model(): + with patch("allin1_infer.models.load_pretrained_model") as load: + warmup._warm_sections() + + load.assert_called_once_with(model_name=warmup.SECTION_MODEL, device="cpu") + + +def test_warmup_continues_after_individual_failure(monkeypatch, capsys): + calls = [] + + def fail(): + calls.append("fail") + raise RuntimeError("offline") + + def succeed(): + calls.append("succeed") + + monkeypatch.setattr(warmup, "_STEPS", (("sections", fail), ("demucs", succeed))) + + assert warmup.main() == 0 + assert calls == ["fail", "succeed"] + assert capsys.readouterr().out.splitlines() == [ + "WARMUP_FAILED sections offline", + "WARMUP_OK demucs", + ] + + +def test_section_warmup_disables_hugging_face_symlinks(monkeypatch): + """Unelevated Windows cannot create the cache symlinks the hub prefers. + + Stubbed rather than patched through the real package so the guarantee is + checked even where the optional inference dependency is not installed. + """ + import sys + import types + + monkeypatch.delenv("HF_HUB_DISABLE_SYMLINKS", raising=False) + package = types.ModuleType("allin1_infer") + models = types.ModuleType("allin1_infer.models") + seen = {} + models.load_pretrained_model = lambda **kwargs: seen.update(kwargs) + package.models = models + monkeypatch.setitem(sys.modules, "allin1_infer", package) + monkeypatch.setitem(sys.modules, "allin1_infer.models", models) + + warmup._warm_sections() + + assert os.environ["HF_HUB_DISABLE_SYMLINKS"] == "1" + assert seen == {"model_name": warmup.SECTION_MODEL, "device": "cpu"} + + +def test_section_worker_disables_hugging_face_symlinks(): + """The worker downloads the same checkpoints in its own process.""" + import importlib + + module = importlib.import_module("app.pipeline.section_worker") + + assert "HF_HUB_DISABLE_SYMLINKS" in module.os.environ diff --git a/tests/test_registry_persistence.py b/tests/test_registry_persistence.py index ea108bad..fe4468d3 100644 --- a/tests/test_registry_persistence.py +++ b/tests/test_registry_persistence.py @@ -205,6 +205,30 @@ def test_restore_recovers_orphan_done_job_from_stems(tmp_path: Path): assert {stem["name"] for stem in restored.stems} == {"vocals", "drums"} +def test_restore_recovers_automatic_sections_from_metadata(tmp_path: Path): + job_dir = tmp_path / "abcdefabc115" + stems_dir = job_dir / "stems" + stems_dir.mkdir(parents=True) + (stems_dir / "vocals.wav").write_bytes(b"RIFF") + sections = [{"id": "auto-001", "kind": "verse"}] + (job_dir / "metadata.json").write_text( + json.dumps( + { + "title": "Structured Song", + "sections": sections, + "sections_source": "automatic", + } + ), + encoding="utf-8", + ) + + restore_registry(tmp_path) + + restored = _jobs["abcdefabc115"] + assert restored.sections == sections + assert restored.sections_source == "automatic" + + def test_restore_recovers_orphan_without_metadata(tmp_path: Path): """#284: a crash between status=done and the metadata write used to leave a complete stems dir permanently unrecoverable. Now it comes back with a diff --git a/uv.lock b/uv.lock index 3bde55c4..e600d1ba 100644 --- a/uv.lock +++ b/uv.lock @@ -3,11 +3,11 @@ revision = 3 requires-python = ">=3.10, <3.14" resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] @@ -20,6 +20,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/0a/a10b45aab35b175aded078a462dc8d0c698f5b13946e7cb0869097b78bb6/absl_py-2.5.0-py3-none-any.whl", hash = "sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba", size = 137410, upload-time = "2026-07-03T10:57:46.735Z" }, ] +[[package]] +name = "all-in-one-infer" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "demucs-infer" }, + { name = "huggingface-hub" }, + { name = "hydra-core" }, + { name = "librosa" }, + { name = "madmom-infer" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "omegaconf" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "soundfile" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/29/021721b25cecf177463362ea7ac34b8fd26dc49aa5036f907b6b15c0495f/all_in_one_infer-3.1.0.tar.gz", hash = "sha256:0a027b0aa216bd0d4cc96a91642b8111573c65fceeae29f81ff9661578416383", size = 93112, upload-time = "2026-07-12T01:13:02.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/23/a76663f2ed86f986943ec8a549387e6ac00e98d09d1df32b6889d309afc1/all_in_one_infer-3.1.0-py3-none-any.whl", hash = "sha256:c41d6203726e058c6ecf02c513a6c2473d46145b4839446691c211184b5abdc1", size = 67910, upload-time = "2026-07-12T01:13:01.463Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -63,30 +90,30 @@ name = "audio-separator" version = "0.44.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts", marker = "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')" }, - { name = "beartype", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "diffq", marker = "(platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'win32')" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "beartype" }, + { name = "diffq", marker = "sys_platform != 'win32'" }, { name = "diffq-fixed", marker = "sys_platform == 'win32'" }, - { name = "einops", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "julius", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "librosa", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "ml-collections", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "onnx-weekly", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "onnx2torch-py313", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "pydub", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "requests", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "resampy", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "rotary-embedding-torch", version = "0.6.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "samplerate", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "six", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "soundfile", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "tqdm", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "einops" }, + { name = "julius" }, + { name = "librosa" }, + { name = "ml-collections" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnx-weekly" }, + { name = "onnx2torch-py313" }, + { name = "pydub" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "resampy" }, + { name = "rotary-embedding-torch", version = "0.6.5", source = { registry = "https://pypi.org/simple" } }, + { name = "samplerate" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "six" }, + { name = "soundfile" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/a1/91884a9cbbace7d1eb480f6446e10fc2cdf0aa8d86da414f4593557b6340/audio_separator-0.44.5.tar.gz", hash = "sha256:58866bc61d0c692fff8a52cda67c284f7847a844048987135677a007bf0ae794", size = 344654, upload-time = "2026-07-20T22:02:44.933Z" } wheels = [ @@ -330,14 +357,11 @@ wheels = [ [[package]] name = "click" -version = "8.3.3" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] [[package]] @@ -358,6 +382,155 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "cython" version = "3.2.9" @@ -421,14 +594,37 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/87/38/55f835ebd9f443465087a6954ede19d4a41aebdf5e28567e89b99d6d2f57/demucs-4.0.1.tar.gz", hash = "sha256:e45a5a788bae79767c37bbf6e69aae03862ddcca05550fb79b926346a177d713", size = 1212924, upload-time = "2023-09-07T16:09:01.334Z" } [[package]] -name = "diffq" -version = "0.2.4" +name = "demucs-infer" +version = "4.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cython", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "einops" }, + { name = "julius" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "pyyaml" }, + { name = "soundfile" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "torchaudio", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torchaudio", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/14/627a789ae0414469cd33dab1f99a41c84428533d9e1a66d33097476f0c90/demucs_infer-4.2.2.tar.gz", hash = "sha256:7a2a2fb1c0db57192fed6485f106986f4cd04c475c989e84a8f4022ce19e26b2", size = 69104, upload-time = "2026-07-12T01:00:24.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/c3/4004c48a48a9507045c3dc107f51bf0abce5165bd695d10eda94f0a38844/demucs_infer-4.2.2-py3-none-any.whl", hash = "sha256:df07b115690021dcfa6b2a6de1b7b352741111bc46fad31ca83eaaba6afced8b", size = 87489, upload-time = "2026-07-12T01:00:22.941Z" }, +] + +[[package]] +name = "diffq" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/fd/4c58807bf855c5929ffa6da55f26dd6b9ae462a4193f5e09cc49fbbfd451/diffq-0.2.4.tar.gz", hash = "sha256:049064861e974ebf00d0badab8b324c775037371419eda3150985b9d477b5bd2", size = 157139, upload-time = "2023-05-05T12:39:43.089Z" } wheels = [ @@ -444,10 +640,10 @@ name = "diffq-fixed" version = "0.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cython", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "cython" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/96/8ca5acf5ecfd4108aa6f345cef171f2fda0c081cf0e7430671712586f172/diffq_fixed-0.2.4.tar.gz", hash = "sha256:cbc906b76fa23d1cf3c0ae517fbab744d9624980a068fe7fbb00dede1d83208d", size = 189695, upload-time = "2024-11-07T21:33:26.204Z" } wheels = [ @@ -485,7 +681,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -525,6 +721,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, + { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, + { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + [[package]] name = "fsspec" version = "2026.3.0" @@ -543,6 +780,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -607,6 +860,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "huggingface-hub" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/35/42316e8f6908b6d21bc8df017cc6efba94fb5edbf99b64e28dd142325e20/huggingface_hub-1.29.0.tar.gz", hash = "sha256:6ebb385a581435325cf6d5c5b233d5d4bc91175834d99fd65dae14379b36e9ad", size = 963121, upload-time = "2026-08-27T12:18:37.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/a5/47c2ea9b228ccbcba8467e9a64823146e8ebbad29855e591d8f5eedcc9c7/huggingface_hub-1.29.0-py3-none-any.whl", hash = "sha256:b00f7782afc14db4bc6572763810a635bdfbab8623d957bfb553bd18e03852cd", size = 795768, upload-time = "2026-08-27T12:18:35.431Z" }, +] + +[[package]] +name = "hydra-core" +version = "1.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/e4/69a522676faf88994d93d8a5e69e0666c61cae7f73d1bbcc483222023e74/hydra_core-1.3.5.tar.gz", hash = "sha256:71c441eabbde086062045e4d3fce9e26015244f1a4ac721cf3e444c7edf10633", size = 3264337, upload-time = "2026-08-05T18:33:21.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/97/f9d463a6f3c7d0955753eca5cbbf35b596ac471dd13fe357211a53fd37be/hydra_core-1.3.5-py3-none-any.whl", hash = "sha256:a3ff35b4ea6794e4c83d993016f4bde4ac35797ebe7a08f30e83ed9341880331", size = 155768, upload-time = "2026-08-05T18:33:19.834Z" }, +] + [[package]] name = "idna" version = "3.13" @@ -656,6 +943,100 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/a1/19/c9e1596b5572c786b93428d0904280e964c930fae7e6c9368ed9e1b63922/julius-0.2.7.tar.gz", hash = "sha256:3c0f5f5306d7d6016fcc95196b274cae6f07e2c9596eed314e4e7641554fbb08", size = 59640, upload-time = "2022-09-19T16:13:34.2Z" } +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + [[package]] name = "lameenc" version = "1.8.2" @@ -787,6 +1168,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, ] +[[package]] +name = "madmom-infer" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/e6/151625e3df0ee3b210f5d17da6f7eb7cf7d7dc6289454bc24560baa52f2a/madmom_infer-0.2.0.tar.gz", hash = "sha256:6f45b3fc0d7b5808586da229cef98aa7ea10448cd38d3e69dd078142b3ba729d", size = 66366, upload-time = "2026-07-12T00:31:30.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/63/4dfb8d7acfd4226d5083a278ff7f97c6fe1cb70fff178e4d7fb854590919/madmom_infer-0.2.0-py3-none-any.whl", hash = "sha256:f4013a7ac2135f2f198d97f9e7840db4fbd993e2922f70b28862394c8f8d28f1", size = 79998, upload-time = "2026-07-12T00:31:29.478Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -850,13 +1247,135 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", +] +dependencies = [ + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" } }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/9b4298911303f74e6d83e64a81d996c0616405ec95046fac7f17e4258b9e/matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83", size = 10024922, upload-time = "2026-07-18T03:38:08.236Z" }, + { url = "https://files.pythonhosted.org/packages/84/6f/0bc3c3d05b021db44c14bc379a7c0df7d57302aa15380c16fd4e63fd6a9b/matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099", size = 10832170, upload-time = "2026-07-18T03:38:10.276Z" }, + { url = "https://files.pythonhosted.org/packages/db/4d/e375f39acdb2af5a9342730618608e39790ec842e6f1b392863028781459/matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407", size = 10916701, upload-time = "2026-07-18T03:38:12.512Z" }, + { url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" }, + { url = "https://files.pythonhosted.org/packages/24/ea/65cec46fe241390ccea1b1754207ee28eb71c5ab866bd5f22fe47e538fa4/matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3", size = 10035860, upload-time = "2026-07-18T03:38:39.663Z" }, + { url = "https://files.pythonhosted.org/packages/c7/10/63fdccccbabe002fb0960876baabc5e3f24d9c1bb4cfb25651457f74b3a0/matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e", size = 10843594, upload-time = "2026-07-18T03:38:42.144Z" }, + { url = "https://files.pythonhosted.org/packages/98/51/a1155945bff7b91381875022ac1522c5dfdac0d006be8e7df389b3134eae/matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f", size = 10927962, upload-time = "2026-07-18T03:38:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7f/36ffe144fc4aacfe0e3ed2318f72b6755d1e73b041d619b4d393e60f5a66/matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464", size = 10049244, upload-time = "2026-07-18T03:38:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/55812d68c0a840d3a463638f48c00ab1fe338518ec49a640cb6473b444af/matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf", size = 10860798, upload-time = "2026-07-18T03:38:58.282Z" }, + { url = "https://files.pythonhosted.org/packages/7a/64/cca444b4eb5e6c768c44fc5e1f0b5211f20ca2b282778051996e996a2bdf/matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f", size = 10943282, upload-time = "2026-07-18T03:39:00.465Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" }, + { url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/69552382fcc8e93d1f2763ef2665980a900a48b7f3a4c57ed290726d1cbc/matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99", size = 10019439, upload-time = "2026-07-18T03:39:43.78Z" }, +] + [[package]] name = "ml-collections" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "absl-py", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "absl-py" }, + { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/f8/1a9ae6696dbb6bc9c44ddf5c5e84710d77fe9a35a57e8a06722e1836a4a6/ml_collections-1.1.0.tar.gz", hash = "sha256:0ac1ac6511b9f1566863e0bb0afad0c64e906ea278ad3f4d2144a55322671f6f", size = 61356, upload-time = "2025-04-17T08:25:02.247Z" } wheels = [ @@ -868,8 +1387,8 @@ name = "ml-dtypes" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/72/307d7c4bd0600601c7133fba5cb78af7db968152951c1cd473abb1cda782/ml_dtypes-0.6.0.tar.gz", hash = "sha256:5e60251d32ced5598972e4d5e06a2f044341f9291402551a3f6f0ec44f9299b0", size = 3032327, upload-time = "2026-08-13T14:14:40.215Z" } wheels = [ @@ -973,9 +1492,9 @@ version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", ] 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" } @@ -993,8 +1512,8 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "llvmlite", version = "0.44.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "llvmlite", version = "0.44.0", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } wheels = [ @@ -1015,9 +1534,9 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "llvmlite", version = "0.47.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "llvmlite", version = "0.47.0", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } wheels = [ @@ -1220,7 +1739,7 @@ name = "nvidia-cudnn-cu12" version = "9.1.0.70" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "nvidia-cublas-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" }, @@ -1231,7 +1750,7 @@ name = "nvidia-cufft-cu12" version = "11.2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117, upload-time = "2024-04-03T20:57:40.402Z" }, @@ -1250,9 +1769,9 @@ name = "nvidia-cusolver-cu12" version = "11.6.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057, upload-time = "2024-04-03T20:58:28.735Z" }, @@ -1263,7 +1782,7 @@ name = "nvidia-cusparse-cu12" version = "12.3.1.170" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763, upload-time = "2024-04-03T20:58:59.995Z" }, @@ -1319,11 +1838,11 @@ name = "onnx-weekly" version = "1.23.0.dev20260817" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ml-dtypes", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "protobuf", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "typing-extensions", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "ml-dtypes" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "protobuf" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/44/f24ce74b129bbcbeac50e07e30ee807f3abaa711f81873fe2afe110e484d/onnx_weekly-1.23.0.dev20260817.tar.gz", hash = "sha256:860d42175f474562fe05236f168ba2340c66b566119750792caa8cada18c74da", size = 5983184, upload-time = "2026-08-17T00:28:03.157Z" } wheels = [ @@ -1352,11 +1871,11 @@ name = "onnx2torch-py313" version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "onnx-weekly", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "torchvision", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnx-weekly" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" } }, + { name = "torchvision" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/bb/6301c1ea4c8f8859aff60d2d58a038fd68f9cdb507d1c60776d4e3b93b1f/onnx2torch_py313-1.6.0.tar.gz", hash = "sha256:d27b54c7e170f12ad252fe186e6cab8f3cade8ee4e596682acc5a3afddadb753", size = 49594, upload-time = "2025-04-11T03:52:32.88Z" } wheels = [ @@ -1371,11 +1890,11 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "flatbuffers", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "packaging", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "protobuf", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "sympy", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "flatbuffers" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" }, @@ -1407,10 +1926,10 @@ resolution-markers = [ "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", ] dependencies = [ - { name = "flatbuffers", marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "packaging", marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "protobuf", marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "flatbuffers" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "protobuf" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/7c/a8/0520890321b8ff40b908cf165a93eb58fbc8f85c14db637277ea866c9544/onnxruntime-1.29.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:07c5907474dec4a2792fd7626b753dc66707808385a6d9eecf993db0066a9d0f", size = 21420890, upload-time = "2026-08-17T22:53:33.429Z" }, @@ -1703,6 +2222,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/b6/65a49a05614b2548edbba3aab118f2ebe7441dfd778accdcdce9f6567f20/pyloudnorm-0.2.0-py3-none-any.whl", hash = "sha256:9bb69afb904f59d007a7f9ba3d75d16fb8aeef35c44d6df822a9f192d69cf13f", size = 10879, upload-time = "2026-01-04T11:43:34.534Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -1735,6 +2263,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1819,9 +2359,9 @@ name = "resampy" version = "0.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numba", version = "0.65.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "numba", version = "0.65.1", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/f1/34be702a69a5d272e844c98cee82351f880985cfbca0cc86378011078497/resampy-0.4.3.tar.gz", hash = "sha256:a0d1c28398f0e55994b739650afef4e3974115edbe96cd4bb81968425e916e47", size = 3080604, upload-time = "2024-03-05T20:36:08.119Z" } wheels = [ @@ -1848,8 +2388,8 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "einops", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "einops" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/99/7e/8dbb4a950a99ec2f459ec1eb73818c519b56a1fee8a9a98f7a57460a0d5a/rotary_embedding_torch-0.6.5.tar.gz", hash = "sha256:a3623274c559e0215922edc7cd14068d5a64dea1ca469e1ecd642b293ee8af78", size = 7114, upload-time = "2024-08-20T20:52:44.155Z" } wheels = [ @@ -1866,8 +2406,8 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "einops", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "einops" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/38/74783585b1f0282fddd3faf1abd6dd20977255c27e31737eced2d7ec05f1/rotary_embedding_torch-0.8.9.tar.gz", hash = "sha256:b213f153cad1d108064d930544fb3af678d56515893d3f869a7a146f87997e3f", size = 7497, upload-time = "2025-07-27T01:26:14.675Z" } wheels = [ @@ -1904,9 +2444,9 @@ name = "samplerate" version = "0.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "cffi" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/9c/6a13fbc59b1ceaef194a53d3aeafa983057133fb13660c423854184eafa7/samplerate-0.1.0.tar.gz", hash = "sha256:75ef725e6cd9c4545569caf4c47147beab7b53b2c36e5122e8c285d348f88847", size = 4044998, upload-time = "2017-02-24T00:01:56.505Z" } wheels = [ @@ -1922,11 +2462,11 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -1963,17 +2503,17 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", ] dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, + { 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 = [ @@ -2012,8 +2552,8 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -2070,14 +2610,14 @@ version = "1.17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version >= '3.11' and python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -2204,8 +2744,8 @@ name = "standard-aifc" version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, - { name = "standard-chunk", marker = "python_full_version >= '3.13'" }, + { name = "audioop-lts" }, + { name = "standard-chunk" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } wheels = [ @@ -2226,7 +2766,7 @@ name = "standard-sunau" version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "audioop-lts" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" } wheels = [ @@ -2250,6 +2790,7 @@ wheels = [ name = "stemdeck" source = { editable = "." } dependencies = [ + { name = "all-in-one-infer" }, { name = "audio-separator", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "audioread", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "beat-this" }, @@ -2283,6 +2824,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "all-in-one-infer", specifier = ">=3.1,<4" }, { name = "audio-separator", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'", specifier = ">=0.24" }, { name = "audioread", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'", specifier = ">=3.0" }, { name = "beat-this", specifier = ">=1.1" }, @@ -2390,13 +2932,13 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "filelock", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "fsspec", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "jinja2", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "sympy", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "typing-extensions", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sympy" }, + { name = "typing-extensions" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3b/55/7192974ab13e5e5577f45d14ce70d42f5a9a686b4f57bbe8c9ab45c4a61a/torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e", size = 150788930, upload-time = "2024-03-27T21:08:09.98Z" }, @@ -2415,11 +2957,11 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "filelock", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "fsspec", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "jinja2", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, @@ -2433,10 +2975,10 @@ dependencies = [ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, - { name = "sympy", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "typing-extensions" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/37/81/aa9ab58ec10264c1abe62c8b73f5086c3c558885d6beecebf699f0dbeaeb/torch-2.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6860df13d9911ac158f4c44031609700e1eba07916fff62e21e6ffa0a9e01961", size = 766685561, upload-time = "2025-01-29T16:19:12.12Z" }, @@ -2467,7 +3009,7 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" } }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/76/70/ca793994d37815070f6b53932b71822f66cfb3e197e6937426815998221e/torchaudio-2.2.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b1d58201d108e85db3e35b84319f33884f61f327c38ead86913218c8c1acc3dd", size = 3398751, upload-time = "2024-03-27T21:12:31.998Z" }, @@ -2486,7 +3028,7 @@ resolution-markers = [ "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" } }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/38/aa/f634960ac094e3fc6869f5c214ccfa6f74da2b1a89cefac024f6c650a717/torchaudio-2.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0eda1cd876f44fc014dc04aa680db2fa355a83df5d834398db6dd5f5cd911f4c", size = 1808471, upload-time = "2025-01-29T16:29:43.783Z" }, @@ -2512,10 +3054,10 @@ name = "torchvision" version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, - { name = "pillow", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" } }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a9/20/72eb0b5b08fa293f20fc41c374e37cf899f0033076f0144d2cdc48f9faee/torchvision-0.21.0-1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5568c5a1ff1b2ec33127b629403adb530fab81378d9018ca4ed6508293f76e2b", size = 2327643, upload-time = "2025-03-18T17:25:51.165Z" }, From 4c8bb9781bd3a5f128fcb5fadde253f2016c5318 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sat, 29 Aug 2026 00:24:44 +0100 Subject: [PATCH 2/7] Let the user clear the section markers, and stop calling them experimental twice Three things about the sections bar, all reported from looking at it. A Clear button sits beside Add, hidden entirely while there is nothing to clear. Clearing cannot be undone and an automatic set costs a whole re-import to regenerate, so the first click only arms the button and the second one clears; it disarms itself after four seconds. The app has no modal-confirm idiom anywhere, and this is the lightest guard that still makes a mis-click harmless. The header laid its controls out with space-between, so every direct child drifted to its own corner and Clear ended up marooned from Add. They are one group now, and the split is title against controls rather than control against control. The "Experimental - drag to adjust." badge is gone, along with the _sectionsSource state that existed only to drive it. How a set arrived is still recorded server-side; the timeline just no longer labels it, because a marker is editable either way. The word now sits under the Song structure toggle instead, where the choice is actually made rather than on the results after the fact. While the toggle was being touched: it takes the same width and height as Split stems, so the pair reads as two segments of one bar rather than a chip loose beside a button. The width is shared through a variable and set as a minimum, not a fixed size, so a longer translation grows instead of clipping. It keeps the timeline blue rather than the accent amber, because two amber controls side by side read as two competing primary actions. Repeated section kinds are numbered. The model predicts boundaries and labels with separate heads, so two neighbouring Verse spans are a real predicted boundary between verse one and verse two rather than a duplicate, and merging them was already tried and found to discard five true boundaries on the reference track. The boundary stays and the labels read Chorus 1 and Chorus 2. A kind used once is never numbered, and a section renamed by hand keeps its own name. Reworking the badge tests into Clear-visibility tests caught a real bug: Clear lives in the header rather than the ribbon, so its state has to be refreshed before the container guard in initSections, and it was not. --- static/css/daw.css | 60 ++++++++++++++++++++++--------- static/css/variables.css | 4 +++ static/index.html | 26 ++++++++++---- static/js/catalog.js | 2 +- static/js/i18n.js | 46 +++++++++++++++++++----- static/js/job.js | 7 +--- static/js/sections.js | 73 +++++++++++++++++++++++++++++++------- tests/js/sections.test.mjs | 56 +++++++++++++++++++++-------- 8 files changed, 208 insertions(+), 66 deletions(-) diff --git a/static/css/daw.css b/static/css/daw.css index 743177c9..5acd51b0 100644 --- a/static/css/daw.css +++ b/static/css/daw.css @@ -234,10 +234,34 @@ input, textarea { font-family: inherit; } opacity: 0.85; } .stem-choice[aria-pressed="false"] .stem-dot { opacity: 0.4; } -/* Automatic song-structure detection. Not a stem and not the primary action, - so it takes the section timeline's blue rather than the amber the Split - button owns -- two amber chips side by side read as two competing CTAs. */ -.structure-toggle { --color: #4a7fff; margin-left: 10px; } +/* Automatic song-structure detection. It sits at the end of the composer next + to Split stems and matches its footprint, so the pair reads as two segments + of one bar rather than a chip loose beside a button. Colour still separates + them: the timeline blue, never the amber the primary action owns. */ +.structure-toggle { + --color: #4a7fff; + min-width: var(--composer-action-w); + height: auto; + align-self: stretch; + margin-left: 0; + justify-content: center; + border-radius: 0; + border: none; + border-left: 1px solid var(--border-strong); + flex-direction: column; + gap: 2px; + line-height: 1.15; +} +.structure-toggle-main { display: inline-flex; align-items: center; gap: 6px; } +/* Says what the feature is, quietly and where the choice is actually made, + rather than as a badge on the results after the fact. */ +.structure-toggle-note { + font-size: 8.5px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + opacity: 0.62; +} /* "All" toggle */ .stem-choice-all { @@ -281,6 +305,11 @@ input, textarea { font-family: inherit; } /* Process button */ .daw-process-btn { height: auto; + /* Shared with .structure-toggle so the two end segments are the same width. + A min rather than a fixed width, because a longer translation must be + allowed to grow instead of being clipped. */ + min-width: var(--composer-action-w); + justify-content: center; padding: 0 20px; background: linear-gradient(180deg, var(--accent), var(--accent-2)); border: none; @@ -1458,18 +1487,9 @@ input, textarea { font-family: inherit; } .daw-label-sub { font-size: 9px; } .daw-sections-header { justify-content: space-between; } .daw-sections-title-group { display: inline-flex; align-items: center; gap: 7px; min-width: 0; } -.sections-suggested-badge { - padding: 2px 5px; - border: 1px solid rgba(74,127,255,0.4); - border-radius: 4px; - color: #7fa2ff; - font-size: 8px; - font-weight: 700; - letter-spacing: 0.05em; - text-transform: uppercase; - white-space: nowrap; -} -.sections-suggested-badge.hidden { display: none; } +/* Clear and Add belong together. The header is space-between, so without this + group each control drifts to its own corner and Clear ends up marooned. */ +.sections-header-actions { display: inline-flex; align-items: center; gap: 6px; } .daw-sections-area { flex: 1; position: relative; @@ -1604,6 +1624,14 @@ input, textarea { font-family: inherit; } border-color: rgba(255,255,255,0.18); } +/* Clear-all sections. Neutral until it is armed, then it states plainly that + the next click destroys something, because nothing here can be undone. */ +.sections-clear-btn[data-armed="1"] { + border-color: color-mix(in srgb, var(--vocals) 55%, transparent); + background: color-mix(in srgb, var(--vocals) 14%, transparent); + color: var(--vocals); +} + /* Wave label (left column in wave header — Mixer label) */ .daw-wave-label { display: flex; diff --git a/static/css/variables.css b/static/css/variables.css index cf68fad0..e81c5cad 100644 --- a/static/css/variables.css +++ b/static/css/variables.css @@ -25,6 +25,10 @@ --piano: #a855f7; --other: #9ca3af; + /* Width shared by the composer's two end segments (Song structure and + Split stems) so they match. See .daw-process-btn / .structure-toggle. */ + --composer-action-w: 140px; + /* Accent */ --accent: #f4b740; --accent-2: #d99a2b; diff --git a/static/index.html b/static/index.html index a155f3c2..cb724b2e 100644 --- a/static/index.html +++ b/static/index.html @@ -107,7 +107,10 @@ the setting once per job, so a change applies to the next import without a restart. --> @@ -357,13 +360,22 @@
Sections - - - + + + + + +
diff --git a/static/js/catalog.js b/static/js/catalog.js index 21c1001d..c1257b3a 100644 --- a/static/js/catalog.js +++ b/static/js/catalog.js @@ -655,7 +655,7 @@ async function loadTrackIntoStudio(trackId) { applyTrackInfoToPanel(track); wireUpAudio(trackId, track.audioStems, track.duration || 0, track.thumb, track.mixUrl ?? null, track.title || "", peaksPromise, track.hasVideo ?? false, track.videoStatus ?? null); - initSections(trackId, track.sections, track.duration || 0, track.sectionsSource); + initSections(trackId, track.sections, track.duration || 0); } export function setCurrentTrack(trackId) { diff --git a/static/js/i18n.js b/static/js/i18n.js index 30747f22..cbdfd981 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -216,7 +216,6 @@ function applyStemRowAriaLabels(scope) { // ───────────────────────────────────────────────────────────────────────── const en = { - "sections.suggested": "Experimental - drag to adjust.", "sections.kind.intro": "Intro", "sections.kind.outro": "Outro", "sections.kind.break": "Break", @@ -227,8 +226,12 @@ const en = { "sections.kind.chorus": "Chorus", "sections.kind.part": "Part", "sections.kindNumbered": "{kind} {n}", + "sections.clear": "Clear", + "sections.clearAria": "Clear all sections", + "sections.clearConfirm": "Confirm?", "structure.toggle": "Song structure", "structure.toggleTitle": "Experimental Song Structure Extraction: automatically label intro, verse and chorus after a split", + "structure.experimental": "Experimental", "doc.title": "StemDeck — split any track into stems", "topbar.urlPlaceholder": "Search, or paste a YouTube or SoundCloud link, or drop an audio file…", @@ -766,7 +769,6 @@ const en = { }; const pl = { - "sections.suggested": "Eksperymentalne - przeciągnij, aby dostosować.", "sections.kind.intro": "Intro", "sections.kind.outro": "Zakończenie", "sections.kind.break": "Przerwa", @@ -777,8 +779,12 @@ const pl = { "sections.kind.chorus": "Refren", "sections.kind.part": "Część", "sections.kindNumbered": "{kind} {n}", + "sections.clear": "Wyczyść", + "sections.clearAria": "Wyczyść wszystkie sekcje", + "sections.clearConfirm": "Potwierdzić?", "structure.toggle": "Struktura utworu", "structure.toggleTitle": "Eksperymentalne wykrywanie struktury utworu: automatycznie oznacza intro, zwrotkę i refren po podziale", + "structure.experimental": "Eksperymentalne", "doc.title": "StemDeck — rozdziel dowolny utwór na ścieżki", "topbar.urlPlaceholder": "Szukaj albo wklej link YouTube lub SoundCloud, albo upuść plik audio…", @@ -1306,7 +1312,6 @@ const pl = { }; const ja = { - "sections.suggested": "試験的機能 - ドラッグして調整してください。", "sections.kind.intro": "イントロ", "sections.kind.outro": "アウトロ", "sections.kind.break": "ブレイク", @@ -1317,8 +1322,12 @@ const ja = { "sections.kind.chorus": "コーラス", "sections.kind.part": "パート", "sections.kindNumbered": "{kind}{n}", + "sections.clear": "クリア", + "sections.clearAria": "すべてのセクションを削除", + "sections.clearConfirm": "確認?", "structure.toggle": "曲の構成", "structure.toggleTitle": "実験的な楽曲構造抽出: 分離後にイントロ、Aメロ、サビを自動でラベル付けします", + "structure.experimental": "試験的", "doc.title": "StemDeck — トラックをパートごとに分離", "topbar.urlPlaceholder": "検索するか、YouTube・SoundCloud のリンクを貼り付けるか、音声ファイルをドロップ…", @@ -1821,7 +1830,6 @@ const ja = { }; const zhHans = { - "sections.suggested": "实验性功能 - 拖动以调整。", "sections.kind.intro": "前奏", "sections.kind.outro": "尾奏", "sections.kind.break": "间奏", @@ -1832,8 +1840,12 @@ const zhHans = { "sections.kind.chorus": "副歌", "sections.kind.part": "部分", "sections.kindNumbered": "{kind}{n}", + "sections.clear": "清除", + "sections.clearAria": "清除所有段落", + "sections.clearConfirm": "确认?", "structure.toggle": "歌曲结构", "structure.toggleTitle": "实验性歌曲结构提取: 分离后自动标记前奏、主歌和副歌", + "structure.experimental": "实验性", "doc.title": "StemDeck — 将任意曲目分离为音轨", "topbar.urlPlaceholder": "搜索,或粘贴 YouTube 或 SoundCloud 链接,或拖放音频文件…", @@ -2336,7 +2348,6 @@ const zhHans = { }; const de = { - "sections.suggested": "Experimentell - zum Anpassen ziehen.", "sections.kind.intro": "Intro", "sections.kind.outro": "Outro", "sections.kind.break": "Break", @@ -2347,8 +2358,12 @@ const de = { "sections.kind.chorus": "Refrain", "sections.kind.part": "Teil", "sections.kindNumbered": "{kind} {n}", + "sections.clear": "Löschen", + "sections.clearAria": "Alle Abschnitte löschen", + "sections.clearConfirm": "Sicher?", "structure.toggle": "Songstruktur", "structure.toggleTitle": "Experimentelle Songstruktur-Erkennung: beschriftet Intro, Strophe und Refrain nach dem Trennen automatisch", + "structure.experimental": "Experimentell", "doc.title": "StemDeck — jeden Track in Stems zerlegen", "topbar.urlPlaceholder": "Suchen, einen YouTube- oder SoundCloud-Link einfügen oder eine Audiodatei ablegen…", @@ -2862,7 +2877,6 @@ const de = { }; const pt = { - "sections.suggested": "Experimental - arraste para ajustar.", "sections.kind.intro": "Introdução", "sections.kind.outro": "Final", "sections.kind.break": "Pausa", @@ -2873,8 +2887,12 @@ const pt = { "sections.kind.chorus": "Refrão", "sections.kind.part": "Parte", "sections.kindNumbered": "{kind} {n}", + "sections.clear": "Limpar", + "sections.clearAria": "Limpar todas as seções", + "sections.clearConfirm": "Confirmar?", "structure.toggle": "Estrutura da música", "structure.toggleTitle": "Extração experimental da estrutura da música: marca intro, verso e refrão automaticamente após a separação", + "structure.experimental": "Experimental", "doc.title": "StemDeck — separe qualquer faixa em stems", "topbar.urlPlaceholder": "Pesquise, ou cole um link do YouTube ou SoundCloud, ou solte um arquivo de áudio…", @@ -3390,7 +3408,6 @@ const pt = { }; const id = { - "sections.suggested": "Eksperimental - seret untuk menyesuaikan.", "sections.kind.intro": "Intro", "sections.kind.outro": "Outro", "sections.kind.break": "Jeda", @@ -3401,8 +3418,12 @@ const id = { "sections.kind.chorus": "Refrain", "sections.kind.part": "Bagian", "sections.kindNumbered": "{kind} {n}", + "sections.clear": "Hapus", + "sections.clearAria": "Hapus semua bagian", + "sections.clearConfirm": "Yakin?", "structure.toggle": "Struktur lagu", "structure.toggleTitle": "Ekstraksi struktur lagu eksperimental: menandai intro, bait, dan refrein secara otomatis setelah pemisahan", + "structure.experimental": "Eksperimental", "doc.title": "StemDeck — pisahkan trek apa pun menjadi stem", "topbar.urlPlaceholder": "Cari, atau tempel tautan YouTube atau SoundCloud, atau seret file audio…", @@ -3905,7 +3926,6 @@ const id = { }; const fr = { - "sections.suggested": "Expérimental - faites glisser pour ajuster.", "sections.kind.intro": "Intro", "sections.kind.outro": "Outro", "sections.kind.break": "Pause", @@ -3916,8 +3936,12 @@ const fr = { "sections.kind.chorus": "Refrain", "sections.kind.part": "Partie", "sections.kindNumbered": "{kind} {n}", + "sections.clear": "Effacer", + "sections.clearAria": "Effacer toutes les sections", + "sections.clearConfirm": "Confirmer ?", "structure.toggle": "Structure du morceau", "structure.toggleTitle": "Extraction expérimentale de la structure: étiquette automatiquement intro, couplet et refrain après la séparation", + "structure.experimental": "Expérimental", "doc.title": "StemDeck — séparez n'importe quel morceau en pistes", "topbar.urlPlaceholder": "Recherchez, ou collez un lien YouTube ou SoundCloud, ou déposez un fichier audio…", @@ -4505,6 +4529,7 @@ const ptPT = { "settings.folder.save": "Guardar", "folderEditor.save": "Guardar", "sections.savingAria": "A guardar secções", + "sections.clearAria": "Limpar todas as secções", "aria.download": "Transferir {name}", "release.download": "Transferir", "release.downloading": "A transferir atualização…", @@ -4531,7 +4556,6 @@ const ptPT = { }; const es = { - "sections.suggested": "Experimental - arrastra para ajustar.", "sections.kind.intro": "Intro", "sections.kind.outro": "Outro", "sections.kind.break": "Pausa", @@ -4542,8 +4566,12 @@ const es = { "sections.kind.chorus": "Estribillo", "sections.kind.part": "Parte", "sections.kindNumbered": "{kind} {n}", + "sections.clear": "Borrar", + "sections.clearAria": "Borrar todas las secciones", + "sections.clearConfirm": "¿Confirmar?", "structure.toggle": "Estructura de la canción", "structure.toggleTitle": "Extracción experimental de la estructura: etiqueta automáticamente intro, verso y estribillo tras la separación", + "structure.experimental": "Experimental", "doc.title": "StemDeck — separa cualquier pista en stems", "topbar.urlPlaceholder": "Busca, pega un enlace de YouTube o SoundCloud, o suelta un archivo de audio…", diff --git a/static/js/job.js b/static/js/job.js index 4295739f..8ec73301 100644 --- a/static/js/job.js +++ b/static/js/job.js @@ -334,12 +334,7 @@ async function finishDoneJob(state) { null, finalState.has_video ?? false, ); - initSections( - finalState.job_id, - finalState.sections, - finalState.duration || 0, - finalState.sections_source, - ); + initSections(finalState.job_id, finalState.sections, finalState.duration || 0); } function applyState(state) { diff --git a/static/js/sections.js b/static/js/sections.js index d2cf3ffd..190097ce 100644 --- a/static/js/sections.js +++ b/static/js/sections.js @@ -22,7 +22,6 @@ const SECTION_KINDS = new Set([ let _trackId = null; let _duration = 0; let _sections = []; -let _sectionsSource = null; let _container = null; let _saveTimer = null; let _saveChain = Promise.resolve(); @@ -31,13 +30,14 @@ onLanguageChange(() => _render()); // ─── Public API ─────────────────────────────────────────── -export function initSections(trackId, sections, duration, sectionsSource = null) { +export function initSections(trackId, sections, duration) { _trackId = trackId; _duration = Math.max(1, duration || 0); _sections = (sections || []).map((s) => ({ ...s })); - _sectionsSource = sectionsSource; + // Clear lives in the header rather than the ribbon, so it has to be correct + // even when the ribbon is absent and the render below never runs. + _refreshClearVisibility(); _container = document.getElementById("daw-sections"); - _updateSuggestedBadge(); if (!_container) return; // Wire the static "Add" button in the label area (may already be wired) @@ -46,6 +46,7 @@ export function initSections(trackId, sections, duration, sectionsSource = null) addBtn.dataset.sectionsWired = "1"; addBtn.addEventListener("click", () => _addSection()); } + _wireClearButton(); _render(); } @@ -63,16 +64,18 @@ export function destroySections() { _hideSaveIndicator(); _trackId = null; _sections = []; - _sectionsSource = null; _duration = 0; if (_container) _container.innerHTML = ""; _container = null; - _updateSuggestedBadge(); + _refreshClearVisibility(); } // ─── Rendering ──────────────────────────────────────────── function _render() { + // Before the container guard: the button lives in the header, not the + // ribbon, so its state must stay correct even when the ribbon is absent. + _refreshClearVisibility(); if (!_container) return; _container.innerHTML = ""; @@ -135,11 +138,6 @@ export function sectionDisplayName(section, all) { return t("sections.kindNumbered", { kind: name, n: position + 1 }); } -function _updateSuggestedBadge() { - const badge = document.getElementById("sectionsSuggested"); - if (badge) badge.classList.toggle("hidden", _sectionsSource !== "automatic" || !_sections.length); -} - function _esc(str) { return String(str) .replace(/&/g, "&") @@ -340,6 +338,57 @@ function _addSection() { if (el) _openRename(section.id, el.querySelector(".section-label")); } +// Removing every marker at once cannot be undone, and an automatic set costs +// a whole re-import to regenerate, so the first click only arms the button. +// The app has no modal-confirm idiom, so this is the lightest guard that still +// makes a mis-click harmless. +const CLEAR_ARM_MS = 4000; +let _clearArmTimer = null; + +function _disarmClear() { + clearTimeout(_clearArmTimer); + _clearArmTimer = null; + const btn = document.getElementById("sectionsClearBtn"); + if (!btn) return; + delete btn.dataset.armed; + const label = btn.querySelector(".sections-clear-label"); + if (label) label.textContent = t("sections.clear"); +} + +function _wireClearButton() { + const btn = document.getElementById("sectionsClearBtn"); + if (!btn || btn.dataset.sectionsWired) return; + btn.dataset.sectionsWired = "1"; + btn.addEventListener("click", () => { + if (btn.dataset.armed === "1") { + _disarmClear(); + clearAllSections(); + return; + } + btn.dataset.armed = "1"; + const label = btn.querySelector(".sections-clear-label"); + if (label) label.textContent = t("sections.clearConfirm"); + clearTimeout(_clearArmTimer); + _clearArmTimer = setTimeout(_disarmClear, CLEAR_ARM_MS); + }); +} + +function _refreshClearVisibility() { + const btn = document.getElementById("sectionsClearBtn"); + if (!btn) return; + btn.classList.toggle("hidden", _sections.length === 0); + if (_sections.length === 0) _disarmClear(); +} + +export function clearAllSections() { + if (!_sections.length) return; + _sections = []; + // The set is now the user's own empty one, not a model suggestion, so the + // experimental badge must go with it. + _render(); + _scheduleSave(); +} + function _deleteSection(id) { _sections = _sections.filter((s) => s.id !== id); _render(); @@ -450,8 +499,6 @@ async function _sendSave(id, body) { return; } if (id === _trackId) { - _sectionsSource = "manual"; - _updateSuggestedBadge(); if (body === JSON.stringify({ sections: _sections }) && _saveTimer === null) _showSaved(); } } catch (e) { diff --git a/tests/js/sections.test.mjs b/tests/js/sections.test.mjs index cd0ff03c..4f94f4f5 100644 --- a/tests/js/sections.test.mjs +++ b/tests/js/sections.test.mjs @@ -4,6 +4,7 @@ import { flushSectionsSave, initSections, sectionDisplayName, + clearAllSections, } from "../../static/js/sections.js"; let pass = 0; @@ -19,7 +20,6 @@ const check = (name, condition) => { }; const sectionKeys = [ - "sections.suggested", "sections.kind.intro", "sections.kind.outro", "sections.kind.break", @@ -36,11 +36,6 @@ for (const { code } of LANGUAGES) { const table = code === "pt-PT" ? TRANSLATIONS.pt : TRANSLATIONS[code]; check(`${code} has every automatic-section label`, sectionKeys.every((key) => table[key])); } -check( - "English automatic-section badge explains that adjustment is experimental", - TRANSLATIONS.en["sections.suggested"] === "Experimental - drag to adjust.", -); - check( "canonical kinds use translated display labels", sectionDisplayName({ kind: "chorus", name: "model-label" }) === "Chorus", @@ -83,17 +78,20 @@ check( }), ); -const badge = { +const clearBtn = { hidden: true, + dataset: {}, classList: { toggle(_name, force) { - badge.hidden = force; + clearBtn.hidden = force; }, }, + querySelector: () => null, + addEventListener: () => {}, }; globalThis.document = { getElementById(id) { - return id === "sectionsSuggested" ? badge : null; + return id === "sectionsClearBtn" ? clearBtn : null; }, }; @@ -101,11 +99,10 @@ initSections( "abcdefabcdef", [{ id: "auto-001", kind: "verse", name: "Verse", start: 0, end: 10, color: "#fff" }], 10, - "automatic", ); -check("automatic sections show the experimental adjustment badge", badge.hidden === false); +check("Clear appears once there is something to clear", clearBtn.hidden === false); destroySections(); -check("destroying sections hides the experimental adjustment badge", badge.hidden === true); +check("Clear disappears with the last section", clearBtn.hidden === true); const requests = []; const complete = []; @@ -118,7 +115,6 @@ initSections( "abcdefabcdef", [{ id: "auto-001", kind: "intro", name: "Intro", start: 0, end: 10, color: "#fff" }], 20, - "automatic", ); const firstSave = flushSectionsSave(); await Promise.resolve(); @@ -126,7 +122,6 @@ initSections( "abcdefabcdef", [{ id: "auto-002", kind: "verse", name: "Verse", start: 10, end: 20, color: "#fff" }], 20, - "automatic", ); const secondSave = flushSectionsSave(); await Promise.resolve(); @@ -141,5 +136,38 @@ complete[1]({ ok: true }); await secondSave; destroySections(); +// Clearing every marker at once, and the Clear button retiring with them. +requests.length = 0; +complete.length = 0; +initSections( + "abcdefabcdef", + [ + { id: "auto-001", kind: "intro", name: "Intro", start: 0, end: 10, color: "#fff" }, + { id: "auto-002", kind: "verse", name: "Verse", start: 10, end: 20, color: "#fff" }, + ], + 20, +); +check("Clear is offered while sections exist", clearBtn.hidden === false); +clearAllSections(); +const clearSave = flushSectionsSave(); +await Promise.resolve(); +check( + "clearing saves an empty section list", + requests.length === 1 && requests[0].sections.length === 0, +); +check("Clear hides itself once the list is empty", clearBtn.hidden === true); +complete[0]({ ok: true }); +await clearSave; + +// Clearing an already-empty list is a no-op: no second save is scheduled and +// Clear stays hidden. (flushSectionsSave always queues a write, so this +// asserts the state rather than the request count.) +requests.length = 0; +complete.length = 0; +clearAllSections(); +check("clearing an already-empty list schedules nothing", requests.length === 0); +check("Clear stays hidden after a redundant clear", clearBtn.hidden === true); +destroySections(); + console.log(`\n${pass} passed, ${fail} failed`); process.exit(fail ? 1 : 0); From fa255ee54c7c3a96048fe4cb231e5cae8537e37a Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sat, 29 Aug 2026 00:25:03 +0100 Subject: [PATCH 3/7] Stop a long export from deleting itself on the way to the client Exporting a mixdown of a long track as WAV failed with a 500, after the server had already spent the whole render producing it. The threshold is about 49.5 minutes at 44.1 kHz and 45.5 at 48 kHz, both inside the 60 minutes StemDeck accepts, so this was a supported track failing rather than an unsupported one being refused. _render_to_file finishes by moving the render into the mixdown cache, pruning the cache, and returning the path the response is built from. The prune evicts oldest-first while the directory is over budget, and a single render larger than the whole 500 MB budget puts it over on its own. So the loop deleted the render, newest and only entry though it was, and FileResponse was handed a path that no longer existed. The prune now takes the file about to be served and never evicts it. Its size still counts toward the total, so an oversized entry clears everything else and then stops, leaving the cache one file over budget until the next render. That is the intended trade: a render the user is waiting on outranks the budget. Reproduced by shrinking the budget below one render, which is the same shape as a 60-minute WAV against the real 500 MB. The regression test asserts both halves, because exempting the served file must not quietly turn the prune into a no-op. The streaming path prunes too, but the client already has the bytes by then, so there the same eviction only costs a cache entry rather than the response. Closes #482 --- app/api/stems.py | 28 ++++++++++++++++++++---- tests/test_stems_api.py | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/app/api/stems.py b/app/api/stems.py index dc2aa947..e4710a15 100644 --- a/app/api/stems.py +++ b/app/api/stems.py @@ -146,16 +146,35 @@ def _mixdown_cache_key( return hashlib.sha1(raw.encode("utf-8"), usedforsecurity=False).hexdigest() -def _prune_mixdown_cache(cache_dir: Path) -> None: +def _prune_mixdown_cache(cache_dir: Path, keep: Path | None = None) -> None: """Evict oldest-first once either the file-count or total-size budget is exceeded. Best-effort: a failed prune just means the cache grows past - budget until the next successful render, not a broken export.""" + budget until the next successful render, not a broken export. + + `keep` is never evicted, and exists because a render is added to the cache + and then pruned before it is served. A single render larger than the whole + budget put the directory over on its own, so the loop deleted it, newest + and only entry though it was, and the caller handed a path that no longer + existed to FileResponse. A 60-minute WAV crosses the 500 MB budget at about + 49.5 minutes, well inside the 60 StemDeck accepts (#482). + + Its size still counts toward the total, so an oversized entry evicts + everything else and then stops, leaving the cache one file over budget + until the next render clears it. That is the intended trade: a rendered + file the user is waiting on outranks the budget. + """ try: entries = sorted( - (p for p in cache_dir.iterdir() if p.is_file() and not p.name.startswith(".")), + ( + p + for p in cache_dir.iterdir() + if p.is_file() and not p.name.startswith(".") and p != keep + ), key=lambda p: p.stat().st_mtime, ) total = sum(p.stat().st_size for p in entries) + if keep is not None and keep.is_file(): + total += keep.stat().st_size except OSError: return while entries and (len(entries) > _MIXDOWN_CACHE_MAX_FILES or total > _MIXDOWN_CACHE_MAX_BYTES): @@ -498,7 +517,8 @@ async def _render_to_file( if cache_path is not None: os.replace(tmp_path, cache_path) - _prune_mixdown_cache(cache_path.parent) + # Exempt from its own prune: this is the file about to be served. + _prune_mixdown_cache(cache_path.parent, keep=cache_path) return cache_path return tmp_path diff --git a/tests/test_stems_api.py b/tests/test_stems_api.py index df9b074e..d98e9b78 100644 --- a/tests/test_stems_api.py +++ b/tests/test_stems_api.py @@ -973,3 +973,51 @@ def test_cached_render_survives_its_response(client, tmp_path): (cached,) = (tmp_path / "cache" / "mixdown").glob("*.wav") assert cached.is_file(), "the cache entry was deleted with the response" assert client.get(url).content == cached.read_bytes() + + +def test_prune_never_evicts_the_render_it_is_about_to_serve(tmp_path, monkeypatch): + """A render bigger than the whole budget used to delete itself (#482). + + _render_to_file moves a finished render into the cache and prunes before + returning the path the response is built from. Eviction is oldest-first, + but a single entry over budget puts the directory over on its own, so the + loop removed it even as the newest and only file, and FileResponse was + handed a path that no longer existed. WAV crosses the 500 MB budget at + about 49.5 minutes; StemDeck accepts 60. + """ + from app.api import stems as stems_mod + + monkeypatch.setattr(stems_mod, "_MIXDOWN_CACHE_MAX_FILES", 100) + monkeypatch.setattr(stems_mod, "_MIXDOWN_CACHE_MAX_BYTES", 25) + cache_dir = tmp_path / "mixdown" + cache_dir.mkdir() + fresh = cache_dir / "fresh.wav" + fresh.write_bytes(b"x" * 400) # one render, far over the whole budget + os.utime(fresh, (99, 99)) # newest + + stems_mod._prune_mixdown_cache(cache_dir, keep=fresh) + + assert fresh.is_file(), "the file about to be served was evicted" + + +def test_prune_still_evicts_older_entries_around_a_kept_render(tmp_path, monkeypatch): + """Exempting the served render must not turn the prune into a no-op.""" + from app.api import stems as stems_mod + + monkeypatch.setattr(stems_mod, "_MIXDOWN_CACHE_MAX_FILES", 100) + monkeypatch.setattr(stems_mod, "_MIXDOWN_CACHE_MAX_BYTES", 25) + cache_dir = tmp_path / "mixdown" + cache_dir.mkdir() + for i in range(4): + p = cache_dir / f"old{i}.wav" + p.write_bytes(b"x" * 10) + os.utime(p, (i, i)) + fresh = cache_dir / "fresh.wav" + fresh.write_bytes(b"x" * 10) + os.utime(fresh, (99, 99)) + + stems_mod._prune_mixdown_cache(cache_dir, keep=fresh) + + remaining = {p.name for p in cache_dir.iterdir()} + assert "fresh.wav" in remaining + assert len(remaining) < 5, "nothing was evicted" From cd24a4e4ef04f1343176da23da7c53d0a5a350fa Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sat, 29 Aug 2026 00:25:27 +0100 Subject: [PATCH 4/7] Clean up the section workspace a crash left in the user's library Section analysis stages its work in a temporary directory inside the job's own stems folder and removes it in a finally. That covers every ordinary ending, cancellation included. It does not cover the process dying: a force quit, a lost machine, an OOM kill, or the desktop shell tearing the backend down while the stage runs. Nothing else in the codebase had ever heard of the prefix, so what was left behind stayed forever. It is not a trivial amount. other.wav inside it is a real file, the other/guitar/piano mix written as pcm_f32le: roughly 1.27 GB for a 60-minute track, plus the extracted spectrograms beside it. The name starts with a dot, so a user wondering why their library outgrew their songs had no obvious way to find it. The window is not small either, because the stage is a CPU inference pass measured in minutes and it is the last thing a job does, which is exactly when an impatient user quits. The sweep runs once at startup and nowhere else. Nothing is analyzing at that point, so every workspace it finds is certainly dead; running it later could delete one out from under a live job. It reuses the same guard as the in-band cleanup, refusing to remove anything whose name lacks the prefix or whose parent is not the stems directory, because it is deleting inside the user's library. Its failures are swallowed and logged for the same reason the stage itself is non-fatal: tidying up must never be the thing that breaks a separation. Closes #483 --- app/main.py | 10 ++++++- app/pipeline/sections.py | 46 +++++++++++++++++++++++++++++++ tests/test_pipeline_sections.py | 48 ++++++++++++++++++++++++++++++++- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index f51ff989..90ee3610 100644 --- a/app/main.py +++ b/app/main.py @@ -18,7 +18,7 @@ from pathlib import Path from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import FileResponse, PlainTextResponse, StreamingResponse +from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from app.api.router import router @@ -81,6 +81,7 @@ validate_target, ) from app.pipeline.collect import sweep_failed_jobs, sweep_old_jobs +from app.pipeline.sections import sweep_orphaned_workspaces as sweep_orphaned_section_workspaces # Set the stemdeck logger level (Python's default root level of WARNING would # silently drop every logger.info(...) call) and attach the rotating file log @@ -197,6 +198,13 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: # A restored queue can be dozens of tracks and hours of GPU, and the user # may well have opened StemDeck to do something else entirely. They press # Start (or simply import something new, which lifts the pause). + # Nothing is analyzing yet, so any section workspace still on disk belongs + # to a process that died mid-stage and is safe to remove (#483). + try: + await asyncio.to_thread(sweep_orphaned_section_workspaces, JOBS_DIR) + except Exception: + _log.exception("could not sweep orphaned section workspaces") + resumed = take_pending_resume() if resumed: jobqueue.pause() diff --git a/app/pipeline/sections.py b/app/pipeline/sections.py index 8d7d1672..7e509b40 100644 --- a/app/pipeline/sections.py +++ b/app/pipeline/sections.py @@ -360,6 +360,52 @@ def _safe_rmtree(path: Path, parent: Path) -> None: logger.error("refusing to remove invalid section workspace %s", resolved) +def sweep_orphaned_workspaces(jobs_dir: Path) -> int: + """Remove section workspaces a previous process died before cleaning up. + + detect_sections stages inside the job's own stems folder and removes the + directory in a finally, which covers every ordinary ending including + cancellation. It does not cover the process dying: a force quit, a lost + machine, an OOM kill, or the desktop shell tearing the backend down while + the stage runs. The stage is a CPU inference pass measured in minutes and + is the last thing a job does, so it is running exactly when an impatient + user quits. + + What is left behind is not trivial. ``other.wav`` inside it is a real file, + the other/guitar/piano mix written as pcm_f32le: about 1.27 GB for a + 60-minute track, plus the extracted spectrograms. The name starts with a + dot, so a user wondering why their library outgrew their songs cannot + easily find it (#483). + + Call this at startup only. Nothing is analyzing yet at that point, so every + workspace found is certainly dead; running it later could delete one out + from under a live job. Errors are swallowed for the same reason the stage + itself is non-fatal: tidying up must never be what breaks a library. + """ + removed = 0 + try: + job_dirs = list(jobs_dir.iterdir()) + except OSError: + return 0 + for job_dir in job_dirs: + stems_dir = job_dir / "stems" + try: + candidates = list(stems_dir.iterdir()) if stems_dir.is_dir() else [] + except OSError: + continue + for entry in candidates: + if not entry.is_dir() or not entry.name.startswith(_WORK_PREFIX): + continue + # Same guard as the in-band cleanup: prefix and parent must both + # match before anything inside a user's library is deleted. + _safe_rmtree(entry, stems_dir) + if not entry.exists(): + removed += 1 + if removed: + logger.info("removed %d orphaned section workspace(s)", removed) + return removed + + def detect_sections(job: Job, stems_dir: Path, duration: float) -> list[dict] | None: """Return automatic section suggestions, or None when analysis is unavailable.""" if job.cancel_requested: diff --git a/tests/test_pipeline_sections.py b/tests/test_pipeline_sections.py index aaae4925..da0ef192 100644 --- a/tests/test_pipeline_sections.py +++ b/tests/test_pipeline_sections.py @@ -11,7 +11,7 @@ from app.core.models import Job, JobCancelled from app.pipeline.section_refine import refine_segments -from app.pipeline.sections import normalize_sections +from app.pipeline.sections import normalize_sections, sweep_orphaned_workspaces MODEL_LABELS = ( "start", @@ -478,3 +478,49 @@ def fake_process(_job, cmd): assert any("amix=inputs=3:normalize=0:duration=longest" in arg for arg in captured) assert "pcm_f32le" in captured assert all(str(stems_dir / f"{name}.wav") in captured for name in ("other", "guitar", "piano")) + + +def test_sweep_removes_a_workspace_a_dead_process_left_behind(tmp_path: Path): + """A force quit bypasses detect_sections' finally, and nothing else in the + codebase has ever heard of the prefix. What is stranded is a pcm_f32le mix + of three stems: about 1.27 GB for a 60-minute track, hidden behind a dot + (#483).""" + jobs_dir = tmp_path / "jobs" + stems_dir = jobs_dir / "abcdefabcdef" / "stems" + stems_dir.mkdir(parents=True) + orphan = stems_dir / ".sections-work-dead" + orphan.mkdir() + (orphan / "other.wav").write_bytes(b"x" * 1024) + keeper = stems_dir / "drums.wav" + keeper.write_bytes(b"audio") + + removed = sweep_orphaned_workspaces(jobs_dir) + + assert removed == 1 + assert not orphan.exists() + assert keeper.is_file(), "a real stem was deleted" + + +def test_sweep_leaves_everything_that_is_not_a_workspace(tmp_path: Path): + """It runs against the user's library, so the prefix and the parent are + both load-bearing. Anything else in a stems folder must survive.""" + jobs_dir = tmp_path / "jobs" + stems_dir = jobs_dir / "abcdefabcdef" / "stems" + stems_dir.mkdir(parents=True) + survivors = [ + stems_dir / "htdemucs_6s", # a real demucs output directory + stems_dir / ".cache", # dot-prefixed, but not ours + stems_dir / "sections-work-no-dot", # close, but missing the leading dot + ] + for path in survivors: + path.mkdir() + (path / "keep.wav").write_bytes(b"x") + + assert sweep_orphaned_workspaces(jobs_dir) == 0 + for path in survivors: + assert (path / "keep.wav").is_file(), f"{path.name} was deleted" + + +def test_sweep_survives_a_library_it_cannot_read(tmp_path: Path): + """Tidying up must never be the thing that breaks startup.""" + assert sweep_orphaned_workspaces(tmp_path / "does-not-exist") == 0 From 7e07d5f7667d90b3d786e2382d01f74d334aa9ef Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sat, 29 Aug 2026 00:25:46 +0100 Subject: [PATCH 5/7] Stop one editor request from holding every other request to the server A PATCH to a job's sections accepted a list of any length. A single request with a large body blocked the event loop, so everything else waited on it, including the progress stream a running job depends on. Measured against a local server, an idle health check went from 32 ms to 5219 ms behind a 33 MB body. The stall grew linearly with the body, bounded only by what the sender was willing to upload. Two things made it worse than a slow endpoint. It needed no valid job, because the body is parsed before the handler runs and only then answers 404. And with a real job it was permanent rather than transient: the list was written to metadata.json and held in the registry, which is re-serialised on every persist. There are two bounds now, because one of them alone does not work. The model caps the list at 10000. Both normalize_sections and the timeline editor refuse a section shorter than half a second, so the longest track StemDeck accepts cannot legitimately carry more than 7200. This is what stops a huge list being stored. That cap does not stop the stall. FastAPI reads and validates a request body before the handler runs, so a model constraint bounds what is kept and not what is parsed: with the cap alone the stall was still 5219 ms. Content-Length is therefore checked in middleware, which is the last point that runs before the body is touched, and the same shape as the upload pre-check already in app/api/jobs.py. Health latency under the same attack is 16 ms. The ceiling is 4 MB, far above either editor's reach: 10000 sections at the longest permitted name is about 1.6 MB and 20000 beats about 0.4 MB. It covers the beat grid too, which had the same flaw in milder form at 922 ms, and leaves uploads alone on their own path and their own 400 MB limit. Closes #481 --- app/api/jobs.py | 12 +++++- app/main.py | 27 ++++++++++++++ tests/test_jobs_api.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/app/api/jobs.py b/app/api/jobs.py index 76fd6978..1f67d028 100644 --- a/app/api/jobs.py +++ b/app/api/jobs.py @@ -453,8 +453,18 @@ def _check_time(cls, v: float) -> float: return round(v, 3) +# Upper bound on a section list. normalize_sections and the timeline editor +# both refuse a section shorter than 0.5 s, so the longest track StemDeck +# accepts (3600 s) cannot legitimately carry more than 7200 of them; 10000 +# leaves headroom while refusing a payload sent to stall the event loop. +# Without a bound here a 33 MB body held every other request for ~4 seconds, +# and needed no valid job to do it: the body is parsed before the handler runs +# and answers 404 (#481). +_MAX_SECTIONS = 10000 + + class SectionsBody(BaseModel): - sections: list[SectionItem] + sections: list[SectionItem] = Field(max_length=_MAX_SECTIONS) @router.patch("/{job_id}/sections") diff --git a/app/main.py b/app/main.py index 90ee3610..99dc0ff6 100644 --- a/app/main.py +++ b/app/main.py @@ -838,6 +838,33 @@ def download_logs_zip() -> StreamingResponse: # restarts -- updated HTML loads against stale modules and the form # silently breaks. `must-revalidate` keeps 304s working (cheap) while # guaranteeing the latest mtime is honored. +# The timeline editors post JSON that is bounded by its model, but a model +# bounds only what is *stored*. FastAPI reads and parses a request body before +# the handler runs, so an oversized payload holds the event loop no matter what +# the model says: 32 MB of sections stalled every other request, including a +# running job's progress stream, for five seconds, and needed no valid job to +# do it (#481). Content-Length is checked here because middleware is the last +# point that runs before the body is touched. Same shape as the upload +# pre-check in app/api/jobs.py. +# +# The ceiling is far above either editor's reach: 10000 sections with the +# longest name each is about 1.6 MB, and 20000 beats about 0.4 MB. Uploads are +# unaffected -- they are a different path with their own 400 MB limit. +_EDITOR_BODY_LIMIT = 4 * 1024 * 1024 +_EDITOR_PATH_SUFFIXES = ("/sections", "/beats") + + +@app.middleware("http") +async def limit_editor_body_size(request: Request, call_next): + if request.method in ("PATCH", "POST", "PUT") and request.url.path.endswith( + _EDITOR_PATH_SUFFIXES + ): + declared = request.headers.get("content-length") + if declared and declared.isdigit() and int(declared) > _EDITOR_BODY_LIMIT: + return JSONResponse({"detail": "request body too large"}, status_code=413) + return await call_next(request) + + @app.middleware("http") async def security_and_cache_headers(request: Request, call_next): response = await call_next(request) diff --git a/tests/test_jobs_api.py b/tests/test_jobs_api.py index 16a45679..cdbc8b77 100644 --- a/tests/test_jobs_api.py +++ b/tests/test_jobs_api.py @@ -7,6 +7,7 @@ import pytest from fastapi.testclient import TestClient +from app.api import jobs as jobs_api from app.core.config import MAX_PENDING_UPLOAD_JOBS, MAX_PENDING_URL_JOBS from app.core.models import Job from app.core.registry import _jobs @@ -340,6 +341,89 @@ def test_sections_accepts_neutral_part_kind(client, done_job): assert response.json()["sections"][0]["kind"] == "part" +def _section(index: int) -> dict: + return { + "id": f"sec{index}", + "name": "Verse", + "kind": "verse", + "start": 0.0, + "end": 1.0, + "color": "#00c8a0", + } + + +def test_sections_rejects_a_list_long_enough_to_stall_the_server(client, done_job): + """The body is parsed before the handler runs, so an unbounded list holds + the event loop and every other request with it (#481). A 33 MB body stalled + an idle server's health check from 31 ms to 3.8 seconds.""" + payload = {"sections": [_section(i) for i in range(jobs_api._MAX_SECTIONS + 1)]} + + r = client.patch(f"/api/jobs/{done_job.id}/sections", json=payload) + + assert r.status_code == 422 + assert done_job.sections is None # nothing partially applied + + +def test_sections_cap_clears_the_longest_legitimate_track(client, done_job): + """0.5 s is the shortest section either the editor or normalize_sections + allows, so a 3600 s track tops out at 7200. The cap must not reject that.""" + assert jobs_api._MAX_SECTIONS >= 3600 / 0.5 + + payload = {"sections": [_section(i) for i in range(7200)]} + + assert client.patch(f"/api/jobs/{done_job.id}/sections", json=payload).status_code == 200 + + +def test_oversized_editor_body_is_refused_before_it_is_parsed(client, done_job): + """A model cap bounds what is stored, not what is parsed. + + FastAPI reads and validates a request body before the handler runs, so the + max_length above does not stop an oversized payload from holding the event + loop. Measured against a live server: 32 MB of sections took an idle health + check from 32 ms to 5219 ms, and 16 ms once Content-Length was checked in + middleware first (#481). + """ + from app.main import _EDITOR_BODY_LIMIT + + padded = dict(_section(0), name="V" * 64) + count = (_EDITOR_BODY_LIMIT // len(json.dumps(padded, separators=(",", ":")))) + 500 + payload = {"sections": [dict(padded, id=f"sec{i}") for i in range(count)]} + # Sent as the exact bytes measured, so the assertion cannot drift from what + # actually goes on the wire and quietly stop testing the ceiling. + raw = json.dumps(payload, separators=(",", ":")).encode() + assert len(raw) > _EDITOR_BODY_LIMIT + + r = client.patch( + f"/api/jobs/{done_job.id}/sections", + content=raw, + headers={"Content-Type": "application/json"}, + ) + + assert r.status_code == 413 + assert done_job.sections is None + + +def test_the_body_ceiling_clears_the_largest_legitimate_editor_payload(client, done_job): + """The ceiling must never be reachable by a real track. 10000 sections at + the longest permitted name is about 1.6 MB against a 4 MB ceiling.""" + from app.main import _EDITOR_BODY_LIMIT + + payload = { + "sections": [ + dict(_section(i), id=f"sec{i}", name="V" * 64) for i in range(jobs_api._MAX_SECTIONS) + ] + } + raw = json.dumps(payload, separators=(",", ":")).encode() + assert len(raw) < _EDITOR_BODY_LIMIT + + r = client.patch( + f"/api/jobs/{done_job.id}/sections", + content=raw, + headers={"Content-Type": "application/json"}, + ) + assert r.status_code == 200 + + def test_sections_unknown_job_returns_404(client): payload = {"sections": []} r = client.patch("/api/jobs/000000000000/sections", json=payload) From 1f26bf666be344547416925615333f3ff0278e7f Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sat, 29 Aug 2026 00:41:04 +0100 Subject: [PATCH 6/7] Import app.api.jobs one way in its test file The new section tests reached for the module through a module-level alias while every other test in the file imports it locally inside the test body, so the same module was being imported in two styles. Fixed by following the file rather than the newcomer: the alias is gone and the three new tests import it the way the other fourteen already do. The reverse would have been a larger diff across tests this branch has no business touching. Behaviour is identical either way; both names bind the same module object, so monkeypatch sees no difference. --- tests/test_jobs_api.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_jobs_api.py b/tests/test_jobs_api.py index cdbc8b77..3fe810a8 100644 --- a/tests/test_jobs_api.py +++ b/tests/test_jobs_api.py @@ -7,7 +7,6 @@ import pytest from fastapi.testclient import TestClient -from app.api import jobs as jobs_api from app.core.config import MAX_PENDING_UPLOAD_JOBS, MAX_PENDING_URL_JOBS from app.core.models import Job from app.core.registry import _jobs @@ -356,7 +355,9 @@ def test_sections_rejects_a_list_long_enough_to_stall_the_server(client, done_jo """The body is parsed before the handler runs, so an unbounded list holds the event loop and every other request with it (#481). A 33 MB body stalled an idle server's health check from 31 ms to 3.8 seconds.""" - payload = {"sections": [_section(i) for i in range(jobs_api._MAX_SECTIONS + 1)]} + import app.api.jobs as jobs_mod + + payload = {"sections": [_section(i) for i in range(jobs_mod._MAX_SECTIONS + 1)]} r = client.patch(f"/api/jobs/{done_job.id}/sections", json=payload) @@ -367,7 +368,9 @@ def test_sections_rejects_a_list_long_enough_to_stall_the_server(client, done_jo def test_sections_cap_clears_the_longest_legitimate_track(client, done_job): """0.5 s is the shortest section either the editor or normalize_sections allows, so a 3600 s track tops out at 7200. The cap must not reject that.""" - assert jobs_api._MAX_SECTIONS >= 3600 / 0.5 + import app.api.jobs as jobs_mod + + assert jobs_mod._MAX_SECTIONS >= 3600 / 0.5 payload = {"sections": [_section(i) for i in range(7200)]} @@ -406,11 +409,12 @@ def test_oversized_editor_body_is_refused_before_it_is_parsed(client, done_job): def test_the_body_ceiling_clears_the_largest_legitimate_editor_payload(client, done_job): """The ceiling must never be reachable by a real track. 10000 sections at the longest permitted name is about 1.6 MB against a 4 MB ceiling.""" + import app.api.jobs as jobs_mod from app.main import _EDITOR_BODY_LIMIT payload = { "sections": [ - dict(_section(i), id=f"sec{i}", name="V" * 64) for i in range(jobs_api._MAX_SECTIONS) + dict(_section(i), id=f"sec{i}", name="V" * 64) for i in range(jobs_mod._MAX_SECTIONS) ] } raw = json.dumps(payload, separators=(",", ":")).encode() From cafd9b67e85402f8d4a2e69b8580b80347f0a9d2 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sat, 29 Aug 2026 00:49:03 +0100 Subject: [PATCH 7/7] Reach app.api.jobs the same way in its last test The real source of the mixed import style in this file was one line the review bots never pointed at: a test that did "from app.api.jobs import _write_json_atomic" while the other sixteen tests bind the module and go through it. That test now binds the module too, so the file has one style and no exceptions. The bots flagged the sixteen and suggested converting two of them, which would have left fourteen the other way. Behaviour is unchanged; both forms resolve to the same function object. --- tests/test_jobs_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_jobs_api.py b/tests/test_jobs_api.py index 3fe810a8..548bc324 100644 --- a/tests/test_jobs_api.py +++ b/tests/test_jobs_api.py @@ -499,10 +499,10 @@ def fail_write(*_args, **_kwargs): def test_atomic_section_metadata_write_leaves_no_temporary_file(tmp_path): - from app.api.jobs import _write_json_atomic + import app.api.jobs as jobs_mod path = tmp_path / "metadata.json" - _write_json_atomic(path, {"sections_source": "manual"}) + jobs_mod._write_json_atomic(path, {"sections_source": "manual"}) assert json.loads(path.read_text(encoding="utf-8"))["sections_source"] == "manual" assert not list(tmp_path.glob(".metadata.json.*.tmp"))